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,423 @@
/*global ActivityClient,
ActivityHandler,
App,
Attachment,
Drafts,
MessageManager,
MocksHelper,
Navigation,
Settings,
Utils
*/
'use strict';
require('/views/shared/test/unit/mock_attachment.js');
require('/services/test/unit/mock_message_manager.js');
require('/services/test/unit/activity/mock_activity_shim.js');
require('/services/test/unit/activity/mock_activity_client.js');
require('/views/shared/test/unit/mock_app.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/test/unit/mock_navigation.js');
require('/services/test/unit/mock_drafts.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/js/activity_handler.js');
var mocksHelperForActivityHandler = new MocksHelper([
'ActivityClient',
'ActivityShim',
'App',
'Attachment',
'Draft',
'Drafts',
'MessageManager',
'Navigation',
'Settings',
'Utils'
]).init();
suite('ActivityHandler', function() {
mocksHelperForActivityHandler.attachTestHelpers();
setup(function() {
this.sinon.stub(Utils, 'onceDocumentIsVisible').returns(Promise.resolve());
this.sinon.stub(Utils, 'alert').returns(Promise.resolve());
this.sinon.stub(ActivityClient);
});
test('init()', function() {
ActivityHandler.init();
sinon.assert.calledOnce(ActivityClient.init);
sinon.assert.calledWith(ActivityClient.init, App.instanceId);
sinon.assert.calledWith(ActivityClient.on, 'new-activity-request');
sinon.assert.calledWith(ActivityClient.on, 'share-activity-request');
});
suite('"share" activity', function() {
var activityData;
function onceShareActivityCompleted() {
sinon.assert.called(ActivityHandler._onShareActivity);
return ActivityHandler._onShareActivity.lastCall.returnValue;
}
setup(function() {
this.sinon.spy(ActivityHandler, '_onShareActivity');
this.sinon.spy(Navigation, 'toPanel');
this.sinon.spy(Drafts, 'add');
this.sinon.spy(Drafts, 'store');
this.sinon.spy(Drafts, 'request');
activityData = {
type: 'video/*',
blobs: [
new Blob(['test'], { type: 'video/x-video' }),
new Blob(['test2'], { type: 'video/x-video' }),
new Blob(),
new Blob(),
new Blob()
],
filenames: [
'testBlob1', 'testBlob2', 'testBlob3', 'testBlob4', 'testBlob5'
]
};
ActivityHandler.init();
});
test('moves to the composer panel after saving draft', function(done) {
ActivityClient.on.withArgs('share-activity-request').yield(
activityData
);
onceShareActivityCompleted().then(() => {
sinon.assert.calledWithMatch(Drafts.add, {
recipients: null,
type: 'mms',
content: [
sinon.match.instanceOf(Attachment),
sinon.match.instanceOf(Attachment),
sinon.match.instanceOf(Attachment),
sinon.match.instanceOf(Attachment),
sinon.match.instanceOf(Attachment)
]
});
sinon.assert.called(Drafts.store);
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: sinon.match.falsy }
);
}).then(done, done);
});
test('Attachment size over max mms should not be appended', function(done) {
// Adjust mmsSizeLimitation for verifying alert popup when size over
// limitation.
Settings.mmsSizeLimitation = 1;
ActivityClient.on.withArgs('share-activity-request').yield(
activityData
);
sinon.assert.calledWith(Utils.alert, {
id: 'attached-files-too-large',
args: { n: 5, mmsSize: '0' }
});
Utils.alert.lastCall.returnValue.then(() => {
sinon.assert.calledWithExactly(ActivityClient.postResult);
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
test('Should append images even when they are big', function(done) {
activityData.blobs = [new Blob(['test'], { type: 'image/jpeg' })];
Settings.mmsSizeLimitation = 1;
ActivityClient.on.withArgs('share-activity-request').yield(
activityData
);
sinon.assert.notCalled(Utils.alert);
onceShareActivityCompleted().then(() => {
sinon.assert.calledWithMatch(Drafts.add, {
recipients: null,
type: 'mms',
content: [sinon.match.instanceOf(Attachment)]
});
sinon.assert.called(Drafts.store);
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: sinon.match.falsy }
);
}).then(done, done);
});
test('Should append vcard attachment', function(done) {
activityData.blobs = [new Blob(['test'], { type: 'text/x-vcard' })];
ActivityClient.on.withArgs('share-activity-request').yield(
activityData
);
sinon.assert.notCalled(Utils.alert);
onceShareActivityCompleted().then(() => {
sinon.assert.calledWithMatch(Drafts.add, {
recipients: null,
type: 'mms',
content: [sinon.match.instanceOf(Attachment)]
});
sinon.assert.called(Drafts.store);
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: sinon.match.falsy }
);
}).then(done, done);
});
test('Appends URL to the Compose field for activity with URL data type',
function(done) {
var urlActivityData = {
type: 'url',
url: 'test_url'
};
ActivityClient.on.withArgs('share-activity-request').yield(
urlActivityData
);
sinon.assert.notCalled(Utils.alert);
onceShareActivityCompleted().then(() => {
sinon.assert.calledWithMatch(Drafts.add, {
recipients: null,
type: 'sms',
content: [urlActivityData.url]
});
sinon.assert.called(Drafts.store);
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: sinon.match.falsy }
);
}).then(done, done);
});
test('Call activity postError if no data to share', function() {
ActivityClient.on.withArgs('share-activity-request').yield({
type: 'url'
});
sinon.assert.notCalled(Navigation.toPanel);
sinon.assert.notCalled(ActivityClient.postResult);
sinon.assert.calledOnce(ActivityClient.postError);
sinon.assert.calledWith(
ActivityClient.postError, 'No data to share found!'
);
});
test('Call activity postError on unknown activity data type', function() {
ActivityClient.on.withArgs('share-activity-request').yield({
type: 'multipart/mixed'
});
sinon.assert.notCalled(Navigation.toPanel);
sinon.assert.notCalled(ActivityClient.postResult);
sinon.assert.calledOnce(ActivityClient.postError);
sinon.assert.calledWith(
ActivityClient.postError,
'Unsupported activity data type: multipart/mixed'
);
});
});
suite('"new" activity', function() {
var emailActivityData;
function onceNewActivityCompleted() {
sinon.assert.called(ActivityHandler._onNewActivity);
return ActivityHandler._onNewActivity.lastCall.returnValue;
}
function assertDraftSaved(draft) {
sinon.assert.calledWithMatch(Drafts.add, draft);
sinon.assert.called(Drafts.store);
sinon.assert.callOrder(Drafts.request, Drafts.add, Drafts.store);
}
var threadDeferred;
setup(function() {
emailActivityData = {
target: 'abc@exmple.com',
body: 'foo'
};
// configure findThreadFromNumber
threadDeferred = Utils.Promise.defer();
this.sinon.stub(MessageManager, 'findThreadFromNumber').returns(
threadDeferred.promise
);
this.sinon.spy(Navigation, 'toPanel');
this.sinon.spy(Drafts, 'add');
this.sinon.spy(Drafts, 'store');
this.sinon.spy(Drafts, 'request');
this.sinon.spy(ActivityHandler, '_onNewActivity');
ActivityHandler.init();
});
test('Non-existing thread, with number and body', function(done) {
ActivityClient.on.withArgs('new-activity-request').yield({
number: '123',
body: 'foo'
});
// This should not be used but keeping it here to have a consistent
// environment for this test case.
threadDeferred.reject(new Error('No thread for this test'));
onceNewActivityCompleted().then(function() {
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
assertDraftSaved({
recipients: ['123'],
type: 'sms',
content: ['foo']
});
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: 'composer' }
);
}).then(done,done);
});
test('existing thread with specified body, navigate to composer',
function(done) {
ActivityClient.on.withArgs('new-activity-request').yield({
number: '123',
body: 'foo'
});
// This should not be used but keeping it here to have a consistent
// environment for this test case.
threadDeferred.resolve(42);
onceNewActivityCompleted().then(function() {
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
assertDraftSaved({
recipients: ['123'],
type: 'sms',
content: ['foo']
});
sinon.assert.calledWithMatch(
Navigation.toPanel,
'composer', { draftId: 'draftId' }
);
}).then(done,done);
});
test('new message with body only', function(done) {
ActivityClient.on.withArgs('new-activity-request').yield(
{ body: 'foo' }
);
onceNewActivityCompleted().then(() => {
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
assertDraftSaved({
recipients: null,
type: 'sms',
content: ['foo']
});
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: sinon.match.falsy }
);
}).then(done,done);
});
test('Non-existing thread with recipient only, navigate to composer',
function(done) {
ActivityClient.on.withArgs('new-activity-request').yield({
number: '123'
});
threadDeferred.reject(new Error('No thread for this test'));
onceNewActivityCompleted().then(function() {
sinon.assert.calledWith(MessageManager.findThreadFromNumber, '123');
assertDraftSaved({
recipients: ['123'],
type: 'sms',
content: null
});
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: 'composer' }
);
}).then(done,done);
});
test('new message with email and body', function(done) {
ActivityClient.on.withArgs('new-activity-request').yield(
emailActivityData
);
// This is not used but it's kept here for consistency.
threadDeferred.reject(new Error('No thread for this test'));
onceNewActivityCompleted().then(function() {
sinon.assert.notCalled(MessageManager.findThreadFromNumber);
assertDraftSaved({
recipients: [emailActivityData.target],
type: 'mms',
content: [emailActivityData.body]
});
sinon.assert.calledWith(
Navigation.toPanel,
'composer',
{ draftId: 'draftId', focus: 'composer' }
);
}).then(done,done);
});
test('existing thread with recipient only, navigate to the thread',
function(done) {
ActivityClient.on.withArgs('new-activity-request').yield({
number: '123'
});
// this time we can find a thread with id=42
threadDeferred.resolve(42);
onceNewActivityCompleted().then(function() {
sinon.assert.calledWithMatch(
Navigation.toPanel, 'thread', { id: 42, focus: 'composer' }
);
}).then(done,done);
});
});
});

View file

@ -0,0 +1,255 @@
/*global MocksHelper, MockL10n, ActivityPicker, MozActivity */
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/js/activity_picker.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_moz_activity.js');
require('/views/shared/test/unit/mock_utils.js');
var mocksHelperAP = new MocksHelper([
'MozActivity',
'Utils'
]).init();
suite('ActivityPicker', function() {
var realMozL10n, onsuccess, onerror;
suiteSetup(function() {
realMozL10n = document.l10n;
document.l10n = MockL10n;
mocksHelperAP.suiteSetup();
onsuccess = function() {};
onerror = function() {};
});
suiteTeardown(function() {
document.l10n = realMozL10n;
mocksHelperAP.suiteTeardown();
});
setup(function() {
mocksHelperAP.setup();
});
teardown(function() {
mocksHelperAP.teardown();
});
suite('dial', function() {
test('dial(number) ', function() {
ActivityPicker.dial('999');
assert.deepEqual(MozActivity.calls[0], {
name: 'dial',
data: { type: 'webtelephony/number', number: '999' }
});
});
test('dial(number, success) ', function() {
ActivityPicker.dial('999', onsuccess);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.deepEqual(MozActivity.calls[0], {
name: 'dial',
data: { type: 'webtelephony/number', number: '999' }
});
});
test('dial(number, success, error) ', function() {
ActivityPicker.dial('999', onsuccess, onerror);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.equal(
MozActivity.instances[0].onerror, onerror
);
assert.deepEqual(MozActivity.calls[0], {
name: 'dial',
data: { type: 'webtelephony/number', number: '999' }
});
});
});
suite('email', function() {
test('email(email) ', function() {
ActivityPicker.email('a@b.com');
assert.deepEqual(MozActivity.calls[0], {
name: 'new',
data: {
type: 'mail',
URI: 'mailto:a@b.com'
}
});
});
test('email(email, success) ', function() {
ActivityPicker.email('a@b.com', onsuccess);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.deepEqual(MozActivity.calls[0], {
name: 'new',
data: {
type: 'mail',
URI: 'mailto:a@b.com'
}
});
});
test('email(email, success, error) ', function() {
ActivityPicker.email('a@b.com', onsuccess, onerror);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.equal(
MozActivity.instances[0].onerror, onerror
);
assert.deepEqual(MozActivity.calls[0], {
name: 'new',
data: {
type: 'mail',
URI: 'mailto:a@b.com'
}
});
});
});
suite('url', function() {
test('url(url) ', function() {
ActivityPicker.url('http://mozilla.com');
assert.deepEqual(MozActivity.calls[0], {
name: 'view',
data: {
type: 'url',
url: 'http://mozilla.com'
}
});
});
test('url(url, success) ', function() {
ActivityPicker.url('http://mozilla.com', onsuccess);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.deepEqual(MozActivity.calls[0], {
name: 'view',
data: {
type: 'url',
url: 'http://mozilla.com'
}
});
});
test('url(url, success, error) ', function() {
ActivityPicker.url('http://mozilla.com', onsuccess, onerror);
assert.equal(
MozActivity.instances[0].onsuccess, onsuccess
);
assert.equal(
MozActivity.instances[0].onerror, onerror
);
assert.deepEqual(MozActivity.calls[0], {
name: 'view',
data: {
type: 'url',
url: 'http://mozilla.com'
}
});
});
});
test('createNewContact', function() {
ActivityPicker.createNewContact({foo: ['bar']});
assert.deepEqual(MozActivity.calls[0], {
name: 'new',
data: {
type: 'webcontacts/contact',
params: { foo: ['bar'] }
}
});
});
test('addToExistingContact', function() {
ActivityPicker.addToExistingContact({foo: ['bar']});
assert.deepEqual(MozActivity.calls[0], {
name: 'update',
data: {
type: 'webcontacts/contact',
params: { foo: ['bar'] }
}
});
});
test('viewContact', function() {
ActivityPicker.viewContact({foo: ['bar']});
assert.deepEqual(MozActivity.calls[0], {
name: 'open',
data: {
type: 'webcontacts/contact',
params: { foo: ['bar'] }
}
});
});
suite('openSettings', function() {
var params = {
name: 'configure',
data: {
target: 'device',
section: 'messaging'
}
};
test('openSettings() ', function() {
ActivityPicker.openSettings();
assert.deepEqual(MozActivity.calls[0], params);
});
test('openSettings(success) ', function() {
ActivityPicker.openSettings(onsuccess);
assert.equal(MozActivity.instances[0].onsuccess, onsuccess);
assert.deepEqual(MozActivity.calls[0], params);
});
test('openSettings(success, error) ', function() {
ActivityPicker.openSettings(onsuccess, onerror);
assert.equal(MozActivity.instances[0].onsuccess, onsuccess);
assert.equal(MozActivity.instances[0].onerror, onerror);
assert.deepEqual(MozActivity.calls[0], params);
});
});
});

View file

@ -0,0 +1,89 @@
/*global App, Promise */
'use strict';
require('/views/shared/js/app.js');
suite('App', function() {
// Taken from app.js
const APPLICATION_READY_CLASS_NAME = 'js-app-ready';
teardown(function() {
document.body.classList.remove(APPLICATION_READY_CLASS_NAME);
});
test('isReady is false by default', function() {
assert.equal(App.isReady(), false);
});
test('setReady sets body class and isReady', function() {
App.setReady();
assert.equal(App.isReady(), true);
assert.ok(document.body.classList.contains(APPLICATION_READY_CLASS_NAME));
});
test('setReady throws exception if called more than once', function() {
App.setReady();
assert.throws(function() {
App.setReady();
});
});
test('whenReady is resolved immediately if app is ready', function(done) {
App.setReady();
App.whenReady().then(function() {
assert.ok(App.isReady());
}).then(done, done);
});
test('whenReady is resolved when setReady is called', function(done) {
var stub = sinon.stub();
this.sinon.spy(App, 'setReady');
var whenReadyPromise = App.whenReady();
whenReadyPromise.then(stub).then(function() {
assert.ok(App.isReady());
sinon.assert.callOrder(App.setReady, stub);
}).then(done, done);
Promise.resolve().then(function() {
App.setReady();
});
});
test('whenReady is rejected in case of error', function(done) {
var error = new Error('Test error');
this.sinon.stub(MutationObserver.prototype, 'observe', function() {
throw error;
});
App.whenReady().then(function() {
throw new Error('Success callback should not have been called.');
}, function(e) {
assert.equal(e, error);
}).then(done, done);
App.setReady();
});
test('whenReady is rejected in case of error in MutationObserver callback',
function(done) {
var error = new Error('Test error');
// This is called inside MutationObserver callback
this.sinon.stub(MutationObserver.prototype, 'disconnect', function() {
throw error;
});
App.whenReady().then(function() {
throw new Error('Success callback should not have been called.');
}, function(e) {
assert.equal(e, error);
}).then(done, done);
App.setReady();
});
});

View file

@ -0,0 +1,61 @@
/*global Promise */
/*exported AssetsHelper */
'use strict';
var AssetsHelper = {
generateCanvas: function(width, height) {
var canvas = document.createElement('canvas'),
context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
var linearGradient = context.createLinearGradient(0, 0, width, height);
linearGradient.addColorStop(0, 'blue');
linearGradient.addColorStop(1, 'red');
context.fillStyle = linearGradient;
context.fillRect (0, 0, width, height);
return canvas;
},
generateImageDataURL: function(width, height, type, quality) {
var canvas = null;
try {
canvas = this.generateCanvas(width, height);
return canvas.toDataURL(type, quality);
} finally {
canvas.width = canvas.height = 0;
canvas = null;
}
},
generateImageBlob: function(width, height, type, quality) {
var canvas = this.generateCanvas(width, height);
return new Promise(function(resolve) {
canvas.toBlob(function(blob) {
canvas.width = canvas.height = 0;
canvas = null;
resolve(blob);
}, type, quality);
});
},
loadFileBlob: function(filePath) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', filePath, true);
xhr.responseType = 'blob';
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onerror = reject;
xhr.send();
});
}
};

View file

@ -0,0 +1,891 @@
/*global ContactRenderer, loadBodyHTML, MockContact, MockL10n, MocksHelper,
Utils, Template, MockContactPhotoHelper, SharedComponents,
MockSettings,
AssetsHelper
*/
'use strict';
require('/views/shared/js/shared_components.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/test/unit/mock_contact.js');
require('/shared/test/unit/mocks/mock_l20n.js');
require('/shared/test/unit/mocks/mock_contact_photo_helper.js');
require('/views/shared/js/contact_renderer.js');
require('/views/shared/test/unit/mock_settings.js');
var mocksHelperForContactRenderer = new MocksHelper([
'Utils',
'ContactPhotoHelper',
'Settings'
]).init();
suite('ContactRenderer', function() {
var realMozL10n;
var ul, contact;
var testImageBlob;
var renderer, unknownRender;
mocksHelperForContactRenderer.attachTestHelpers();
suiteSetup(function(done) {
realMozL10n = document.l10n;
document.l10n = MockL10n;
AssetsHelper.generateImageBlob(400, 400, 'image/jpeg', 0.5).then(
(blob) => {
testImageBlob = blob;
}
).then(done, done);
});
suiteTeardown(function() {
document.l10n = realMozL10n;
realMozL10n = null;
});
setup(function() {
loadBodyHTML('/index.html');
this.sinon.spy(Template.prototype, 'interpolate');
ul = document.createElement('ul');
contact = MockContact();
});
suite('suggestion', function() {
setup(function() {
renderer = ContactRenderer.flavor('suggestion');
});
test('Rendered Contact "givenName familyName"', function() {
var html;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
carrier: 'TEF',
name: 'Pepito O\'Hare',
nameHTML: 'Pepito O'Hare',
number: '+346578888888',
phoneDetailsHTML: SharedComponents.phoneDetails(
{
number: '+346578888888',
type: 'Mobile',
carrier: 'TEF'
}
).toString(),
photoHTML: '',
type: 'Mobile'
});
html = ul.firstElementChild.innerHTML;
assert.include(html, 'Pepito O\'Hare');
});
test('Rendered Contact highlighted "givenName familyName"', function() {
var html;
renderer.render({
contact: contact,
input: 'Pepito O\'Hare',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.include(html, '<span class="highlight">Pepito</span>');
assert.include(html, '<span class="highlight">O\'Hare</span>');
});
test('Rendered Contact "number"', function() {
var html;
contact.tel[0].carrier = null;
contact.tel[0].type = null;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.ok(html.includes('+346578888888'));
});
test('Rendered Contact highlighted "number"', function() {
var html;
contact.tel[0].carrier = null;
contact.tel[0].type = null;
renderer.render({
contact: contact,
input: '346578888888',
target: ul
});
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
carrier: '',
name: 'Pepito O\'Hare',
nameHTML: 'Pepito O&apos;Hare',
number: '+346578888888',
phoneDetailsHTML: SharedComponents.phoneDetails(
{ number: '+<span class="highlight">346578888888</span>' },
{ safe: ['number'] }
).toString(),
photoHTML: '',
type: ''
});
html = ul.firstElementChild.innerHTML;
assert.ok(
html.includes('+<span class="highlight">346578888888</span>')
);
});
test('Rendered Contact highlighted "name number"', function() {
var html;
renderer.render({
contact: contact,
input: 'Pepito 346578888888',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.include(html, '<span class="highlight">Pepito</span>');
assert.include(html, '+<span class="highlight">346578888888</span>');
});
test('Rendered Contact with type and number', function() {
this.sinon.spy(SharedComponents, 'phoneDetails');
contact.tel[0].carrier = null;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
sinon.assert.calledWith(
SharedComponents.phoneDetails,
{
number: contact.tel[0].value,
type: contact.tel[0].type[0],
carrier: ''
},
{ safe: ['number'] }
);
});
test('Rendered Contact with type and highlighted number', function() {
this.sinon.spy(SharedComponents, 'phoneDetails');
contact.tel[0].carrier = null;
renderer.render({
contact: contact,
input: '346578888888',
target: ul
});
sinon.assert.calledWith(
SharedComponents.phoneDetails,
{
number: '+<span class="highlight">346578888888</span>',
type: contact.tel[0].type[0],
carrier: ''
},
{ safe: ['number'] }
);
});
test('Rendered Contact with type, number and carrier', function() {
this.sinon.spy(SharedComponents, 'phoneDetails');
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
sinon.assert.calledWith(
SharedComponents.phoneDetails,
{
number: contact.tel[0].value,
type: contact.tel[0].type[0],
carrier: contact.tel[0].carrier
},
{ safe: ['number'] }
);
});
test('Rendered Contact with type, highlighted number and carrier',
function() {
this.sinon.spy(SharedComponents, 'phoneDetails');
renderer.render({
contact: contact,
input: '346578888888',
target: ul
});
sinon.assert.calledWith(
SharedComponents.phoneDetails,
{
number: '+<span class="highlight">346578888888</span>',
type: contact.tel[0].type[0],
carrier: contact.tel[0].carrier
},
{ safe: ['number'] }
);
});
test('Rendered Contact "email"', function() {
MockSettings.supportEmailRecipient = true;
var html;
contact.email[0].type = null;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
html = ul.lastElementChild.innerHTML;
assert.ok(html.includes('a@b.com'));
});
test('Rendered Contact highlighted "email"', function() {
MockSettings.supportEmailRecipient = true;
var html;
contact.email[0].type = null;
renderer.render({
contact: contact,
input: 'a@b.com',
target: ul
});
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
carrier: '',
name: 'Pepito O\'Hare',
nameHTML: 'Pepito O&apos;Hare',
number: 'a@b.com',
numberHTML: '<span class="highlight">a@b.com</span>',
photoHTML: '',
type: ''
});
html = ul.lastElementChild.innerHTML;
assert.ok(
html.includes('<span class="highlight">a@b.com</span>')
);
});
test('Rendered Contact highlighted "name email"', function() {
MockSettings.supportEmailRecipient = true;
var html;
renderer.render({
contact: contact,
input: 'Pepito a@b.com',
target: ul
});
html = ul.lastElementChild.innerHTML;
assert.include(html, '<span class="highlight">Pepito</span>');
assert.include(html, '<span class="highlight">a@b.com</span>');
});
test('Rendered Contact "type | email"', function() {
var html;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
html = ul.lastElementChild.innerHTML;
assert.isFalse(html.includes(
'<span data-l10n-id="Personal">Personal</span> | ' + 'a@b.com'));
});
test('Rendered Contact highlighted "type | email"', function() {
var html;
renderer.render({
contact: contact,
input: 'a@b.com',
target: ul
});
html = ul.lastElementChild.innerHTML;
assert.isFalse(html.includes(
'<span data-l10n-id="Personal">Personal</span> | ' +
'<span class="highlight">a@b.com</span>'
));
});
test('Rendered Contact w/ multiple: all (isSuggestion) ' +
'and No Support EmailRecipient ',
function() {
MockSettings.supportEmailRecipient = false;
renderer.render({
contact: contact,
input: '+12125559999',
target: ul
});
assert.equal(ul.children.length, 2);
});
test('Rendered Contact w/ multiple: all (isSuggestion) ' +
'and Support EmailRecipient ',
function() {
MockSettings.supportEmailRecipient = true;
renderer.render({
contact: contact,
input: '+12125559999',
target: ul
});
assert.equal(ul.children.length, 3);
});
test('Rendered Contact omit numbers already in recipient list ' +
'and No Support EmailRecipient ',
function() {
MockSettings.supportEmailRecipient = false;
var html;
var skip = ['+346578888888'];
// This contact has two tel entries.
renderer.render({
contact: contact,
input: '+346578888888',
target: ul,
skip: skip
});
html = ul.innerHTML;
assert.ok(!html.includes('346578888888'));
assert.equal(ul.children.length, 1);
});
test('Rendered Contact omit numbers already in recipient list ' +
'and Support EmailRecipient ',
function() {
MockSettings.supportEmailRecipient = true;
var html;
var skip = ['+346578888888'];
// This contact has two tel entries.
renderer.render({
contact: contact,
input: '+346578888888',
target: ul,
skip: skip
});
html = ul.innerHTML;
assert.ok(!html.includes('346578888888'));
assert.equal(ul.children.length, 2);
});
test('Rendered Contact omit emails already in recipient list ' +
'and Support EmailRecipient ',
function() {
MockSettings.supportEmailRecipient = true;
var html;
var skip = ['a@b.com'];
// This contact has three tel entries.
renderer.render({
contact: contact,
input: 'a@b.com',
target: ul,
skip: skip
});
html = ul.innerHTML;
assert.ok(!html.includes('a@b.com'));
assert.equal(ul.children.length, 2);
});
test('does not include photo', function() {
var html;
renderer.render({
contact: contact,
input: 'foo',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.isFalse(html.includes('span[data-type=img]'));
});
test('append information block in the li', function() {
var li;
var block = document.createElement('div');
var selector = '.suggestion';
renderer.render({
contact: contact,
input: 'Additional info',
infoBlock: block,
infoBlockParentSelector: selector,
target: ul
});
li = ul.lastElementChild;
assert.isTrue(!!li.querySelector(selector));
assert.equal(li.querySelector(selector).lastElementChild, block);
});
test('Rendered no "Tel" and No Support EmailRecipient ', function() {
contact.tel = null;
MockSettings.supportEmailRecipient = false;
var result = renderer.render({
contact: contact,
input: 'foo',
target: ul
});
assert.isFalse(result);
});
test('Rendered no "email" and No Support EmailRecipient ', function() {
contact.email = null;
MockSettings.supportEmailRecipient = false;
var result = renderer.render({
contact: contact,
input: 'foo',
target: ul
});
assert.ok(result);
});
test('Rendered no "Tel" and Support EmailRecipient ', function() {
contact.tel = null;
MockSettings.supportEmailRecipient = true;
var result = renderer.render({
contact: contact,
input: 'foo',
target: ul
});
assert.ok(result);
});
test('Rendered no "email" and Support EmailRecipient ', function() {
contact.email = null;
MockSettings.supportEmailRecipient = true;
var result = renderer.render({
contact: contact,
input: 'foo',
target: ul
});
assert.ok(result);
});
test('Rendered no "Tel"/"email" and Support EmailRecipient ', function() {
contact.tel = null;
contact.email = null;
MockSettings.supportEmailRecipient = true;
var result = renderer.render({
contact: contact,
input: 'foo',
target: ul
});
assert.isFalse(result);
});
});
suite('suggestionUnknown', function() {
var html;
var unknownContact = {
name: ['unknown'],
tel: [{value: '+346578888888'}]
};
setup(function() {
unknownRender = ContactRenderer.flavor('suggestionUnknown');
});
test('Rendered unknownContact "number"', function() {
unknownContact.tel[0].carrier = null;
unknownContact.tel[0].type = null;
unknownRender.render({
contact: unknownContact,
input: 'foo',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.ok(html.includes('+346578888888'));
});
test('Rendered unknownContact highlighted "number"', function() {
unknownContact.tel[0].carrier = null;
unknownContact.tel[0].type = null;
unknownRender.render({
contact: unknownContact,
input: '346578888888',
target: ul
});
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
carrier: '',
name: 'unknown',
nameHTML: 'unknown',
number: '+346578888888',
phoneDetailsHTML: SharedComponents.phoneDetails(
{ number: '+<span class="highlight">346578888888</span>' },
{ safe: ['number'] }
).toString(),
photoHTML: '',
type: ''
});
html = ul.firstElementChild.innerHTML;
assert.ok(
html.includes('+<span class="highlight">346578888888</span>')
);
});
test('Rendered unknownContact omit numbers already in recipient list',
function() {
var skip = ['+346578888888'];
// This unknownContact has two tel entries.
unknownRender.render({
contact: unknownContact,
input: '+346578888888',
target: ul,
skip: skip
});
html = ul.innerHTML;
assert.ok(!html.includes('346578888888'));
assert.equal(ul.children.length, 0);
});
test('does not include photo', function() {
unknownRender.render({
contact: unknownContact,
input: 'foo',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.isFalse(html.includes('span[data-type=img]'));
});
});
suite('prompt', function() {
setup(function() {
renderer = ContactRenderer.flavor('prompt');
});
test('Rendered Contact w/ multiple: one', function() {
renderer.render({
contact: contact,
input: '+12125559999',
target: ul
});
assert.equal(ul.children.length, 1);
});
test('Rendered Contact w/ multiple: one w/ minimal match', function() {
renderer.render({
contact: contact,
input: '5559999',
target: ul
});
assert.equal(ul.children.length, 1);
});
test('Render contact without photo does not render the photo', function() {
var html;
renderer.render({
contact: contact,
input: '5559999',
target: ul
});
html = ul.firstElementChild.innerHTML;
assert.isFalse(html.includes('span[data-type=img]'));
});
test('Render contact with photo renders the image', function() {
var html;
var blob = testImageBlob;
this.sinon.stub(MockContactPhotoHelper, 'getThumbnail').returns(blob);
this.sinon.spy(Utils, 'asyncLoadRevokeURL');
this.sinon.spy(Utils, 'getContactDetails');
this.sinon.spy(window, 'encodeURI');
renderer.render({
contact: contact,
input: '5559999',
target: ul
});
sinon.assert.calledWith(Template.prototype.interpolate, undefined);
var photo = 'data-type="img"';
sinon.assert.calledWithMatch(Template.prototype.interpolate, {
carrier: 'XXX',
name: 'Pepito O\'Hare',
nameHTML: 'Pepito O&apos;Hare',
number: '+12125559999',
phoneDetailsHTML: SharedComponents.phoneDetails(
{
number: '+12125559999',
type: 'B',
carrier: 'XXX'
},
{ safe: ['number'] }
).toString(),
photoHTML: sinon.match(photo),
type: 'B'
});
html = ul.firstElementChild.innerHTML;
var contactPhotoElement = ul.firstElementChild.querySelector(
'.contact-photo'
);
assert.ok(html.includes('span'));
assert.ok(contactPhotoElement.style.backgroundImage.indexOf('blob:') > 0);
sinon.assert.calledWith(
encodeURI,
Utils.getContactDetails.returnValues[0].photoURL
);
sinon.assert.calledWith(
Utils.asyncLoadRevokeURL,
Utils.getContactDetails.returnValues[0].photoURL
);
});
});
suite('Secure User Input', function() {
setup(function() {
this.sinon.stub(Utils, 'getContactDetails', function(number, contacts) {
return {
isContact: !!contacts,
title: number
};
});
});
test('+99', function() {
var ul = document.createElement('ul');
assert.doesNotThrow(function() {
var renderer = ContactRenderer.flavor('suggestion');
renderer.render({
contact: {
name: 'Spider Monkey',
tel: [{ value: '...' }]
},
input: '+99',
target: ul
});
});
sinon.assert.calledWith(Utils.getContactDetails, '...');
});
test('*67 [800]-555-1212', function() {
var ul = document.createElement('ul');
assert.doesNotThrow(function() {
var renderer = ContactRenderer.flavor('suggestion');
renderer.render({
contact: {
name: 'Spider Monkey',
tel: [{ value: '...' }]
},
input: '*67 [800]-555-1212',
target: ul
});
});
sinon.assert.calledWith(Utils.getContactDetails, '...');
});
test('\\^$*+?.', function() {
var ul = document.createElement('ul');
assert.doesNotThrow(function() {
var renderer = ContactRenderer.flavor('suggestion');
renderer.render({
contact: {
name: 'Spider Monkey',
tel: [{ value: '...' }]
},
input: '\\^$*+?.',
target: ul
});
});
sinon.assert.calledWith(Utils.getContactDetails, '...');
});
});
suite('Defensive Contact Rendering', function() {
test('has tel number', function() {
var ul = document.createElement('ul');
var contact = new MockContact();
var renderer = ContactRenderer.flavor('suggestion');
var isRendered = renderer.render({
contact: contact,
input: contact.tel[0].value,
target: ul
});
assert.isTrue(isRendered);
});
test('no tel number, has email address', function() {
MockSettings.supportEmailRecipient = true;
var ul = document.createElement('ul');
var contact = new MockContact();
contact.tel = null;
var renderer = ContactRenderer.flavor('suggestion');
var isRendered = renderer.render({
contact: contact,
input: contact.email[0].value,
target: ul
});
assert.isTrue(isRendered);
});
test('has tel number, no email address', function() {
MockSettings.supportEmailRecipient = true;
var ul = document.createElement('ul');
var contact = new MockContact();
contact.email = null;
var renderer = ContactRenderer.flavor('suggestion');
var isRendered = renderer.render({
contact: contact,
input: contact.tel[0].value,
target: ul
});
assert.isTrue(isRendered);
});
test('no tel number, no email address', function() {
MockSettings.supportEmailRecipient = true;
var ul = document.createElement('ul');
var contact = new MockContact();
contact.tel = null;
contact.email = null;
var renderer = ContactRenderer.flavor('suggestion');
var isNotRendered = renderer.render({
contact: contact,
input: null,
target: ul
});
assert.isFalse(isNotRendered);
});
});
suite('report-view', function() {
var renderer;
setup(function() {
renderer = ContactRenderer.flavor('report-view');
});
test('correct rendering', function() {
var selector = '.js-contact-info';
var infoBlock = document.createElement('div');
renderer.render({
contact: contact,
target: ul,
input: contact.tel[0].value,
infoBlock: infoBlock,
infoBlockParentSelector: selector,
});
var contactInfo = ul.querySelector(selector);
assert.isTrue(contactInfo.contains(infoBlock));
assert.notEqual(contactInfo.tagName, 'A');
});
});
suite('group-view', function() {
var renderer;
setup(function() {
renderer = ContactRenderer.flavor('group-view');
});
test('correct rendering', function() {
var selector = '.js-contact-info';
renderer.render({
contact: contact,
target: ul,
input: contact.tel[0].value
});
var contactInfo = ul.querySelector(selector);
assert.equal(contactInfo.tagName, 'A');
});
});
});

View file

@ -0,0 +1,890 @@
/*global MockContact, Contacts, fb, MockFbReaderUtilsObj,
MockSettings, MocksHelper */
'use strict';
require('/shared/test/unit/mocks/mock_moz_phone_number_service.js');
require('/shared/test/unit/mocks/mock_fb_reader_utils.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_contact.js');
require('/views/shared/js/contacts.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/test/unit/mock_utils.js');
var mocksHelperForContactsUnitTest = new MocksHelper([
'Settings',
'Utils'
]).init();
function withFilter(str) {
return sinon.match( { filterValue: str } );
}
suite('Contacts', function(done) {
mocksHelperForContactsUnitTest.attachTestHelpers();
var realFb = window.fb;
var targetFbNumber = '+34658789147';
var fbContactName = 'Carlos Facebook';
var targetLocalNumber = '+34698745123';
var targetLocalEmail = 'a@b.com';
var localContactName = 'Jose Local';
var notFoundNumber = '+34633789102';
var notFoundEmail = 'a@c.com';
suiteSetup(function() {
var mockReaderUtils = new MockFbReaderUtilsObj();
mockReaderUtils.targetFbNumber = targetFbNumber;
mockReaderUtils.fbContactName = fbContactName;
window.fb = mockReaderUtils;
});
setup(function(){
// Do not use the Native API
this.sinon.stub(navigator.mozContacts, 'find');
});
suiteTeardown(function() {
window.fb = realFb;
});
suite('Contacts.findContactByString, single-term', function() {
test('(string[tel,givenName,familyName], ...) Match', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('O\'Hare')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('O\'Hare').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('(string[tel,givenName,familyName], ...) No Match', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('wontmatch')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('wontmatch').then(function(contacts) {
// contacts were not found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('(string[tel,email,givenName,familyName], ...) Match', function(done) {
MockSettings.supportEmailRecipient = true;
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('a@b.com')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('a@b.com').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('(string[tel,email,givenName,familyName], ...) No Match',
function(done) {
MockSettings.supportEmailRecipient = true;
navigator.mozContacts.find.withArgs(
withFilter('z@y.com')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('z@y.com').then(function(contacts) {
// contacts were not found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('(string[tel], ...) Match', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('+346578888888')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('+346578888888').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('(string[tel], ...) No Match', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('911')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('911').then(function(contacts) {
// contacts were not found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
});
suite('Contacts.findContactByString, multi-term', function() {
test('no predominate', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('Pepito O\'Hare').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('no predominate, reversed', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('O\'Hare')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('O\'Hare Pepito').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('predominate first, upper', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepi')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('Pepi O').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('predominate last, upper', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepi')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('O Pepi').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('predominate first, lower', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('pepi')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('pepi o').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('predominate last, lower', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('pepi')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('o pepi').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('multi-word name', function(done) {
var result = MockContact.list([
{ givenName: ['Mary Anne'], familyName: ['Jones'] }
]);
navigator.mozContacts.find.withArgs(
withFilter('mary')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('mary anne').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('name first, part of tel number last', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('Pepito 8888').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('part of tel number first, name last', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('8888 Pepito').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('name first, part of email address last', function(done) {
MockSettings.supportEmailRecipient = true;
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve(result)
);
Contacts.findByString('Pepito a@b').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('part of email address first, name last', function(done) {
MockSettings.supportEmailRecipient = true;
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve(result)
);
Contacts.findByString('a@b Pepito').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('string search yields a contact without familyName', function(done) {
var result = MockContact.list([
{ givenName: ['julien'] }
]);
navigator.mozContacts.find.withArgs(
withFilter('julien')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('julien 123').then((contacts) => {
// "julien" yields a result that has only a givenName, no familyName.
// This test checks that our algorithm works also in such cases.
// See Bug 952533
assert.isNotNull(contacts);
assert.lengthOf(contacts, 0);
}).then(done, done);
});
test('no matches, predominate first, upper', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('Pepito S').then(function(contacts) {
// No contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('no matches, predominate last, upper', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('Pepito')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('S Pepito').then(function(contacts) {
// No contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('no matches, predominate first, lower', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('pepi')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('pepi s').then(function(contacts) {
// No contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('no matches, predominate last, lower', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('pepi')
).returns(
Promise.resolve([])
);
Contacts.findContactByString('s pepi').then(function(contacts) {
// No contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
});
suite('Contacts.findExact', function() {
test('yields a match ', function(done) {
var result = MockContact.list([
{ givenName: ['Jane'], familyName: ['Doozer'] }
]);
navigator.mozContacts.find.withArgs(
withFilter('doozer')
).returns(
Promise.resolve(result)
);
Contacts.findExact('jane doozer').then(function(contacts) {
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('yields no matches ', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('doozer')
).returns(
Promise.resolve([])
);
Contacts.findExact('j doozer').then(function(contacts) {
assert.equal(contacts.length, 0);
}).then(done, done);
});
});
suite('Contacts.findByPhoneNumber', function() {
test('removes spaces', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('+33123456789')
).returns(
Promise.resolve(result)
);
Contacts.findByPhoneNumber('+33 1 23 45 67 89').then(function(contacts){
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('The mozContacts find() call returned an error', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('callonerror')
).returns(
Promise.resolve([])
);
Contacts.findByPhoneNumber('callonerror').then((results) => {
assert.isArray(results);
assert.lengthOf(results, 0);
}).then(done, done);
});
test('Local number found.', function(done) {
var result = [{
name: [localContactName]
}];
navigator.mozContacts.find.withArgs(
withFilter(targetLocalNumber)
).returns(
Promise.resolve(result)
);
Contacts.findByPhoneNumber(targetLocalNumber).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.isTrue(!contacts[0].isFbContact);
assert.equal(contacts[0].name[0], localContactName);
}).then(done, done);
});
test('Local number not found. FB Number found', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(targetFbNumber)
).returns(
Promise.resolve([])
);
Contacts.findByPhoneNumber(targetFbNumber).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.equal(contacts[0].name[0], fbContactName);
assert.equal(contacts[0].isFbContact, true);
}).then(done, done);
});
test('Local number not found. FB Number not found either', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(notFoundNumber)
).returns(
Promise.resolve([])
);
Contacts.findByPhoneNumber(notFoundNumber).then(function(contacts) {
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('Local number not found. FB returns error', function(done) {
fb.inError = true;
navigator.mozContacts.find.withArgs(
withFilter(targetFbNumber)
).returns(
Promise.resolve([])
);
Contacts.findByPhoneNumber(targetFbNumber).then(function(contacts) {
assert.equal(contacts.length, 0);
delete window.fb.inError;
}).then(done, done);
});
});
suite('Contacts.findByAddress', function() {
test('removes spaces', function(done) {
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('+33123456789')
).returns(
Promise.resolve(result)
);
Contacts.findByAddress('+33 1 23 45 67 89').then(function(contacts){
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('The mozContacts find() call returned an error', function(done) {
navigator.mozContacts.find.withArgs(
withFilter('callonerror')
).returns(
Promise.resolve([])
);
Contacts.findByAddress('callonerror').then((results) => {
assert.isArray(results);
assert.lengthOf(results, 0);
}).then(done, done);
});
test('Local number found.', function(done) {
var result = [{
name: [localContactName]
}];
navigator.mozContacts.find.withArgs(
withFilter(targetLocalNumber)
).returns(
Promise.resolve(result)
);
Contacts.findByAddress(targetLocalNumber).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.isTrue(!contacts[0].isFbContact);
assert.equal(contacts[0].name[0], localContactName);
}).then(done, done);
});
test('Local email found.', function(done) {
var result = [{
name: [localContactName]
}];
navigator.mozContacts.find.withArgs(
withFilter(targetLocalEmail)
).returns(
Promise.resolve(result)
);
Contacts.findByAddress(targetLocalEmail).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.isTrue(!contacts[0].isFbContact);
assert.equal(contacts[0].name[0], localContactName);
}).then(done, done);
});
test('Local number not found. FB Number found', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(targetFbNumber)
).returns(
Promise.resolve([])
);
Contacts.findByAddress(targetFbNumber).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.equal(contacts[0].name[0], fbContactName);
assert.equal(contacts[0].isFbContact, true);
}).then(done, done);
});
test('Local number not found. FB Number not found either', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(notFoundNumber)
).returns(
Promise.resolve([])
);
Contacts.findByAddress(notFoundNumber).then(function(contacts) {
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('Local email not found. FB Number not found either', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(notFoundEmail)
).returns(
Promise.resolve([])
);
Contacts.findByAddress(notFoundEmail).then(function(contacts) {
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('Local number not found. FB returns error', function(done) {
fb.inError = true;
navigator.mozContacts.find.withArgs(
withFilter(targetFbNumber)
).returns(
Promise.resolve([])
);
Contacts.findByAddress(targetFbNumber).then(function(contacts) {
assert.equal(contacts.length, 0);
delete window.fb.inError;
}).then(done, done);
});
});
suite('Contacts.findExactByEmail', function() {
test('Local email found.', function(done) {
var result = [{
name: [localContactName]
}];
navigator.mozContacts.find.withArgs(
withFilter(targetLocalEmail)
).returns(
Promise.resolve(result)
);
Contacts.findExactByEmail(targetLocalEmail).then(function(contacts) {
assert.equal(contacts.length, 1);
assert.isTrue(!contacts[0].isFbContact);
assert.equal(contacts[0].name[0], localContactName);
}).then(done, done);
});
test('Local email not found. FB Email not found either', function(done) {
navigator.mozContacts.find.withArgs(
withFilter(notFoundEmail)
).returns(
Promise.resolve([])
);
Contacts.findExactByEmail(notFoundEmail).then(function(contacts) {
assert.equal(contacts.length, 0);
}).then(done, done);
});
test('Local email not found. FB returns error', function(done) {
fb.inError = true;
var result = [{
name: [localContactName]
}];
navigator.mozContacts.find.withArgs(
withFilter(targetLocalEmail)
).returns(
Promise.resolve(result)
);
Contacts.findExactByEmail(targetLocalEmail).then(function(contacts) {
assert.equal(contacts.length, 1);
delete window.fb.inError;
}).then(done, done);
});
});
suite('Contacts.addUnknown', function() {
test('For adding an unknown contact', function() {
var length = Contacts.getunknownLength();
Contacts.addUnknown('123456');
assert.equal(length + 1, Contacts.getunknownLength());
});
test('For not adding the duplicate unknown contact', function() {
var length = Contacts.getunknownLength();
Contacts.addUnknown('123456');
assert.equal(length, Contacts.getunknownLength());
});
});
suite('Contacts.clearUnknown', function() {
test('For clearing unknown contact', function() {
Contacts.clearUnknown();
var length = Contacts.getunknownLength();
assert.equal(length, 0);
});
});
suite('Contacts.findByUnknown function', function() {
setup(function() {
Contacts.clearUnknown();
Contacts.addUnknown('123123456');
Contacts.addUnknown('789789789');
Contacts.addUnknown('456456456');
Contacts.addUnknown('123456789');
});
test('checking single unknown contact using substring', function(done) {
Contacts.findByUnknown('6456').then(function(list) {
assert.equal(list[0].name == '456456456', true);
assert.equal(list.length, 1);
}).then(done, done);
});
test('checking multiple unknown contact using substring', function(done) {
Contacts.findByUnknown('456').then(function(list) {
assert.equal(list[0].name == '123123456', true);
assert.equal(list[1].name == '456456456', true);
assert.equal(list[2].name == '123456789', true);
assert.equal(list.length, 3);//List can have at max 3 unknown elements
}).then(done, done);
});
test('checking multiple unknown contact using fullstring', function(done) {
Contacts.findByUnknown('456456456').then(function(list) {
assert.equal(list[0].name == '456456456', true);
assert.equal(list.length, 1);
}).then(done, done);
});
test('checking unknown contact after clearUnknown', function(done) {
Contacts.clearUnknown();
Contacts.findByUnknown('456').then(function(list) {
assert.equal(list.length, 0);
}).then(done, done);
});
test('Unknown List cannot have more than 3 prediction', function(done) {
Contacts.clearUnknown();
Contacts.addUnknown('123123456');
Contacts.addUnknown('789123789');
Contacts.addUnknown('456456123');
Contacts.addUnknown('123456789');
Contacts.findByUnknown('123').then(function(list) {
//Prediction should had been 4 but since we restict it to a max of 3
assert.equal(list.length, 3);
}).then(done, done);
});
});
suite('Contacts.findBy (success)', function() {
test('(object, ...), Match', function(done) {
var filter = {
filterBy: ['tel'],
filterOp: 'contains',
filterValue: '12125559999'
};
var result = MockContact.list();
navigator.mozContacts.find.withArgs(
withFilter('12125559999')
).returns(
Promise.resolve(result)
);
Contacts.findBy(filter).then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 1);
}).then(done, done);
});
test('(object, ...), No Match', function(done) {
var filter = {
filterBy: ['tel'],
filterOp: 'contains',
filterValue: '911'
};
navigator.mozContacts.find.withArgs(
withFilter('911')
).returns(
Promise.resolve([])
);
Contacts.findBy(filter).then(function(contacts) {
// contacts were not found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
});
suite('Contacts.findBy (error)', function() {
test('({}, ...)', function(done) {
Contacts.findBy({}).then(function(contacts) {
assert.isTrue(Array.isArray(contacts));
assert.equal(contacts.length, 0);
}).then(done, done);
});
});
suite('Contacts validation', function() {
test('Contact validation, predominate first', function(done) {
var result = MockContact.list([
{ givenName: ['Jane'], familyName: ['D'] },
{ givenName: ['jane'], familyName: ['doe'] },
{ givenName: ['jane'], familyName: ['dow'] },
{ givenName: ['Jane'], familyName: ['Doozer'] }
]);
navigator.mozContacts.find.withArgs(
withFilter('jane')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('jane d').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
// This was relaxed by the change from "startsWith" to "contains"
assert.equal(contacts.length, 4);
}).then(done, done);
});
test('Contact validation, predominate last', function(done) {
var result = MockContact.list([
{ givenName: ['Jane'], familyName: ['Doozer'] },
{ givenName: ['jane'], familyName: ['doe'] },
{ givenName: ['jerry'], familyName: ['doe'] },
{ givenName: ['j'], familyName: ['dow'] },
{ givenName: ['john'], familyName: ['doland'] }
]);
navigator.mozContacts.find.withArgs(
withFilter('do')
).returns(
Promise.resolve(result)
);
Contacts.findContactByString('j do').then(function(contacts) {
// contacts were found
assert.ok(Array.isArray(contacts));
assert.equal(contacts.length, 5);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,288 @@
/*global loadBodyHTML,
Dialog,
MockL10n,
TransitionEvent
*/
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/js/dialog.js');
suite('Dialog', function() {
var nativeMozL10n = document.l10n;
var params = null;
suiteSetup(function() {
loadBodyHTML('/index.html');
document.l10n = MockL10n;
params = {
title: {
raw: 'Foo Title'
},
body: {
raw: 'Foo Body'
},
options: {
cancel: {
text: {
raw: 'Foo Cancel'
}
}
}
};
});
suiteTeardown(function() {
document.l10n = nativeMozL10n;
params = null;
});
test('Creation', function() {
var dialog = new Dialog(params);
// We check if the object is created properly
assert.isFalse(!dialog.show);
assert.equal(typeof dialog.show, 'function');
assert.isFalse(!dialog.hide);
assert.equal(typeof dialog.hide, 'function');
});
test('Appending to DOM', function() {
var previouslyDefinedForms = document.getElementsByTagName('form').length;
// In this case we have several forms pre-defined (5):
// - "messages-compose-form"
// - "messages-edit-form"
// - "loading"
// - "threads-edit-form"
assert.equal(previouslyDefinedForms, 4);
// Now we create the new element
var dialog = new Dialog(params);
// We check if the object is appended to the DOM
dialog.show();
// Is appended properly?
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
assert.equal(currentlyDefinedFormsLength, 5);
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
assert.equal(dialogForm.dataset.type, 'confirm');
});
test('Has GaiaSimPicker', function() {
assert.ok(document.getElementById('sim-picker'));
});
test('Focus', function() {
var dialog = new Dialog(params);
this.sinon.stub(dialog.form, 'focus');
this.sinon.stub(HTMLElement.prototype, 'blur');
dialog.show();
sinon.assert.called(HTMLElement.prototype.blur);
var transitionend = new TransitionEvent('transitionend', {
bubbles: true,
cancelable: true,
propertyName: 'transform'
});
dialog.form.dispatchEvent(transitionend);
sinon.assert.called(dialog.form.focus);
});
test('Redundant shows have no effect', function() {
var dialog = new Dialog(params);
var spy = this.sinon.spy(document.body, 'appendChild');
dialog.show();
dialog.show();
assert.ok(spy.calledOnce);
});
test('Hiding removes element after transition', function() {
var dialog = new Dialog(params);
dialog.show();
assert.notEqual(dialog.form.parentElement, null);
dialog.hide();
assert.notEqual(dialog.form.parentElement, null);
var transitionend = new TransitionEvent('transitionend', {
bubbles: true,
cancelable: true,
propertyName: 'transform'
});
dialog.form.dispatchEvent(transitionend);
assert.equal(dialog.form.parentElement, null);
});
test('Checking the structure. Default.', function() {
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
// We retrieve the last created form
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
// We check how many buttons we have (only the mandatory one)
var formOptions = dialogForm.getElementsByTagName('button');
assert.equal(formOptions.length, 1);
});
test('Checking the structure. Confirm.', function() {
// We add the confirm
params.options.confirm = {
text: {
raw: 'Foo Cancel'
}
};
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
// We retrieve the last created form
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
// We check how many buttons we have (mandatory + confirm one)
var formOptions = dialogForm.getElementsByTagName('button');
assert.equal(formOptions.length, 2);
// We check if there is a 'recommend' style
var optionalOptions = dialogForm.getElementsByClassName('recommend');
assert.equal(optionalOptions.length, 1);
});
test('Checking the structure. Confirm (with custom class name).', function() {
// We add the confirm
params.options.confirm = {
text: {
raw: 'Foo Cancel'
},
className: 'test-class'
};
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
// We retrieve the last created form
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
// We check whether default 'recommend' class wasn't applied
var optionalOptions = dialogForm.getElementsByClassName('recommend');
assert.equal(optionalOptions.length, 0);
// We check whether custom class name was applied
optionalOptions = dialogForm.getElementsByClassName('test-class');
assert.equal(optionalOptions.length, 1);
});
test('Checking the localization.', function() {
params.title = 'l10n Title',
params.body = { raw: 'non-l10n Body' },
params.options.cancel = {
text: { id: 'l10n keyCancel' }
};
params.options.confirm = {
text: { id: 'l10n keyConfirm', args: { n: 1 } }
};
var l10nSpy = this.sinon.spy(document.l10n, 'setAttributes');
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
// We retrieve the last created form
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
// We check how many buttons we have (mandatory + confirm one)
var titleDOM = dialogForm.querySelector('h1');
var bodyDOM = dialogForm.querySelector('p');
var formOptions = dialogForm.getElementsByTagName('button');
assert.equal(formOptions.length, 2);
// We check localization
assert.equal(titleDOM.getAttribute('data-l10n-id'), params.title);
assert.equal(bodyDOM.textContent, params.body.raw);
assert.isFalse(bodyDOM.hasAttribute('data-l10n-id'));
sinon.assert.calledWith(
l10nSpy, formOptions[0], params.options.cancel.text.id
);
sinon.assert.calledWith(
l10nSpy, formOptions[1],
params.options.confirm.text.id, params.options.confirm.text.args
);
});
test('Checking parametrized body localization.', function() {
params.title = 'l10n Title';
params.body = {
id: 'l10n Body',
args: {
n: 3,
numbers: ['123', '456', '789']
}
};
params.options.cancel = {
text: 'l10n keyCancel'
};
params.options.confirm = {
text: 'l10n keyConfirm'
};
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
// We retrieve the last created form
var currentlyDefinedForms = document.getElementsByTagName('form');
var currentlyDefinedFormsLength = currentlyDefinedForms.length;
// We check the type
var dialogForm = currentlyDefinedForms[currentlyDefinedFormsLength - 1];
// We check how many buttons we have (mandatory + confirm one)
var bodyDOM = dialogForm.querySelector('p');
// We check localization
assert.equal(
bodyDOM.getAttribute('data-l10n-id'), params.body.id,
'Body DOM localized with proper string'
);
assert.equal(
bodyDOM.getAttribute('data-l10n-args'), JSON.stringify(params.body.args),
'Body DOM localized with proper string'
);
});
test('Should prevent pointer events before transitionend', function() {
// Now we create the new element
var dialog = new Dialog(params);
// We append the element to the DOM
dialog.show();
assert.isTrue(document.body.classList.contains('dialog-animating'));
var transitionend = new TransitionEvent('transitionend', {
bubbles: true,
propertyName: 'transform'
});
dialog.form.dispatchEvent(transitionend);
assert.isFalse(document.body.classList.contains('dialog-animating'));
});
test('adding a class name', function() {
params.classes = ['specific-class1', 'specific-class2'];
var dialog = new Dialog(params);
dialog.show();
var elt = document.querySelector('.specific-class1');
assert.ok(elt);
assert.ok(elt.classList.contains('specific-class2'));
});
});

View file

@ -0,0 +1,306 @@
/*global Dialog,
ErrorDialog,
MockL10n,
MocksHelper,
Settings
*/
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/test/unit/mock_dialog.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/js/dialog.js');
require('/views/shared/js/error_dialog.js');
var mocksHelperForDialog = new MocksHelper([
'Dialog',
'Settings'
]).init();
suite('ErrorDialog', function() {
// Silencing jshint complaints related to "new ErrorDialog(..)" invocations
/*jshint nonew: false */
var realL10n;
mocksHelperForDialog.attachTestHelpers();
setup(function() {
realL10n = document.l10n;
document.l10n = MockL10n;
this.sinon.spy(Dialog, 'call');
});
teardown(function() {
document.l10n = realL10n;
});
suite('basic tests', function() {
test('uses correct Dialog title, body and cancel button l10nIds',
function() {
var errorDescription = {
prefix: 'TEST'
};
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: errorDescription.prefix + 'Title',
body: errorDescription.prefix + 'Body',
options: {
cancel: {
text: errorDescription.prefix + 'BtnOk'
}
},
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('throws if prefix is not passed', function() {
assert.throws(function() {
(new ErrorDialog({}));
});
});
});
suite('uses correct Dialog options', function() {
test('confirm handler is not set, errorDescription.executeHandler is true',
function() {
var errorDescription = {
prefix: 'TEST',
executeHandler: true
};
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: sinon.match.any,
options: {
cancel: {
text: errorDescription.prefix + 'BtnOk'
}
},
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('confirm handler is set, errorDescription.executeHandler is false',
function() {
var errorDescription = {
prefix: 'TEST',
executeHandler: false
};
var dialogOptions = { confirmHandler: () => {} };
(new ErrorDialog(errorDescription, dialogOptions));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: sinon.match.any,
options: {
cancel: {
text: errorDescription.prefix + 'BtnOk'
}
},
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('confirm handler is set, errorDescription.executeHandler is true',
function() {
var errorDescription = {
prefix: 'TEST',
executeHandler: true
};
var dialogOptions = { confirmHandler: () => {} };
(new ErrorDialog(errorDescription, dialogOptions));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: sinon.match.any,
options: {
cancel: {
text: errorDescription.prefix + 'BtnOk'
},
confirm: {
text: errorDescription.prefix + 'Confirm',
method: dialogOptions.confirmHandler
}
},
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
});
suite('respects errorDescription.showRecipient', function() {
test('errorDescription.showRecipient is true, recipients array is set',
function() {
var errorDescription = {
prefix: 'TEST',
showRecipient: true
};
var dialogOptions = {
recipients: ['111', '222', '333']
};
(new ErrorDialog(errorDescription, dialogOptions));
var fragmentContainsBody = sinon.match((fragment) => {
var body = fragment.querySelector('span');
var expectedL10nId = errorDescription.prefix + 'Body';
var hasL10nId = (body.dataset.l10nId === expectedL10nId);
var expectedArgs = JSON.stringify(
{ n: dialogOptions.recipients.length }
);
var hasL10nArgs = (body.dataset.l10nArgs === expectedArgs);
return hasL10nId && hasL10nArgs;
}, 'fragment contains correct body');
var fragmentContainsRecipients = sinon.match((fragment) => {
var recipients = fragment.querySelector('ul');
return dialogOptions.recipients.every((recipient, i) => {
var li = recipients.children[i];
return li.textContent === recipient;
});
}, 'fragment contains all recipients');
var fragmentContainsBodyAndRecipients =
sinon.match.instanceOf(DocumentFragment)
.and(fragmentContainsBody)
.and(fragmentContainsRecipients);
var expectedClasses = [
'error-dialog-TEST', 'error-dialog', 'error-dialog-show-recipient'
];
sinon.assert.calledWithMatch(Dialog.call, sinon.match.any, {
body: {
raw: fragmentContainsBodyAndRecipients
},
classes: expectedClasses
});
});
test('errorDescription.showRecipient is true, recipients array is not set',
function() {
var errorDescription = {
prefix: 'TEST',
showRecipient: true
};
assert.throws(function() {
(new ErrorDialog(errorDescription));
});
});
});
suite('respects errorDescription.showDsdsStatus', function() {
test('errorDescription.showDsdsStatus is true, SIM 1 is active',
function() {
var errorDescription = {
prefix: 'TEST',
showDsdsStatus: true
};
Settings.mmsServiceId = 0;
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: {
id: errorDescription.prefix + 'Body',
args: {
activeSimId: '1',
nonActiveSimId: '2'
}
},
options: sinon.match.any,
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('errorDescription.showDsdsStatus is true, SIM 2 is active',
function() {
var errorDescription = {
prefix: 'TEST',
showDsdsStatus: true
};
Settings.mmsServiceId = 1;
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: {
id: errorDescription.prefix + 'Body',
args: {
activeSimId: '2',
nonActiveSimId: '1'
}
},
options: sinon.match.any,
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('errorDescription.showDsdsStatus is false, any SIM is active',
function() {
var errorDescription = {
prefix: 'TEST',
showDsdsStatus: false
};
Settings.mmsServiceId = 0;
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: errorDescription.prefix + 'Body',
options: sinon.match.any,
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
test('errorDescription.showDsdsStatus is true, mmsServiceId is not valid',
function() {
var errorDescription = {
prefix: 'TEST',
showDsdsStatus: true
};
// No mmsServiceId available
Settings.mmsServiceId = null;
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: {
id: errorDescription.prefix + 'Body',
args: null
},
options: sinon.match.any,
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
Dialog.call.reset();
// Unknown mmsServiceId
Settings.mmsServiceId = 2;
(new ErrorDialog(errorDescription));
sinon.assert.calledWith(Dialog.call, sinon.match.any, {
title: sinon.match.any,
body: {
id: errorDescription.prefix + 'Body',
args: null
},
options: sinon.match.any,
classes: ['error-dialog-' + errorDescription.prefix, 'error-dialog']
});
});
});
});

View file

@ -0,0 +1,34 @@
/*global Errors */
'use strict';
require('/views/shared/js/errors.js');
suite('Errors', function() {
test('Gets default value for unknown error code', function() {
var defaultError = Errors.get('UnknownError');
assert.isNotNull(defaultError);
assert.isString(defaultError.prefix);
assert.deepEqual(defaultError, Errors.get('I do not know this error!'));
});
test('Gets correct error description for known error code', function() {
var defaultError = Errors.get('UnknownError');
var knownErrorCodes = [
'NoSignalError', 'NotFoundError', 'FdnCheckError', 'InvalidAddressError',
'NoSimCardError', 'RadioDisabledError', 'NonActiveSimCardError',
'SimNotMatchedError', 'NonActiveSimCardToSendError',
'RadioDisabledToDownloadError'
];
knownErrorCodes.forEach((code) => {
var error = Errors.get(code);
assert.isNotNull(error);
assert.isString(error.prefix);
assert.notEqual(error, defaultError);
});
});
});

View file

@ -0,0 +1,100 @@
/*exported FixturePhones */
'use strict';
/**
* Derived from
* /dom/phonenumberutils/tests/test_phonenumber.xul
*
* isTestable: true|false
*
* Fixtures marked isTestable: false are effectively
* placeholders that stand-in to show the correct
* behaviour in cases where
* navigator.mozPhoneNumberService.normalize is
* available (sufficient permissions required)
*
*
* title: string
*
* Description used for the suite title.
*
* values: array
*
* Array of values that are tested by comparing
* each value to every other value.
*
*/
var FixturePhones = [
{
// This is only tested where there are sufficient permissions
// to use navigator.mozPhoneNumberService.normalize
isTestable: !!(navigator.mozPhoneNumberService &&
navigator.mozPhoneNumberService.normalize),
title: 'Non-Digit in Contact',
values: [
'1-800-BUY-A-CAR', '1 800 BUY A CAR', '1 (800) BUY A CAR',
'+18002892227', '18002892227', '8002892227'
]
},
{
isTestable: true,
title: 'US',
values: [
'9995551234', '+19995551234', '(999) 555-1234',
'1 (999) 555-1234', '+1 (999) 555-1234', '+1 999-555-1234'
]
},
{
isTestable: true,
title: 'DE',
values: [
'01149451491934', '49451491934', '451491934',
'0451 491934', '+49 451 491934', '+49451491934'
]
},
{
isTestable: true,
title: 'IT',
values: [
'0577-555-555', '0577555555', '05 7755 5555', '+39 05 7755 5555'
]
},
{
isTestable: true,
title: 'ES',
values: [
'612123123', '612 12 31 23', '+34 612 12 31 23'
]
},
{
isTestable: true,
title: 'BR',
values: [
'01187654321', '0411187654321', '551187654321',
'90411187654321', '+551187654321'
]
},
{
isTestable: true,
title: 'CL',
values: [
'0997654321', '997654321', '(99) 765 4321', '+56 99 765 4321'
]
},
{
isTestable: true,
title: 'CO',
values: [
'5712234567', '12234567', '(1) 2234567', '+57 1 2234567'
]
},
{
isTestable: true,
title: 'FR',
values: [
'0123456789', '+33123456789', '0033123456789',
'01.23.45.67.89', '01 23 45 67 89', '01-23-45-67-89',
'+33 1 23 45 67 89', '+33 (1) 23456789'
]
}
];

View file

@ -0,0 +1,121 @@
/*global InterInstanceEventDispatcher,
MockBroadcastChannel,
BroadcastChannel
*/
'use strict';
require('/shared/js/event_dispatcher.js');
require('/views/shared/js/inter_instance_event_dispatcher.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
suite('InterInstanceEventDispatcher >', function() {
var realBroadcastChannel;
suiteSetup(function() {
realBroadcastChannel = Object.getOwnPropertyDescriptor(
window, 'BroadcastChannel'
);
window.BroadcastChannel = MockBroadcastChannel;
});
suiteTeardown(function() {
Object.defineProperty(window, 'BroadcastChannel', realBroadcastChannel);
});
setup(function() {
this.sinon.spy(window, 'BroadcastChannel');
this.sinon.stub(BroadcastChannel.prototype, 'addEventListener');
this.sinon.stub(BroadcastChannel.prototype, 'removeEventListener');
this.sinon.stub(BroadcastChannel.prototype, 'close');
this.sinon.stub(BroadcastChannel.prototype, 'postMessage');
this.sinon.spy(window, 'addEventListener');
InterInstanceEventDispatcher.connect();
});
teardown(function() {
InterInstanceEventDispatcher.disconnect();
});
test('correctly connects to BroadcastChannel', function() {
sinon.assert.calledWith(BroadcastChannel, 'messages-channel');
sinon.assert.calledWith(
BroadcastChannel.prototype.addEventListener,
'message'
);
InterInstanceEventDispatcher.connect();
InterInstanceEventDispatcher.connect();
sinon.assert.calledOnce(BroadcastChannel);
InterInstanceEventDispatcher.disconnect();
InterInstanceEventDispatcher.connect();
sinon.assert.calledTwice(BroadcastChannel);
});
test('correctly disconnects from BroadcastChannel', function() {
var addEventListenerArgs =
BroadcastChannel.prototype.addEventListener.lastCall.args;
window.addEventListener.withArgs('unload').yield();
sinon.assert.called(BroadcastChannel.prototype.close);
sinon.assert.calledWith(
BroadcastChannel.prototype.removeEventListener,
addEventListenerArgs[0],
// Reference to "onmessage" listener function
addEventListenerArgs[1]
);
});
test('correctly handles incoming BroadcastChannel messages', function() {
BroadcastChannel.prototype.postMessage.reset();
var onDraftsChangedStub = sinon.stub();
InterInstanceEventDispatcher.on('draft-saved', onDraftsChangedStub);
BroadcastChannel.prototype.addEventListener.withArgs('message').yield({
data: {
name: 'draft-saved',
parameters: { key: 'value' }
}
});
sinon.assert.calledWith(onDraftsChangedStub, { key: 'value' });
BroadcastChannel.prototype.addEventListener.withArgs('message').yield({
data: {
name: 'draft-saved',
parameters: { key: 'value#2' }
}
});
sinon.assert.calledTwice(onDraftsChangedStub);
sinon.assert.calledWith(onDraftsChangedStub, { key: 'value#2' });
sinon.assert.notCalled(BroadcastChannel.prototype.postMessage);
});
test('correctly handles outgoing BroadcastChannel messages', function() {
var onDraftsChangedStub = sinon.stub();
InterInstanceEventDispatcher.on('draft-saved', onDraftsChangedStub);
assert.throws(() => {
InterInstanceEventDispatcher.emit('something-changed');
}, 'Event "something-changed" is not allowed!');
InterInstanceEventDispatcher.disconnect();
assert.throws(() => {
InterInstanceEventDispatcher.emit('draft-saved', { key: 'value' });
}, 'Channel is not created!');
InterInstanceEventDispatcher.connect();
InterInstanceEventDispatcher.emit('draft-saved', { key: 'value' });
sinon.assert.calledWith(BroadcastChannel.prototype.postMessage, {
name: 'draft-saved',
parameters: { key: 'value' }
});
sinon.assert.notCalled(onDraftsChangedStub, 'Does not broadcast to itself');
});
});

View file

@ -0,0 +1,217 @@
/*global LocalizationHelper,
MockL10n
*/
'use strict';
require('/views/shared/js/localization_helper.js');
require('/shared/test/unit/mocks/mock_l20n.js');
suite('LocalizationHelper >', function() {
var navigatorMozL10n;
function onDOMRetranslated() {
document.addEventListener.withArgs('DOMRetranslated').yield();
return false;
}
function onTimeFormatChange() {
window.addEventListener.withArgs('timeformatchange').yield();
return true;
}
setup(function(done) {
navigatorMozL10n = document.l10n;
document.l10n = MockL10n;
this.sinon.stub(document.l10n, 'translateFragment');
this.sinon.stub(window, 'addEventListener');
this.sinon.stub(document, 'addEventListener');
const getAttr = this.sinon.stub(document.documentElement, 'getAttribute');
getAttr.withArgs('lang').returns('be-BY');
getAttr.withArgs('dir').returns('rtl');
[
'attachment-container',
'attachment-container',
'custom'
].forEach((iframeClassName) => {
var iframe = document.createElement('iframe');
iframe.className = iframeClassName;
document.body.appendChild(iframe);
});
var customIframeDocument = document.querySelector(
'iframe.custom'
).contentDocument;
customIframeDocument.documentElement.lang = 'en-US';
customIframeDocument.documentElement.dir = 'ltr';
LocalizationHelper.init().then(
() => undefined).then(done, done);
});
teardown(function() {
document.l10n = navigatorMozL10n;
});
test('localizes only attachment iframes', function() {
sinon.assert.calledTwice(document.l10n.translateFragment);
var attachmentContainers = document.querySelectorAll(
'iframe.attachment-container'
);
Array.forEach(attachmentContainers, (iframe) => {
var doc = iframe.contentDocument;
assert.equal(doc.documentElement.lang, 'be-BY');
assert.equal(doc.documentElement.dir, 'rtl');
sinon.assert.calledWith(document.l10n.translateFragment, doc.body);
});
// Custom iframe should stay untouched.
var customIframeDocument = document.querySelector(
'iframe.custom'
).contentDocument;
assert.equal(customIframeDocument.documentElement.lang, 'en-US');
assert.equal(customIframeDocument.documentElement.dir, 'ltr');
});
[onDOMRetranslated, onTimeFormatChange].forEach(function(forceUpdateMethod) {
suite('localizes date & time ' + forceUpdateMethod.name +' >', function() {
var node;
setup(function() {
node = document.createElement('div');
node.classList.add('l10n-contains-date');
document.body.appendChild(node);
});
teardown(function() {
node.remove();
});
test('ignores if date or format are unavailable', function() {
node.dataset.l10nDate = '';
node.dataset.l10nDateFormat = 'format';
node.textContent = 'not-changed-content';
forceUpdateMethod();
assert.equal(node.textContent, 'not-changed-content');
assert.isFalse(node.hasAttribute('data-l10n-id'));
assert.isFalse(node.hasAttribute('data-l10n-args'));
node.dataset.l10nDate = Date.now();
node.dataset.l10nDateFormat = '';
node.textContent = 'not-changed-content';
forceUpdateMethod();
assert.equal(node.textContent, 'not-changed-content');
assert.isFalse(node.hasAttribute('data-l10n-id'));
assert.isFalse(node.hasAttribute('data-l10n-args'));
});
test('correctly formats node content', function() {
var timestamp = Date.now();
var options = {
month: 'long',
day: '2-digit',
year: 'numeric',
};
function assertChange(options, doNotFormat) {
options.hour12 = navigator.mozHour12;
var formatter =
new Intl.DateTimeFormat(navigator.languages, options);
// In case of time format change date-only elements aren't updated
if (doNotFormat) {
assert.equal(
node.textContent, '', 'Date-only elements should not be updated'
);
} else {
assert.equal(
node.textContent,
formatter.format(new Date(timestamp))
);
}
assert.isFalse(node.hasAttribute('data-l10n-id'));
assert.isFalse(node.hasAttribute('data-l10n-args'));
}
node.dataset.l10nDate = timestamp;
node.dataset.l10nDateFormat = JSON.stringify(options);
// In case of time format change date-only elements shouldn't be updated
assertChange(options, forceUpdateMethod());
options.hour = 'numeric';
options.minute = 'numeric';
node.dataset.l10nDateFormat = JSON.stringify(options);
navigator.mozHour12 = true;
forceUpdateMethod();
assertChange(options);
navigator.mozHour12 = false;
forceUpdateMethod();
assertChange(options);
});
test('correctly formats node l10n attributes', function() {
var timestamp = Date.now();
var options = {
month: 'long',
day: '2-digit',
year: 'numeric',
};
function assertChange(options, doNotFormat) {
options.hour12 = navigator.mozHour12;
var formatter =
new Intl.DateTimeFormat(navigator.languages, options);
var l10nAttributes = document.l10n.getAttributes(node);
// In case of time format change date-only elements aren't updated
if (doNotFormat) {
assert.deepEqual(l10nAttributes.args, {
data: 'custom'
}, 'Date-only elements should not be updated');
} else {
assert.deepEqual(l10nAttributes.args, {
data: 'custom',
date: formatter.format(new Date(timestamp))
});
}
assert.equal(l10nAttributes.id, 'custom');
assert.equal(node.textContent, 'not-changed-content');
}
document.l10n.setAttributes(node, 'custom', {
data: 'custom'
});
node.dataset.l10nDate = timestamp;
node.dataset.l10nDateFormat = JSON.stringify(options);
node.textContent = 'not-changed-content';
// In case of time format change date-only elements shouldn't be updated
assertChange(options, forceUpdateMethod());
options.hour = 'numeric';
options.minute = 'numeric';
node.dataset.l10nDateFormat = JSON.stringify(options);
navigator.mozHour12 = true;
forceUpdateMethod();
assertChange(options);
navigator.mozHour12 = false;
forceUpdateMethod();
assertChange(options);
});
});
});
});

Binary file not shown.

View file

@ -0,0 +1,6 @@
BEGIN:VCARD
VERSION:3.0
FN:dennis
N:;dennis;;;
TEL;TYPE=CELL:0975557726
END:VCARD

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,88 @@
/*global getMockupedDate */
/*exported MockThreadMessages */
'use strict';
function MockThreadMessages() {
var messagesMockup = [
{
sender: null,
receiver: '197746797',
body: 'Error message:)',
delivery: 'sending',
deliveryInfo: [{
receiver: '197746797',
deliveryStatus: 'pending',
deliveryTimestamp: null,
readStatus: 'not-applicable',
readTimestamp: null
}],
error: true,
id: 47,
threadId: 1,
timestamp: +getMockupedDate(0)
},
{
sender: null,
receiver: '197746797',
body: 'Nothing :)',
delivery: 'sent',
deliveryInfo: [{
receiver: '197746797',
deliveryStatus: 'success',
deliveryTimestamp: +getMockupedDate(0),
readStatus: 'not-applicable',
readTimestamp: null
}],
id: 46,
threadId: 1,
timestamp: +getMockupedDate(0)
},
{
sender: '197746797',
body: 'Recibido!',
delivery: 'received',
id: 40,
threadId: 1,
timestamp: +getMockupedDate(2)
},
{
sender: null,
receiver: '197746797',
body: 'Nothing :)',
delivery: 'error',
deliveryInfo: [{
receiver: '197746797',
deliveryStatus: 'errpr',
deliveryTimestamp: null,
readStatus: 'not-applicable',
readTimestamp: null
}],
id: 460,
threadId: 1,
timestamp: +getMockupedDate(6)
},
{
sender: null,
receiver: '197746797',
body: 'Nothing at all :)',
delivery: 'error',
deliveryInfo: [{
receiver: '197746797',
deliveryStatus: 'error',
deliveryTimestamp: null,
readStatus: 'not-applicable',
readTimestamp: null
}],
id: 461,
threadId: 1,
timestamp: +getMockupedDate(6)
}];
messagesMockup.sort(function(a, b) {
return b.timestamp - a.timestamp;
});
return messagesMockup;
}

View file

@ -0,0 +1,6 @@
/*exported MockActivityHandler */
'use strict';
var MockActivityHandler = {
init() {}
};

View file

@ -0,0 +1,25 @@
'use strict';
var MockActivityPicker = {};
[
'email', 'dial', 'url',
'createNewContact', 'addToExistingContact'
].forEach(function(fn) {
MockActivityPicker[fn] = function() {
MockActivityPicker[fn].called = true;
MockActivityPicker[fn].calledWith = [].slice.call(arguments);
};
MockActivityPicker[fn].mSetup = function() {
MockActivityPicker[fn].called = false;
MockActivityPicker[fn].calledWith = null;
};
MockActivityPicker[fn].mTeardown = function() {
delete MockActivityPicker[fn].called;
delete MockActivityPicker[fn].calledWith;
};
});

View file

@ -0,0 +1,11 @@
/* exported MockApp */
(function(exports) {
'use strict';
exports.MockApp = {
instanceId: 'app-instance-id',
whenReady: () => Promise.resolve(),
setReady: () => {}
};
})(window);

View file

@ -0,0 +1,36 @@
/*exported MockasyncStorage */
'use strict';
var MockasyncStorage = {
keys: {},
getItem: function(key, cb) {
if (!cb) {
return;
}
cb(this.keys[key]);
},
setItem: function(key, value, cb) {
this.keys[key] = value;
if (typeof cb === 'function') {
cb();
}
},
removeItem: function(key) {
delete this.keys[key];
},
clear: function() {
this.keys = {};
},
mSetup: function() {
this.clear();
},
mTeardown: function() {
this.clear();
}
};

View file

@ -0,0 +1,27 @@
/*global Utils */
/*exported MockAttachment */
'use strict';
function MockAttachment(blob, options) {
this.blob = blob;
options = options || {};
this.name = options.name;
this.isDraft = !!options.isDraft;
this.mNextRender = document.createElement('iframe');
this.mNextRender.className = 'attachment-container';
}
MockAttachment.prototype = {
get size() {
return this.blob && this.blob.size;
},
get type() {
return Utils.typeFromMimeType(this.blob.type);
},
render: function() {
return this.mNextRender;
},
view: function() {},
updateFileSize: function() {}
};

View file

@ -0,0 +1,15 @@
'use strict';
function MockAttachmentRenderer() {}
MockAttachmentRenderer.prototype = {
render: function() {
return Promise.resolve();
},
getAttachmentContainer: function() {},
updateFileSize: function() {}
};
MockAttachmentRenderer.for = function() {
return new MockAttachmentRenderer();
};

View file

@ -0,0 +1,12 @@
/*exported MockBroadcastChannel */
'use strict';
function MockBroadcastChannel(name) {
this.name = name;
}
MockBroadcastChannel.prototype.addEventListener = () => {};
MockBroadcastChannel.prototype.removeEventListener = () => {};
MockBroadcastChannel.prototype.postMessage = () => {};
MockBroadcastChannel.prototype.close = () => {};

View file

@ -0,0 +1,35 @@
/*exported MockCompose */
'use strict';
var MockCompose = {
isSubjectVisible: false,
init: function() {},
on: function(type, handler) {},
off: function(type, handler) {},
offAll: function() {},
getContent: function() {},
getText: function() {},
isEmpty: () => true,
isSubjectMaxLength: () => false,
setupLock() {},
refresh() {},
scrollToTarget: function(target) {},
scrollMessageContent: function() {},
prepend: function(item) {},
append: () => {},
clear: () => {},
focus: function() {},
getSubject: function() {},
setSubject: function() {},
showSubject: function() {},
hideSubject: function() {},
fromDraft: function() {},
mSetup: function() {
this.size = null;
this.type = 'sms';
}
};

View file

@ -0,0 +1,70 @@
'use strict';
function MockContact(name) {
name = typeof name !== 'undefined' ? name : {
familyName: ['O\'Hare'],
givenName: ['Pepito']
};
if (!(this instanceof MockContact)) {
return new MockContact(name);
}
this.id = '1';
this.familyName = name.familyName;
this.givenName = name.givenName;
this.name = [
[this.givenName, this.familyName].join(' ')
];
this.tel = [
{
'value': '+346578888888',
'type': ['Mobile'],
'carrier': 'TEF'
},
{
'value': '+12125559999',
'type': 'Batphone', // Make sure we cover both strings and arrays
'carrier': 'XXX'
}
];
this.email = [
{
'value': 'a@b.com'
}
];
this.category = [
'favorite'
];
this.org = [
''
];
}
/*
MockContact.list([names])
Create contacts lists
MockContact.list();
MockContact.list([
{ givenName: ['Jane'], familyName: ['Doozer'] },
{ givenName: ['doug'], familyName: ['dooley'] }
]);
*/
MockContact.list = function(names) {
if (names && names.length) {
return names.map(function(name) {
return new MockContact(name);
});
} else {
return [new MockContact()];
}
};

View file

@ -0,0 +1,14 @@
/*exported MockContactRenderer */
'use strict';
function MockContactRenderer(opts) {
}
MockContactRenderer.prototype = {
render: function() {}
};
MockContactRenderer.flavor = function() {
return new MockContactRenderer();
};

View file

@ -0,0 +1,23 @@
/*global MockContact */
/*exported MockContacts */
'use strict';
require('/views/shared/test/unit/mock_contact.js');
(function(exports) {
function returnMock() {
var result = MockContact.list();
return Promise.resolve(result);
}
exports.MockContacts = {
findBy: returnMock,
findByPhoneNumber: returnMock,
findByAddress: returnMock,
findExactByEmail: returnMock,
findByString: returnMock,
findExact: returnMock,
addUnknown: () => {},
};
})(window);

View file

@ -0,0 +1,102 @@
/*global MockRecipients, Template */
/*exported MockConversationView */
'use strict';
require('/test/unit/mock_recipients.js');
var MockConversationView = {
CHUNK_SIZE: 10,
CONVERTED_MESSAGE_DURATION: 3000,
IMAGE_RESIZE_DURATION: 3000,
recipients: {
add: function() {},
focus: function() {}
},
recipientsList: document.createElement('div'),
// For Information view testing. Need to be updated with Conversation View
// layout.
optionsButton: document.createElement('button'),
callNumberButton: document.createElement('button'),
attachButton: document.createElement('button'),
subheader: document.createElement('div'),
container: document.createElement('article'),
composeForm: document.createElement('form'),
headerText: document.createElement('h1'),
inEditMode: false,
init: function() {},
initRecipients: function() {
this.recipients = new MockRecipients({
outer: 'messages-to-field',
inner: 'messages-recipients-list',
template: new Template('messages-recipient-tmpl')
});
},
on: function() {},
once: () => {},
initSentAudio: function() {},
getIdIterator: function() {},
onMessageContentChange: function() {},
assimilateRecipients: function() {},
resizeHandler: function() {},
requestContact: function() {},
updateComposerHeader: function() {},
isScrolledManually: false,
manageScroll: function() {},
scrollViewToBottom: function() {},
back: function() {},
isKeyboardDisplayed: function() {},
getMessageContainer: function() {},
updateHeaderData: function() {},
initializeRendering: function() {},
stopRendering: function() {},
showFirstChunk: function() {},
createMmsContent: function() {},
renderMessages: function() {},
_createNotDownloadedHTML: function() {},
buildMessageDOM: function() {},
appendMessage: function() {},
showChunkOfMessages: function() {},
setHeaderAction: function() {},
setHeaderContent: function() {},
cleanForm: function() {},
clear: function() {},
toggleCheckedAll: function() {},
startEdit: function() {},
delete: function() {},
cancelEdit: function() {},
updateSelectionStatus: function() {},
handleMessageClick: function() {},
handleEvent: function() {},
cleanFields: function() {},
onSendClick: function() {},
onMessageSending: function() {},
onMessageSent: function() {},
onMessageFailed: function() {},
onDeliverySuccess: function() {},
onReadSuccess: function() {},
removeMessageDOM: function() {},
retrieveMMS: function() {},
resendMessage: function() {},
toFieldKeypress: function() {},
toFieldInput: function() {},
searchContact: function() {},
onHeaderActivation: function() {},
promptContact: () => Promise.resolve(),
prompt: () => Promise.resolve(),
saveDraft: function() {},
discardDraft: function() {},
isShowMessageErrorCalledTimes: 0,
showMessageError: function() {
this.isShowMessageErrorCalledTimes += 1;
},
mSetup: function() {
this.isShowMessageErrorCalledTimes = 0;
},
mTeardown: function() {
this.isShowMessageErrorCalledTimes = 0;
}
};

View file

@ -0,0 +1,57 @@
'use strict';
function MockDialog(params) {
MockDialog.calls.push(params);
MockDialog.instances.push(this);
Object.keys(params.options).forEach(function(option) {
MockDialog.triggers[option] = function() {
MockDialog.triggers[option].called = true;
params.options[option].method();
};
// Setup the initial |false| value, this prevents assert.isFalse from
// failing on |undefined| trigger properties
MockDialog.triggers[option].called = false;
});
}
MockDialog.triggers = {};
MockDialog.prototype = {
show: function() {
this.show.called = true;
}
};
MockDialog.mSetup = function() {
MockDialog.calls = [];
MockDialog.instances = [];
};
MockDialog.mTeardown = function() {
delete MockDialog.calls;
delete MockDialog.instances;
Object.keys(MockDialog.triggers).forEach(function(trigger) {
MockDialog.triggers[trigger].called = false;
});
};
function MockErrorDialog() {
MockErrorDialog.calls.push(arguments);
}
MockErrorDialog.prototype = {
show: function() {
this.show.called = true;
}
};
MockErrorDialog.mSetup = function() {
MockErrorDialog.calls = [];
};
MockErrorDialog.mTeardown = function() {
delete MockErrorDialog.calls;
};

View file

@ -0,0 +1,38 @@
/*exported MockInboxView */
'use strict';
var MockInboxView = {
count: 0,
inEditMode: false,
container: document.createElement('div'),
init: function() {},
updateThread: function() {},
getIdIterator: function() {},
setContact: function() {},
handleEvent: function() {},
updateSelectionStatus: function() {},
cleanForm: function() {},
toggleCheckedAll: function() {},
removeConversationDOM: function() {},
delete: function() {},
setEmpty: function() {},
showOptions: function() {},
startEdit: function() {},
cancelEdit: function() {},
renderThreads: () => {},
createThread: function() {},
insertThreadContainer: function() {},
onMessageSending: function() {},
onMessageReceived: function() {},
onThreadsDeleted: function() {},
appendThread: function() {},
onDraftSaved: function() {},
onDraftDeleted: () => {},
showDraftSavedBanner: () => {},
createThreadContainer: function() {},
updateContactsInfo: function() {},
mark: function() {},
markReadUnread: () => {},
once: () => {}
};

View file

@ -0,0 +1,18 @@
/*exported MockInformation, MockGroupView, MockReportView */
'use strict';
function MockInformation(type) {
}
MockInformation.initDefaultViews = () => {};
MockInformation.prototype = {
onDeliverySuccess: function() {},
onReadSuccess: function() {},
show: function() {},
refresh: function() {},
reset: function() {}
};
var MockGroupView = new MockInformation();
var MockReportView = new MockInformation();

View file

@ -0,0 +1,10 @@
/*exported MockInterInstanceEventDispatcher */
'use strict';
function MockInterInstanceEventDispatcher() {}
MockInterInstanceEventDispatcher.on = () => {};
MockInterInstanceEventDispatcher.emit = () => {};
MockInterInstanceEventDispatcher.connect = () => {};
MockInterInstanceEventDispatcher.disconnect = () => {};

View file

@ -0,0 +1,8 @@
/*exported MockLinkActionHandler */
'use strict';
var MockLinkActionHandler = {
onContextMenu: function() {},
onClick: function() {}
};

View file

@ -0,0 +1,9 @@
/*exported MockLinkHelper */
'use strict';
var MockLinkHelper = {
searchAndLinkClickableData: function(input) {
return input;
}
};

View file

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

View file

@ -0,0 +1,82 @@
/*exported MockMessages */
'use strict';
var MockMessages = {
smilMockup: '<smil><head><layout>' +
'<root-layout width="320px" height="480px"/>' +
'<region id="Image" left="0px" top="0px" width="320px" ' +
'height="320px" fit="meet"/><region id="Text" left="0px" ' +
'top="320px" width="320px" height="160px" fit="meet"/>' +
'</layout></head><body><par dur="5000ms"><img src="IMG_0011.jpg" ' +
'region="Image"/></par></body></smil>',
sms: function(opts = {}) {
// default sms message
var message = {
id: 1,
iccId: null,
threadId: 1,
sender: 'sender',
receiver: 'receiver',
body: 'body',
delivery: 'received',
deliveryStatus: 'success',
deliveryTimestamp: Date.now(),
type: 'sms',
messageClass: 'normal',
timestamp: Date.now(),
sentTimestamp: Date.now(),
read: true
};
for (var key in message) {
if (opts.hasOwnProperty(key)) {
message[key] = opts[key];
}
}
return message;
},
mms: function(opts = {}) {
var now = new Date();
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
// default mms message
var message = {
id: 1,
iccId: null,
threadId: 1,
sender: 'sender',
receivers: ['receiver'],
delivery: 'received',
deliveryInfo: [{
receiver: 'receiver',
deliveryStatus: 'success',
readStatus: 'success',
deliveryTimestamp: +now,
readTimestamp: +now,
}],
type: 'mms',
timestamp: +now,
sentTimestamp: +now,
read: true,
subject: '',
smil: this.smilMockup,
attachments: [{
location: 'text1',
content: new Blob(['This is a plain text'], { type: 'text/plain' })
}],
expiryDate: +tomorrow
};
for (var key in message) {
if (opts.hasOwnProperty(key)) {
message[key] = opts[key];
}
}
return message;
}
};

View file

@ -0,0 +1,16 @@
/*exported MockMimeMapper */
'use strict';
var MockMimeMapper = {
guessTypeFromFileProperties: function(filename, mimetype) {
return mimetype;
},
ensureFilenameMatchesType: function(filename, mimetype) {
if (filename.indexOf('.') !== -1) {
return filename;
}
return filename + '.' + 'jpg';
}
};

View file

@ -0,0 +1,21 @@
'use strict';
function MockMozActivity(activity) {
MockMozActivity.calls.push(activity);
MockMozActivity.instances.push(this);
this.onsuccess = function() {};
this.onerror = function() {};
}
// create an array to hold any calls to this function to test them
MockMozActivity.mSetup = function() {
MockMozActivity.calls = [];
MockMozActivity.instances = [];
};
// destroy the array so it resets every time
MockMozActivity.mTeardown = function() {
delete MockMozActivity.calls;
delete MockMozActivity.instances;
};

View file

@ -0,0 +1,8 @@
/* exported MockMozAlarms */
'use strict';
var MockMozAlarms = {
add: function() {},
remove: function() {}
};

View file

@ -0,0 +1,19 @@
/* global
Promise
*/
/* exported MockNavigation */
'use strict';
var MockNavigation = {
back: () => Promise.resolve(),
init: () => Promise.resolve(),
isCurrentPanel: () => {},
isDefaultPanel: () => false,
toPanel: () => Promise.resolve(),
setReady: () => {},
on: () => {},
off: () => {},
once: () => {},
hasPendingInit: () => {}
};

View file

@ -0,0 +1,10 @@
/*exported MockMozPhoneNumberService */
'use strict';
var MockMozPhoneNumberService = {
normalize: function nmpns_normalize() {
return true;
},
fuzzyMatch: function nmpns_fuzzyMatch() {
}
};

View file

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

View file

@ -0,0 +1,7 @@
/* exported MockNotify */
'use strict';
var MockNotify = {
ringtone: function() {},
vibrate: function() {}
};

View file

@ -0,0 +1,98 @@
'use strict';
function MockRecipients(setup) {
this.setup = setup;
this.recipientsList = document.getElementById(setup.inner);
Object.defineProperty(this, 'length', {
get: function() {
return this.numbersList.length;
},
set: function(val) {
this.numbersList.length = val;
}
});
Object.defineProperty(this, 'numbers', {
get: function() {
return Array.from(this.numbersList);
}
});
this.events = {
add: [],
remove: [],
modechange: []
};
this.numbersList = [];
this.inputValue = '';
this.list = [];
}
MockRecipients.prototype.add = function(contact) {
var span = document.createElement('span');
span.textContent = contact.number;
this.recipientsList.appendChild(span);
this.numbersList.push(contact.number);
this.list.push(contact);
this.emit('add', this.length, contact);
return this;
};
MockRecipients.prototype.remove = function(phone) {
var index = this.numbersList.indexOf(phone);
if (index != -1) {
this.numbersList.splice(this.numbersList.indexOf(phone), 1);
this.emit('remove', this.length);
}
return this;
};
MockRecipients.prototype.render = function() {
return this;
};
MockRecipients.prototype.focus = function() {
return this;
};
MockRecipients.prototype.update = function() {
return this;
};
MockRecipients.prototype.visible = function() {
return this;
};
MockRecipients.prototype.on = function(event, callback) {
this.events[event].push(callback);
return this;
};
MockRecipients.prototype.off = function() {
return this;
};
MockRecipients.prototype.emit = function(type) {
var handlers = this.events;
var args = [].slice.call(arguments, 1);
var handler, stack;
if (!handlers[type]) {
throw new Error('Invalid event type: ' + type);
}
stack = handlers[type].slice();
if (stack.length) {
while ((handler = stack.pop())) {
handler.apply(null, args);
}
}
return this;
};
MockRecipients.View = function() {};
MockRecipients.View.isFocusable = true;

View file

@ -0,0 +1,22 @@
/*exported MockSelectionHandler */
'use strict';
function MockSelectionHandler(options) {
this.selected = null;
}
MockSelectionHandler.prototype = {
get selectedCount() {
return this.selected.size;
},
get selectedList() {
return Array.from(this.selected);
},
select: function() {},
unselect: function() {},
toggleCheckedAll: function() {},
cleanForm: function() {},
allSelected: function() {}
};

View file

@ -0,0 +1,32 @@
(function(exports) {
'use strict';
var MockSettings = {
SERVICE_ID_KEYS: {
mmsServiceId: 'ril.mms.defaultServiceId',
smsServiceId: 'ril.sms.defaultServiceId'
},
mmsSizeLimitation: 295 * 1024,
maxConcatenatedMessages: 10,
mmsServiceId: null,
smsServiceId: null,
supportEmailRecipient: false,
init: () => {},
switchMmsSimHandler: function() {},
isDualSimDevice: function() { return false; },
hasSeveralSim: function() { return false; },
getServiceIdByIccId: function() { return null; },
setReadAheadThreadRetrieval: function() {},
mSetup: function() {
MockSettings.mmsSizeLimitation = 295 * 1024;
MockSettings.maxConcatenatedMessages = 10;
MockSettings.mmsServiceId = null;
MockSettings.smsServiceId = null;
MockSettings.supportEmailRecipient = false;
}
};
exports.MockSettings = MockSettings;
}(this));

View file

@ -0,0 +1,18 @@
/* global Promise */
/* exported MockSilentSms */
'use strict';
var MockSilentSms = {
mSilentSmsNumber: 9090,
checkSilentModeFor: function (smsNumber) {
return Promise.resolve(smsNumber === this.mSilentSmsNumber);
},
init: () => {},
mSetup: function() {
this.mSilentSmsNumber = 9090;
},
mTeardown: function() {
this.mSilentSmsNumber = 9090;
}
};

View file

@ -0,0 +1,14 @@
/*exported MockSMIL */
'use strict';
var MockSMIL = {
mParsed: [{'name': 'IMG_0011.jpg', 'blob': {}}],
parse: function ms_parse(message) {
return Promise.resolve(this.mParsed);
},
generate: function ms_generate(slides) {
return { smil: 'smil', attachments: 'attachments' };
}
};

View file

@ -0,0 +1,16 @@
/*exported MockSubjectComposer */
'use strict';
function MockSubjectComposer() {}
MockSubjectComposer.prototype.setValue = () => {};
MockSubjectComposer.prototype.getValue = () => {};
MockSubjectComposer.prototype.isVisible = () => {};
MockSubjectComposer.prototype.reset = () => {};
MockSubjectComposer.prototype.show = () => {};
MockSubjectComposer.prototype.hide = () => {};
MockSubjectComposer.prototype.toggle = () => {};
MockSubjectComposer.prototype.focus = () => {};
MockSubjectComposer.prototype.getMaxLength = () => {};
MockSubjectComposer.prototype.on = () => {};

View file

@ -0,0 +1,6 @@
/*exported MockSystemMessageHandler */
'use strict';
var MockSystemMessageHandler = {
init() {}
};

View file

@ -0,0 +1,11 @@
/*exported MockTimeHeaders */
'use strict';
var MockTimeHeaders = {
init: function() {},
startScheduler: function() {},
stopScheduler: function() {},
update: function() {},
updateAll: function() {}
};

View file

@ -0,0 +1,10 @@
/*exported MockURL */
'use strict';
var MockURL = {
createObjectURL: function(url) {
return url;
},
revokeObjectURL: function(url) {}
};

View file

@ -0,0 +1,41 @@
'use strict';
/* global Utils, Promise */
/* exported MockUtils */
var MockUtils = {
// we need that this function does real work, so it's copied from the real
// Utils.js
camelCase: Utils.camelCase,
date: Utils.date,
getDayDate: Utils.getDayDate,
setHeaderDate: Utils.setHeaderDate,
getFormattedHour: Utils.getFormattedHour,
_getFormatter: Utils._getFormatter,
// real code needed here to map types
typeFromMimeType: Utils.typeFromMimeType,
escapeRegex: Utils.escapeRegex,
params: Utils.params,
url: Utils.url,
getContactDetails: Utils.getContactDetails,
getResizedImgBlob: Utils.getResizedImgBlob,
getSizeForL10n: Utils.getSizeForL10n,
getPhoneDetails: Utils.getPhoneDetails,
removeNonDialables: Utils.removeNonDialables,
multiRecipientMatch: Utils.multiRecipientMatch,
probablyMatches: Utils.probablyMatches,
getDisplayObject: Utils.getDisplayObject,
basicContact: Utils.basicContact,
asyncLoadRevokeURL: Utils.asyncLoadRevokeURL,
isEmailAddress: Utils.isEmailAddress,
closeNotificationsForThread: () => Promise.resolve(),
imageToDataUrl: Utils.imageToDataUrl,
debounce: Utils.debounce,
throttle: Utils.throttle,
alert: Utils.alert,
confirm: Utils.confirm,
Promise: Utils.Promise,
getSimNameByIccId: Utils.getSimNameByIccId,
initializeShimHost: () => Promise.resolve(),
onceDocumentIsVisible: Utils.onceDocumentIsVisible
};

View file

@ -0,0 +1,8 @@
/*exported MockWaitingScreen */
'use strict';
var MockWaitingScreen = {
show: function() {},
hide: function() {}
};

View file

@ -0,0 +1,966 @@
/* global
AnimationEvent,
HashChangeEvent,
MocksHelper,
Navigation,
NavigationFactory,
Promise
*/
'use strict';
require('/shared/js/event_dispatcher.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/js/navigation.js');
var mocksHelperForNavigation = new MocksHelper([
'Utils'
]).init();
suite('navigation >', function() {
var FAKE_VIEW_OBJECTS =
['InboxView', 'ConversationView', 'ReportView', 'GroupView'];
var fakeWindow, fakeLocation;
var elements;
mocksHelperForNavigation.attachTestHelpers();
function fakeViewObjects() {
FAKE_VIEW_OBJECTS.forEach((objectName) => {
fakeWindow[objectName] = {
beforeLeave: sinon.stub(),
beforeEnter: sinon.stub(),
afterLeave: sinon.stub(),
afterEnter: sinon.stub()
};
});
}
function resetViewObjectStubs() {
FAKE_VIEW_OBJECTS.map(
(objectName) => fakeWindow[objectName]
).forEach((viewObject) => {
['beforeLeave', 'beforeEnter', 'afterLeave', 'afterEnter'].forEach(
(stepName) => viewObject[stepName].reset()
);
});
}
function fakeAssign(url) {
// hashchange event should not be fired if hash does not change.
if (url.startsWith('#') && url !== fakeLocation.hash) {
setFakeLocation({ hash: url });
Promise.resolve().then(
() => window.dispatchEvent(new HashChangeEvent('hashchange'))
);
}
}
function setFakeLocation({ pathname, hash }) {
if (pathname !== undefined) {
fakeLocation.pathname = pathname;
}
if (hash !== undefined) {
fakeLocation.hash = hash;
}
fakeLocation.href =
'app://sms.gaiamobile.org' + fakeLocation.pathname + fakeLocation.hash;
}
function findViewElements() {
elements = {};
FAKE_VIEW_OBJECTS.forEach((viewName) => {
elements[viewName] = document.querySelector('.panel-' + viewName);
});
}
function styleDescriptor(setterStub, propSetterStubs) {
return {
get: function() {
var result = {};
var descriptor = {};
for (var prop in propSetterStubs) {
descriptor[prop] = { set: propSetterStubs[prop] };
}
Object.defineProperties(result, descriptor);
return result;
},
set: setterStub,
enumerable: true,
configurable: true
};
}
setup(function() {
this.sinon.stub(window, 'setTimeout').yieldsAsync();
loadBodyHTML('/index.html');
navigator.mozHasPendingMessage = sinon.stub();
fakeLocation = {
assign: sinon.spy(fakeAssign),
hash: '',
href: 'app://sms.gaiamobile.org/',
pathname: '/' // old architecture
};
fakeWindow = Object.create(window, {
location: {
get: () => fakeLocation
},
addEventListener: {
get: () => (...args) => window.addEventListener(...args)
},
removeEventListener: {
get: () => (...args) => window.removeEventListener(...args)
},
history: {
value: {
back: () => {}
}
}
});
findViewElements();
fakeViewObjects();
});
teardown(function() {
Navigation.cleanup();
fakeWindow = null;
elements = null;
window.Navigation = null;
document.body.innerHTML = '';
delete navigator.mozHasPendingMessage;
});
suite('non split views >', function() {
setup(function() {
window.Navigation = NavigationFactory(fakeWindow);
navigator.mozHasPendingMessage.withArgs('notification').returns(false);
});
suite('init() >', function() {
var panel;
setup(function() {
panel = document.querySelector('.panel-InboxView');
});
test('display the right panel', function(done) {
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.isTrue(elements.InboxView.classList.contains('panel-hidden'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
Navigation.init().then(() => {
assert.isTrue(elements.InboxView.classList.contains('panel-active'));
assert.isFalse(elements.InboxView.classList.contains('panel-hidden'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'false');
}).then(done, done);
});
test('runs lifecycle methods for the right view', function(done) {
setFakeLocation({ hash: '#/composer?id=3' });
var transitionArgs = { id: '3' };
Navigation.init().then(() => {
sinon.assert.calledWithMatch(
fakeWindow.ConversationView.beforeEnter,
transitionArgs
);
sinon.assert.calledWithMatch(
fakeWindow.ConversationView.afterEnter,
transitionArgs
);
assert.ok(Navigation.isCurrentPanel('composer', { id: 3 }));
}).then(done, done);
});
test('Do nothing when an unknown view is asked (hash)', function(done) {
this.sinon.stub(console, 'error');
setFakeLocation({ hash: '#/unknown?id=3' });
Navigation.init().catch(() => {}).then(() => {
sinon.assert.notCalled(fakeWindow.InboxView.beforeEnter);
sinon.assert.notCalled(fakeWindow.InboxView.afterEnter);
assert.isFalse(Navigation.isCurrentPanel('thread-list'));
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
sinon.assert.calledTwice(console.error);
}).then(done, done);
});
test('Set init pending when launched by notification', function(done) {
this.sinon.stub(console, 'error');
navigator.mozHasPendingMessage.withArgs('notification').returns(true);
assert.isFalse(Navigation.hasPendingInit());
Navigation.init().catch(() => {}).then(() => {
assert.isTrue(Navigation.hasPendingInit());
sinon.assert.notCalled(fakeWindow.InboxView.beforeEnter);
sinon.assert.notCalled(fakeWindow.InboxView.afterEnter);
assert.isFalse(Navigation.isCurrentPanel('thread-list'));
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
sinon.assert.notCalled(console.error);
// Navigate to composer panel
return Navigation.toPanel('composer');
}).then(() => {
assert.isFalse(Navigation.hasPendingInit());
}).then(done, done);
});
test('Initial navigation request should proceed to the completion ' +
'without waiting for the document to be completely loaded',
function(done) {
Object.defineProperty(document, 'readyState', { value: 'interactive' });
Navigation.init().then(done, done);
});
test('Initial panel should fade-in', function(done) {
var styleSetterStub = sinon.stub();
var styleAnimationNameStub = sinon.stub();
Object.defineProperty(
panel, 'style',
styleDescriptor(
styleSetterStub, { animationName: styleAnimationNameStub }
)
);
Navigation.init().then(() => {
sinon.assert.calledWith(styleAnimationNameStub, 'fade-in');
sinon.assert.calledWith(styleSetterStub, '');
}).then(done, done);
});
});
suite('back() >', function() {
setup(function(done) {
Navigation.init().then(done, done);
});
test('Properly returns back in history', function(done) {
this.sinon.stub(fakeWindow.history, 'back', () => {
fakeAssign('#/composer');
});
Navigation.back().then(() => {
sinon.assert.called(fakeWindow.InboxView.beforeLeave);
sinon.assert.called(fakeWindow.history.back);
}).then(done, done);
});
});
suite('toPanel >', function() {
setup(function(done) {
Navigation.init().then(done, done);
});
test('Return a rejected promise if the panel is unknown', function(done) {
Navigation.toPanel('unknown').then(
function onresolve() {
done(new Error('should not return a resolved promise'));
}, function onreject() {
// we don't pass "done" directly because we don't want that done
// gets an argument as it interprets this as a failure
done();
}
);
});
test('Return a rejected promise if the panel is badly configured',
function(done) {
Navigation.toPanel('bad-panel').then(
function onresolve() {
done(new Error('should not return a resolved promise'));
}, function onreject() {
done();
}
);
});
test('Queue panel requests while not ready', function(done) {
var fakeInbox = fakeWindow.InboxView;
var fakeConversation = fakeWindow.ConversationView;
fakeWindow.InboxView = null;
fakeWindow.ConversationView = null;
Navigation.toPanel('composer');
Navigation.toPanel('thread', { id: 1 }).then(function() {
sinon.assert.callOrder(
fakeWindow.InboxView.beforeLeave,
fakeWindow.ConversationView.beforeEnter,
fakeWindow.InboxView.afterLeave,
fakeWindow.ConversationView.afterEnter
);
}).then(done, done);
setTimeout(() => {
fakeWindow.InboxView = fakeInbox;
fakeWindow.ConversationView = fakeConversation;
Navigation.setReady();
});
});
test('Queue panel transition requests', function(done) {
Navigation.toPanel('thread-list');
Navigation.toPanel('composer').then(function() {
sinon.assert.callOrder(
fakeWindow.InboxView.beforeEnter,
fakeWindow.InboxView.afterEnter,
fakeWindow.ConversationView.beforeEnter,
fakeWindow.ConversationView.afterEnter
);
}).then(done, done);
});
test('Queued panel also forwards rejections', function(done) {
fakeWindow.ConversationView.beforeLeave.throws(
new Error('error while entering ReportView')
);
var req1 = Navigation.toPanel('thread', {id: 1}).then(
() => 'resolved', () => 'rejected'
);
var req2 = Navigation.toPanel('report-view', {id: 1}).then(
() => 'resolved', () => 'rejected'
);
Promise.all([req1, req2]).then(function(results) {
assert.deepEqual(results, [ 'resolved', 'rejected' ]);
sinon.assert.callOrder(
fakeWindow.ConversationView.beforeEnter,
fakeWindow.ConversationView.afterEnter,
fakeWindow.ConversationView.beforeLeave
);
}).then(done, done);
});
test('Transition should resolve correctly while requesting same panel',
function(done) {
Navigation.toPanel('thread', { id: 1 });
Navigation.toPanel('thread', { id: 1 }).then(() =>
Navigation.toPanel('thread-list')
).then(() => {
sinon.assert.callOrder(
fakeWindow.ConversationView.beforeEnter,
fakeWindow.ConversationView.afterEnter,
fakeWindow.InboxView.beforeEnter,
fakeWindow.InboxView.afterEnter
);
}).then(done, done);
});
suite('lifecycle >', function() {
var panel1, panel2, panelName1, panelName2, panelName3;
setup(function(done) {
panel1 = fakeWindow.ConversationView;
panel2 = fakeWindow.ReportView;
panelName1 = 'thread';
panelName2 = 'report-view';
panelName3 = 'thread-list';
Navigation.toPanel(panelName1).then(function() {
// reseting lifecycle functions because we want to test the next
// transition
resetViewObjectStubs();
}).then(done, done);
});
test('lifecycle order', function(done) {
var onNavigatedStub = sinon.stub();
Navigation.on('navigated', onNavigatedStub);
Navigation.toPanel(panelName2).then(function() {
sinon.assert.callOrder(
panel1.beforeLeave,
panel2.beforeEnter,
panel2.afterEnter,
panel1.afterLeave,
onNavigatedStub
);
}).then(done, done);
});
test('isCurrentPanel is changed at the right time', function(done) {
var results = [];
panel1.beforeLeave = function() {
results.push(Navigation.isCurrentPanel(panelName1));
};
panel1.afterLeave = function() {
results.push(Navigation.isCurrentPanel(panelName2));
};
panel2.beforeEnter = function() {
results.push(Navigation.isCurrentPanel(panelName1));
};
panel2.afterEnter = function() {
results.push(Navigation.isCurrentPanel(panelName2));
};
Navigation.toPanel(panelName2).then(function() {
assert.equal(results.length, 4);
results.forEach(function(val, i) {
assert.ok(val, 'result ' + i);
});
}).then(done, done);
});
test('lifecycle methods get passed the correct args', function(done) {
var expectedArgs = {
id: sinon.match(1),
meta: {
next: {
panel: panelName2,
args: sinon.match({
id: sinon.match(1)
})
},
prev: {
panel: panelName1,
args: sinon.match.object
}
}
};
Navigation.toPanel(panelName2, { id: 1 }).then(function() {
sinon.assert.calledWithMatch(panel1.beforeLeave, expectedArgs);
sinon.assert.calledWithMatch(panel1.afterLeave, expectedArgs);
sinon.assert.calledWithMatch(panel2.beforeEnter, expectedArgs);
sinon.assert.calledWithMatch(panel2.afterEnter, expectedArgs);
}).then(done, done);
});
suite('beforeLeave failures >', function() {
test('throwing', function(done) {
panel1.beforeLeave.onFirstCall().throws();
Navigation.toPanel(panelName2).then(function resolved_unexpected() {
throw new Error('toPanel should not be resolved');
}, function rejected_expected() {
sinon.assert.notCalled(panel2.beforeEnter);
sinon.assert.notCalled(panel1.afterLeave);
sinon.assert.notCalled(panel2.afterEnter);
assert.ok(Navigation.isCurrentPanel(panelName1));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}).then(done, done);
});
test('returning a rejected promise', function(done) {
panel1.beforeLeave.onFirstCall().returns(Promise.reject());
Navigation.toPanel(panelName2).then(function resolved_unexpected() {
throw new Error('toPanel should not be resolved');
}, function rejected_expected() {
sinon.assert.notCalled(panel2.beforeEnter);
sinon.assert.notCalled(panel1.afterLeave);
sinon.assert.notCalled(panel2.afterEnter);
assert.ok(Navigation.isCurrentPanel(panelName1));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}).then(done, done);
});
});
suite('beforeEnter failures >', function() {
test('throwing', function(done) {
panel2.beforeEnter.onFirstCall().throws();
Navigation.toPanel(panelName2).then(function resolved_expected() {
sinon.assert.callOrder(
panel1.beforeLeave,
panel2.beforeEnter,
panel2.afterEnter,
panel1.afterLeave
);
assert.ok(Navigation.isCurrentPanel(panelName2));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
test('returning a rejected promise', function(done) {
panel2.beforeEnter.onFirstCall().returns(Promise.reject());
Navigation.toPanel(panelName2).then(function resolved_expected() {
sinon.assert.callOrder(
panel1.beforeLeave,
panel2.beforeEnter,
panel2.afterEnter,
panel1.afterLeave
);
assert.ok(Navigation.isCurrentPanel(panelName2));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
});
suite('afterLeave failures >', function() {
test('throwing', function(done) {
panel1.afterLeave.onFirstCall().throws();
Navigation.toPanel(panelName2).then(function resolved_expected() {
sinon.assert.called(panel2.afterEnter);
assert.ok(Navigation.isCurrentPanel(panelName2));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
test('returning a rejected promise', function(done) {
panel1.afterLeave.onFirstCall().returns(Promise.reject());
Navigation.toPanel(panelName2).then(function resolved_expected() {
sinon.assert.called(panel2.afterEnter);
assert.ok(Navigation.isCurrentPanel(panelName2));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
});
suite('afterEnter failures >', function() {
test('throwing', function(done) {
panel2.afterEnter.onFirstCall().throws();
this.sinon.stub(console, 'error');
Navigation.toPanel(panelName2).then(function resolved_expected() {
assert.ok(Navigation.isCurrentPanel(panelName2));
sinon.assert.called(console.error);
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
test('returning a rejected promise', function(done) {
panel2.afterEnter.onFirstCall().returns(Promise.reject());
this.sinon.stub(console, 'log');
Navigation.toPanel(panelName2).then(function resolved_expected() {
assert.ok(Navigation.isCurrentPanel(panelName2));
// check we can have a new transition
return Navigation.toPanel(panelName3);
}, function rejected_unexpected() {
throw new Error('toPanel should not be rejected');
}).then(done, done);
});
});
});
test('Remove any focus left on specific elements ', function() {
this.sinon.spy(document.activeElement, 'blur');
Navigation.toPanel('thread');
sinon.assert.called(document.activeElement.blur);
});
});
suite('URL manipulation >', function() {
setup(function(done) {
Navigation.init().then(done, done);
});
test('toPanel changes the location', function(done) {
Navigation.toPanel('composer', { id: 3 }).then(() => {
sinon.assert.calledWith(
fakeWindow.location.assign,
'#/composer?id=3'
);
return Navigation.toPanel('thread-list');
}).then(() => {
sinon.assert.calledWith(
fakeWindow.location.assign,
'#'
);
}).then(done, done);
});
});
test('isDefaultPanel() correctly determines default panel', function() {
// If hash is empty.
setFakeLocation({ hash: '' });
assert.isTrue(Navigation.isDefaultPanel());
// False if hash set to some different panel.
setFakeLocation({ hash: '#/composer' });
assert.isFalse(Navigation.isDefaultPanel());
});
suite('isCurrentPanel()', function() {
var view1 = 'thread-list';
var view2 = 'thread';
setup(function(done) {
Navigation.init().then(done, done);
});
test('returns false if the argument is falsy', function() {
assert.isFalse(Navigation.isCurrentPanel(null));
assert.isFalse(Navigation.isCurrentPanel());
});
test('returns the correct value without args', function() {
assert.isTrue(Navigation.isCurrentPanel(view1));
assert.isFalse(Navigation.isCurrentPanel(view2));
assert.isFalse(Navigation.isCurrentPanel(view1, { prop: 1 }));
});
test('returns the correct value with args', function(done) {
Navigation.toPanel(view2, { prop1: 1, prop2: 'prop2' })
.then(function() {
assert.isTrue(Navigation.isCurrentPanel(view2));
assert.isTrue(Navigation.isCurrentPanel(view2, { prop1: 1 }));
assert.isTrue(Navigation.isCurrentPanel(view2, { prop2: 'prop2' }));
assert.isTrue(
Navigation.isCurrentPanel(view2, { prop1: 1, prop2: 'prop2' })
);
assert.isFalse(Navigation.isCurrentPanel(view1));
assert.isFalse(Navigation.isCurrentPanel(view2, { prop1: 2 }));
assert.isFalse(
Navigation.isCurrentPanel(view2, { prop1: 1, prop2: 'prop1' })
);
assert.isFalse(
Navigation.isCurrentPanel(view2, { prop2: 'prop1' })
);
}).then(done, done);
});
});
suite('sliding views,', function() {
setup(function(done) {
Navigation.init().then(done, done);
});
function animationEndEvent() {
return new AnimationEvent('animationend', {
bubbles: true,
animationName: 'some-name'
});
}
test('resolves the promise after animationend events', function(done) {
// neutralizes the setTimeout fallback
window.setTimeout.restore();
var panel = document.querySelector('.panel-ConversationView');
this.sinon.spy(panel, 'removeEventListener');
setTimeout(
() => panel.dispatchEvent(animationEndEvent())
);
this.sinon.stub(window, 'setTimeout').returns(42);
this.sinon.stub(window, 'clearTimeout');
Navigation.toPanel('thread').then(() => {
sinon.assert.calledWith(panel.removeEventListener, 'animationend');
sinon.assert.called(window.setTimeout);
sinon.assert.calledWith(window.clearTimeout, 42);
}).then(done, done);
});
suite('animations,', function() {
var panel1, panel2;
var styleStubs;
var styleAnimationNameStubs;
setup(function() {
panel1 = document.querySelector('.panel-InboxView');
panel2 = document.querySelector('.panel-ConversationView');
styleStubs = [];
styleAnimationNameStubs = [];
[panel1, panel2].forEach((panel, i) => {
styleStubs[i] = sinon.stub();
styleAnimationNameStubs[i] = sinon.stub();
Object.defineProperty(
panel, 'style',
styleDescriptor(
styleStubs[i], { animationName: styleAnimationNameStubs[i] }
)
);
});
});
teardown(function() {
document.dir = '';
});
test('sliding forwards in LTR', function(done) {
Navigation.toPanel('thread').then(() => {
// panel1 is the old panel, panel2 is the new panel
sinon.assert.calledWith(
styleAnimationNameStubs[0], 'current-to-left'
);
sinon.assert.calledWith(
styleAnimationNameStubs[1], 'right-to-current'
);
sinon.assert.calledWith(styleStubs[0], '');
sinon.assert.calledWith(styleStubs[1], '');
}).then(done, done);
});
test('sliding forwards in RTL', function(done) {
document.dir = 'rtl';
Navigation.toPanel('thread').then(() => {
// panel1 is the old panel, panel2 is the new panel
sinon.assert.calledWith(
styleAnimationNameStubs[0], 'current-to-right'
);
sinon.assert.calledWith(
styleAnimationNameStubs[1], 'left-to-current'
);
sinon.assert.calledWith(styleStubs[0], '');
sinon.assert.calledWith(styleStubs[1], '');
}).then(done, done);
});
suite('sliding backwards', function() {
setup(function(done) {
// First going to the conversation view, and resetting the stubs
Navigation.toPanel('thread').then(() => {
styleStubs.forEach((stub) => stub.reset());
styleAnimationNameStubs.forEach((stub) => stub.reset());
}).then(done, done);
});
test('in LTR', function(done) {
Navigation.toPanel('thread-list').then(() => {
// panel1 is the new panel, panel2 is the old panel
sinon.assert.calledWith(
styleAnimationNameStubs[0], 'left-to-current'
);
sinon.assert.calledWith(
styleAnimationNameStubs[1], 'current-to-right'
);
sinon.assert.calledWith(styleStubs[0], '');
sinon.assert.calledWith(styleStubs[1], '');
}).then(done, done);
});
test('in RTL', function(done) {
document.dir = 'rtl';
Navigation.toPanel('thread-list').then(() => {
// panel1 is the new panel, panel2 is the old panel
sinon.assert.calledWith(
styleAnimationNameStubs[0], 'right-to-current'
);
sinon.assert.calledWith(
styleAnimationNameStubs[1], 'current-to-left'
);
sinon.assert.calledWith(styleStubs[0], '');
sinon.assert.calledWith(styleStubs[1], '');
}).then(done, done);
});
});
});
});
suite('setReady', function() {
test('Remove hidden class once the navigation is ready', function() {
assert.lengthOf(document.querySelectorAll('.panel-hidden'), 4);
Navigation.setReady();
assert.lengthOf(document.querySelectorAll('.panel-hidden'), 0);
});
});
});
suite('split views >', function() {
setup(function() {
setFakeLocation({ pathname: '/views/inbox/' });
window.Navigation = NavigationFactory(fakeWindow);
navigator.mozHasPendingMessage.withArgs('notification').returns(false);
Navigation.setReady();
});
suite('init()) >', function() {
test('display the right panel', function(done) {
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
Navigation.init().then(() => {
assert.ok(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'false');
}).then(done, done);
});
test('find the right split view', function(done) {
setFakeLocation({ pathname: '/views/conversation/', hash: '#?id=3'});
var transitionArgs = { id: '3' };
Navigation.init().then(() => {
sinon.assert.calledWithMatch(
fakeWindow.ConversationView.beforeEnter,
transitionArgs
);
sinon.assert.calledWithMatch(
fakeWindow.ConversationView.afterEnter,
transitionArgs
);
assert.ok(Navigation.isCurrentPanel('thread', { id: 3 }));
}).then(done, done);
});
test('find the right split subview', function(done) {
setFakeLocation(
{ pathname: '/views/conversation/', hash: '#/report-view?id=3' }
);
var transitionArgs = { id: '3' };
Navigation.init().then(() => {
sinon.assert.calledWithMatch(
fakeWindow.ReportView.beforeEnter,
transitionArgs
);
sinon.assert.calledWithMatch(
fakeWindow.ReportView.afterEnter,
transitionArgs
);
assert.ok(Navigation.isCurrentPanel('report-view', { id: 3 }));
}).then(done, done);
});
test('Do nothing when an unknown view is asked (hash)', function(done) {
this.sinon.stub(console, 'error');
setFakeLocation({ hash: '#/unknown?id=3' });
Navigation.init().catch(() => {}).then(() => {
sinon.assert.notCalled(fakeWindow.InboxView.beforeEnter);
sinon.assert.notCalled(fakeWindow.InboxView.afterEnter);
assert.isFalse(Navigation.isCurrentPanel('thread-list'));
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
sinon.assert.calledTwice(console.error);
}).then(done, done);
});
test('Do nothing when an unknown view is asked (path)', function(done) {
this.sinon.stub(console, 'error');
setFakeLocation({ pathname: '/unknown/' });
Navigation.init().catch(() => {}).then(() => {
sinon.assert.notCalled(fakeWindow.InboxView.beforeEnter);
sinon.assert.notCalled(fakeWindow.InboxView.afterEnter);
assert.isFalse(Navigation.isCurrentPanel('thread-list'));
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
sinon.assert.calledTwice(console.error);
}).then(done, done);
});
test('Load the view from path when an unrelated subview is asked',
function(done) {
this.sinon.stub(console, 'error');
setFakeLocation({ pathname: '/views/inbox/', hash: '#/composer?id=3' });
Navigation.init().then(() => {
sinon.assert.called(fakeWindow.InboxView.beforeEnter);
sinon.assert.called(fakeWindow.InboxView.afterEnter);
assert.isTrue(Navigation.isCurrentPanel('thread-list'));
assert.isTrue(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'false');
sinon.assert.calledOnce(console.error);
}).then(done, done);
});
test('Set init pending when launched by notification', function(done) {
this.sinon.stub(console, 'error');
navigator.mozHasPendingMessage.withArgs('notification').returns(true);
assert.isFalse(Navigation.hasPendingInit());
Navigation.init().catch(() => {}).then(() => {
assert.isTrue(Navigation.hasPendingInit());
sinon.assert.notCalled(fakeWindow.InboxView.beforeEnter);
sinon.assert.notCalled(fakeWindow.InboxView.afterEnter);
assert.isFalse(Navigation.isCurrentPanel('thread-list'));
assert.isFalse(elements.InboxView.classList.contains('panel-active'));
assert.equal(elements.InboxView.getAttribute('aria-hidden'), 'true');
sinon.assert.notCalled(console.error);
// Navigate to composer panel
return Navigation.toPanel('composer');
}).then(() => {
assert.isFalse(Navigation.hasPendingInit());
}).then(done, done);
});
test('Initial navigation request should proceed to the completion ' +
'without waiting for the document to be completely loaded and ' +
'Navigation to be marked as ready',
function(done) {
Object.defineProperty(document, 'readyState', { value: 'interactive' });
var NonReadyNavigation = NavigationFactory(fakeWindow);
// For the split-view case Navigation.init doesn't wait for the setReady
// call or document load event.
NonReadyNavigation.init().then(done, done);
});
});
suite('URL manipulation >', function() {
setup(function(done) {
setFakeLocation({ pathname: '/views/conversation/', hash: '#?id=3' });
Navigation.init().then(done, done);
});
test('toPanel changes the location', function(done) {
Navigation.toPanel('composer', { id: 3 }).then(() => {
sinon.assert.calledWith(
fakeWindow.location.assign,
'#/composer?id=3'
);
return new Promise((resolve) => {
fakeWindow.location.assign = sinon.spy(resolve);
Navigation.toPanel('thread-list');
});
}).then(() => {
sinon.assert.calledWith(
fakeWindow.location.assign, '/views/inbox/index.html#'
);
}).then(done, done);
});
});
});
});

View file

@ -0,0 +1,209 @@
/*global MockNavigatorSettings, MockAudio, MockVibrate, SettingsURL, Notify,
MocksHelper */
'use strict';
require('/shared/test/unit/mocks/mock_settings_url.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_audio.js');
require('/views/shared/test/unit/mock_navigator_vibrate.js');
require('/views/shared/js/notify.js');
var mocksHelperNotifications = new MocksHelper([
'Audio',
'SettingsURL'
]).init();
suite('check the ringtone and vibrate function', function() {
var realMozSettings;
var isDocumentHidden = true;
mocksHelperNotifications.attachTestHelpers();
setup(function() {
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
// Spy on the mocks
this.sinon.spy(Audio.prototype, 'play');
this.sinon.stub(navigator, 'vibrate', MockVibrate);
this.sinon.spy(window, 'Audio');
this.sinon.useFakeTimers();
MockNavigatorSettings.mSyncRepliesOnly = true;
this.sinon.stub(SettingsURL.prototype, 'get', function() {
return 'ringtone';
});
this.sinon.stub(SettingsURL.prototype, 'set', function(value) {
return value;
});
Object.defineProperty(document, 'hidden', {
configurable: true,
get: () => isDocumentHidden
});
});
teardown(function() {
navigator.mozSettings = realMozSettings;
delete document.hidden;
});
function sendVisibilityEvent() {
var visibilityEvent = new CustomEvent(
'visibilitychange',
{ bubbles: true }
);
window.dispatchEvent(visibilityEvent);
}
function assertAudio() {
sinon.assert.called(Audio);
sinon.assert.called(Audio.prototype.play);
assert.equal(MockAudio.instances[0].src, 'ringtone');
assert.equal(
MockAudio.instances[0].mozAudioChannelType, 'notification'
);
}
function assertVibrate() {
sinon.assert.calledWith(
navigator.vibrate,
[200, 200, 200, 200]
);
}
suite('mozSettings is unavailable', function() {
test('We do nothing but we do not stop', function(done) {
delete navigator.mozSettings;
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
sinon.assert.notCalled(Audio.prototype.play);
sinon.assert.notCalled(navigator.vibrate);
sinon.assert.notCalled(Audio);
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
});
suite('volume is 1 and vibration is enabled', function() {
test('play ringtone and vibrate', function(done) {
var settings = {
'audio.volume.notification': 1,
'notification.ringtone': 'ringtone',
'vibration.enabled': true
};
navigator.mozSettings.createLock().set(settings);
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
// As document is not shown in the test, we launch the
// event of visibility
sendVisibilityEvent();
assertAudio();
assertVibrate();
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
});
suite('volume is 1 and vibration is disabled', function() {
test('play ringtone and do not vibrate', function(done) {
var settings = {
'audio.volume.notification': 1,
'notification.ringtone': 'ringtone',
'vibration.enabled': false
};
navigator.mozSettings.createLock().set(settings);
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
sinon.assert.notCalled(navigator.vibrate);
assertAudio();
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
});
suite('volume is 0 and vibration is enabled', function() {
test('do not play ringtone and vibrate', function(done) {
var settings = {
'audio.volume.notification': 0,
'notification.ringtone': 'ringtone',
'vibration.enabled': true
};
navigator.mozSettings.createLock().set(settings);
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
// As document is not shown in the test, we launch the
// event of visibility
sendVisibilityEvent();
sinon.assert.notCalled(Audio.prototype.play);
sinon.assert.notCalled(Audio);
assertVibrate();
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
test('vibrate asap if the app is displayed', function(done) {
var settings = {
'audio.volume.notification': 0,
'notification.ringtone': 'ringtone',
'vibration.enabled': true
};
navigator.mozSettings.createLock().set(settings);
isDocumentHidden = false;
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
assertVibrate();
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
});
suite('volume is 0 and vibration is disabled', function() {
test('play ringtone and do not vibrate', function(done) {
var settings = {
'audio.volume.notification': 0,
'notification.ringtone': 'ringtone',
'vibration.enabled': false
};
navigator.mozSettings.createLock().set(settings);
Promise.all([
Notify.ringtone(),
Notify.vibrate()
]).then(() => {
sinon.assert.notCalled(Audio.prototype.play);
sinon.assert.notCalled(navigator.vibrate);
sinon.assert.notCalled(Audio);
}).then(done, done);
MockNavigatorSettings.mReplyToRequests();
});
});
});

View file

@ -0,0 +1,200 @@
/*global SelectionHandler
*/
'use strict';
require('/views/shared/js/selection_handler.js');
suite('SelectionHandler', function() {
const LENGTH = 10;
var container, checkUncheckAllButton, selectionHandler, options;
setup(function() {
container = document.createElement('article');
checkUncheckAllButton = document.createElement('button');
var innerHTML = '';
var idSet = new Set();
for (var i = 0; i < LENGTH; i++) {
innerHTML += '<label>' +
'<input type="checkbox" value="' + i + '">' +
'<span></span></label>';
idSet.add(i);
}
container.innerHTML = innerHTML;
options = {
container: container,
checkUncheckAllButton: checkUncheckAllButton,
updateSelectionStatus: sinon.stub(),
getIdIterator: sinon.stub().returns(idSet.values()),
isInEditMode: sinon.stub()
};
});
suite('create SelectionHandler instance', function() {
test('throws if options is not valid', function() {
assert.throws(() => new SelectionHandler());
assert.throws(() => new SelectionHandler({}));
assert.throws(() => new SelectionHandler({
// One element missed
container: container,
// Methods
updateSelectionStatus: sinon.stub(),
getIdIterator: sinon.stub(),
isInEditMode: sinon.stub()
}));
});
test('correctly set selected Set', function() {
var options = {
container: container,
checkUncheckAllButton: checkUncheckAllButton,
updateSelectionStatus: sinon.stub(),
getIdIterator: sinon.stub(),
isInEditMode: sinon.stub()
};
selectionHandler = new SelectionHandler(options);
assert.isTrue(selectionHandler.selected instanceof Set);
for (var key in options) {
assert.equal(options[key], selectionHandler[key]);
}
});
});
suite('onSelected', function() {
setup(function() {
options.isInEditMode.returns(true);
selectionHandler = new SelectionHandler(options);
});
teardown(function() {
container.innerHTML = '';
});
test('Early exist if not in edit mode', function() {
selectionHandler.isInEditMode.returns(false);
container.querySelectorAll('input')[0].click();
sinon.assert.notCalled(selectionHandler.updateSelectionStatus);
assert.equal(selectionHandler.selectedCount, 0);
});
test('input selected/unselected', function() {
var target = container.querySelectorAll('input')[0];
var event = new MouseEvent('click', {
bubbles: true,
cancelable: true
});
target.dispatchEvent(event);
sinon.assert.called(selectionHandler.updateSelectionStatus);
assert.equal(selectionHandler.selectedCount, 1);
assert.equal(selectionHandler.selectedList[0], target.value);
target.dispatchEvent(event);
sinon.assert.called(selectionHandler.updateSelectionStatus);
assert.equal(selectionHandler.selectedCount, 0);
});
});
suite('updateCheckboxes', function() {
setup(function() {
selectionHandler = new SelectionHandler(options);
});
test('checkbox UI should sync up with data', function() {
var allInputs = container.querySelectorAll('input');
var selectedIds = [1,2];
selectedIds.forEach((id) => {
selectionHandler.select(id);
});
// No checkbox selected on UI
Array.prototype.forEach.call(allInputs, (input) => {
assert.isFalse(input.checked);
});
selectionHandler.updateCheckboxes();
// UI should sync up with selected data
Array.prototype.forEach.call(allInputs, (input, index) => {
assert.equal(
input.checked,
selectedIds.indexOf(index) > -1
);
});
});
});
suite('toggleCheckedAll', function() {
setup(function() {
selectionHandler = new SelectionHandler(options);
this.sinon.stub(selectionHandler, 'updateCheckboxes');
});
test('Should select all when not all data selected first', function() {
selectionHandler.select(1);
selectionHandler.toggleCheckedAll();
assert.equal(selectionHandler.selectedCount, LENGTH);
});
test('Should unselect all when all selected first', function() {
for (var id of selectionHandler.inputIdSet) {
selectionHandler.select(id);
}
assert.equal(selectionHandler.selectedCount, LENGTH);
selectionHandler.toggleCheckedAll();
assert.equal(selectionHandler.selectedCount, 0);
});
});
suite('cleanForm', function() {
setup(function() {
selectionHandler = new SelectionHandler(options);
this.sinon.stub(selectionHandler, 'updateCheckboxes');
});
test('selected data cleared', function() {
// 1 data added
selectionHandler.select(1);
assert.equal(selectionHandler.selectedCount, 1);
// Data is cleared after cleanForm
selectionHandler.cleanForm();
assert.equal(selectionHandler.selectedCount, 0);
sinon.assert.called(selectionHandler.updateCheckboxes);
});
});
suite('allSelected', function() {
setup(function() {
selectionHandler = new SelectionHandler(options);
});
test('return false if none or some selected', function() {
assert.isFalse(selectionHandler.allSelected());
selectionHandler.select(0);
assert.isFalse(selectionHandler.allSelected());
});
test('return true if all selected', function() {
for (var id of selectionHandler.inputIdSet) {
selectionHandler.select(id);
}
assert.isTrue(selectionHandler.allSelected());
});
});
});

View file

@ -0,0 +1,351 @@
/*
Settings Tests
*/
/*global Settings,
MockNavigatorSettings,
MockL10n
*/
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/views/shared/js/settings.js');
suite('Settings >', function() {
var nativeSettings;
setup(function() {
nativeSettings = navigator.mozSettings;
});
teardown(function() {
MockNavigatorSettings.mTeardown();
navigator.mozSettings = nativeSettings;
Settings.mmsSizeLimitation = 295 * 1024;
Settings.maxConcatenatedMessages = 10;
});
suite('Without mozSettings', function() {
setup(function() {
navigator.mozSettings = null;
Settings.mmsSizeLimitation = 'whatever is default';
Settings.maxConcatenatedMessages = 'whatever is default';
Settings.mmsServiceId = 'no service ID';
Settings.smsServiceId = 'no service ID';
Settings.init();
});
test('Query Max concatenated Messages without settings', function() {
assert.equal(Settings.maxConcatenatedMessages, 'whatever is default');
});
test('Query size limitation without settings', function() {
assert.equal(Settings.mmsSizeLimitation, 'whatever is default');
});
test('Query mmsServiceId without settings', function() {
assert.equal(Settings.mmsServiceId, 'no service ID');
});
test('Query smsServiceId without settings', function() {
assert.equal(Settings.smsServiceId, 'no service ID');
});
test('Reports no dual SIM', function() {
assert.isFalse(Settings.isDualSimDevice());
assert.isFalse(Settings.hasSeveralSim());
});
test('getServiceIdByIccId returns null', function() {
assert.isNull(Settings.getServiceIdByIccId('anything'));
});
test('setReadAheadThreadRetrieval does nothing', function() {
assert.doesNotThrow(() => {
Settings.setReadAheadThreadRetrieval(9);
});
});
});
suite('With mozSettings', function() {
var realMozL10n, realSettings;
function findSettingsReq(key) {
var locks = navigator.mozSettings.createLock.returnValues;
var foundLock = locks.find(function(lock) {
return lock.get.calledWith(key);
});
return foundLock ? foundLock.get.firstCall.returnValue : null;
}
function triggerSettingsReqSuccess(setting, value) {
var req = findSettingsReq(setting);
req.result = {};
req.result[setting] = value;
req.onsuccess();
}
function assertSettingIsRetrieved(prop, setting, value, expected) {
triggerSettingsReqSuccess(setting, value);
if (arguments.length <= 3) {
expected = value;
}
assert.equal(
Settings[prop], expected,
'The setting ' + setting + ' is equal to ' + expected
);
}
setup(function() {
realMozL10n = document.l10n;
document.l10n = MockL10n;
realSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
Settings.mmsSizeLimitation = 'whatever is default';
Settings.maxConcatenatedMessages = 'whatever is default';
Settings.mmsServiceId = 'no service ID';
this.sinon.stub(navigator.mozSettings, 'createLock', function() {
var api = {
get: function() {
return {};
},
set: function() {}
};
sinon.spy(api, 'get');
sinon.spy(api, 'set');
return api;
});
});
teardown(function() {
document.l10n = realMozL10n;
realMozL10n = null;
navigator.mozSettings = realSettings;
realSettings = null;
});
test('Query size limitation with settings exist ' +
'(Size Limitation = 500KB and maxConcat = 10)', function() {
Settings.init();
assert.equal(Settings.mmsSizeLimitation, 'whatever is default');
assert.equal(Settings.maxConcatenatedMessages, 'whatever is default');
// only made two calls (maxConcatenatedMessages/mmsSizeLimitation)
// to get settings(non-DSDS case)
sinon.assert.calledTwice(navigator.mozSettings.createLock);
var setting = 512 * 1024;
var expected = setting - 5 * 1024;
assertSettingIsRetrieved(
'mmsSizeLimitation',
'dom.mms.operatorSizeLimitation',
setting,
expected
);
assertSettingIsRetrieved(
'maxConcatenatedMessages',
'operatorResource.sms.maxConcat',
10
);
assert.isFalse(Settings.hasSeveralSim());
});
suite('in a dual SIM device with 2 SIMs,', function() {
setup(function() {
// TODO use the mocks from shared and update them according to latest
// spec
navigator.mozMobileConnections = [
{ iccId: 'SIM 1' },
{ iccId: 'SIM 2' }
];
Settings.init();
});
test('init is correctly executed', function() {
assert.equal(Settings.mmsServiceId, 'no service ID');
// Four calls for
// maxConcatenatedMessages/mmsSizeLimitation/mmsServiceId/smsServiceId
sinon.assert.callCount(navigator.mozSettings.createLock, 4);
});
test('Dual SIM state is correctly reported', function() {
assert.isTrue(Settings.hasSeveralSim());
assert.isTrue(Settings.isDualSimDevice());
});
test('the settings are correctly retrieved', function() {
for (var prop in Settings.SERVICE_ID_KEYS) {
var setting = Settings.SERVICE_ID_KEYS[prop];
assertSettingIsRetrieved(prop, setting, 0);
}
});
test('getServiceIdByIccId returns the correct id', function() {
assert.equal(Settings.getServiceIdByIccId('SIM 1'), 0);
assert.equal(Settings.getServiceIdByIccId('SIM 2'), 1);
assert.isNull(Settings.getServiceIdByIccId('SIM 3'));
});
});
test('in a single SIM device', function() {
navigator.mozMobileConnections = [{ iccId: 'SIM 1' }];
Settings.init();
// Two calls for
// maxConcatenatedMessages/mmsSizeLimitation
sinon.assert.calledTwice(navigator.mozSettings.createLock);
for (var prop in Settings.SERVICE_ID_KEYS) {
var setting = Settings.SERVICE_ID_KEYS[prop];
assert.isNull(findSettingsReq(setting));
assert.equal(Settings[prop], 'no service ID');
}
assert.isFalse(Settings.hasSeveralSim());
assert.isFalse(Settings.isDualSimDevice());
assert.isNull(Settings.getServiceIdByIccId('SIM 1'));
});
test('in a dual SIM device with only 1 SIM', function() {
navigator.mozMobileConnections = [{ iccId: 'SIM 1' }, { iccId: null }];
Settings.init();
// Four calls for
// maxConcatenatedMessages/mmsSizeLimitation/mmsServiceId/smsServiceId
sinon.assert.callCount(navigator.mozSettings.createLock, 4);
for (var prop in Settings.SERVICE_ID_KEYS) {
var setting = Settings.SERVICE_ID_KEYS[prop];
assert.ok(findSettingsReq(setting));
}
assert.isFalse(Settings.hasSeveralSim());
assert.isTrue(Settings.isDualSimDevice());
assert.equal(Settings.getServiceIdByIccId('SIM 1'), 0);
});
test('in a triple SIM device with 2 SIMs', function() {
navigator.mozMobileConnections = [
{ iccId: 'SIM 1' },
{ iccId: 'SIM 2' },
{ iccId: null }
];
Settings.init();
assert.isTrue(Settings.hasSeveralSim());
assert.isTrue(Settings.isDualSimDevice());
});
test('mmsServiceId observer and update', function() {
navigator.mozMobileConnections = [
{ iccId: 'SIM 1' },
{ iccId: 'SIM 2' }
];
Settings.init();
for (var prop in Settings.SERVICE_ID_KEYS) {
var setting = Settings.SERVICE_ID_KEYS[prop];
triggerSettingsReqSuccess(setting, 0);
MockNavigatorSettings.mTriggerObservers(setting, {settingValue: 1});
assert.equal(Settings[prop], 1);
}
});
suite('switchSimHandler for async callback when ready', function() {
var conn;
var listenerSpy;
// The real navigator.mozMobileConnections is not a real array
var mockMozMobileConnections = {
0: {
iccId: 'SIM 1',
addEventListener: function() {},
removeEventListener: function() {},
data: {
state: 'searching'
}
},
1: {
iccId: 'SIM 2',
addEventListener: function() {},
removeEventListener: function() {},
data: {
state: 'searching'
}
}
};
setup(function() {
if (!('mozMobileConnections' in navigator)) {
navigator.mozMobileConnections = null;
}
// let's use the mock's implementation
navigator.mozSettings.createLock.restore();
this.sinon.stub(window.navigator, 'mozMobileConnections',
mockMozMobileConnections);
conn = window.navigator.mozMobileConnections[1];
listenerSpy = this.sinon.spy(conn, 'addEventListener');
this.sinon.stub(conn, 'removeEventListener');
this.sinon.stub(console, 'error');
Settings.init();
});
test('Should return resolve directly if state is already registered',
function(done) {
conn.data.state = 'registered';
Settings.switchMmsSimHandler(1).then(function onsuccess(){
sinon.assert.notCalled(listenerSpy);
}).then(done,done);
});
test('Should not be triggered if connection does not exist',
function(done) {
Settings.switchMmsSimHandler('invalid').catch(function onerror(err){
sinon.assert.notCalled(conn.removeEventListener);
assert.equal(err, 'Invalid connection');
}).then(done,done);
});
test('callback when data connection state changes', function(done) {
conn.data.state = 'searching';
Settings.switchMmsSimHandler(1).then(function onsuccess(){
assert.equal(
MockNavigatorSettings.mSettings['ril.mms.defaultServiceId'], 1
);
assert.equal(
MockNavigatorSettings.mSettings['ril.data.defaultServiceId'], 1
);
sinon.assert.calledOnce(conn.removeEventListener);
}).then(done,done);
conn.data.state = 'registered';
listenerSpy.yield();
});
});
test('setReadAheadThreadRetrieval()', function() {
Settings.setReadAheadThreadRetrieval(9);
sinon.assert.calledWithMatch(
navigator.mozSettings.createLock.lastCall.returnValue.set,
{ 'ril.sms.maxReadAheadEntries' : 9 }
);
});
});
});

View file

@ -0,0 +1,130 @@
/* global SharedComponents, Template */
'use strict';
require('/views/shared/js/shared_components.js');
suite('SharedComponents >', function() {
setup(function() {
loadBodyHTML('/index.html');
});
suite('phoneDetails >', function() {
test('localized correctly', function() {
var phoneDetails = SharedComponents.phoneDetails({
number: '1',
type: 'Mobile'
}).toDocumentFragment();
assert.equal(
phoneDetails.querySelector('.phone-type').getAttribute('data-l10n-id'),
'Mobile'
);
assert.equal(
phoneDetails.querySelector('.phone-type-separator')
.getAttribute('data-l10n-id'),
'phone-type-separator'
);
assert.equal(
phoneDetails.querySelector('.phone-carrier-separator')
.getAttribute('data-l10n-id'),
'phone-carrier-separator'
);
});
test('no phone details passed', function() {
assert.throws(function() {
SharedComponents.phoneDetails();
});
assert.throws(function() {
SharedComponents.phoneDetails(null, {});
});
});
test('no phone type, no phone carrier', function() {
var phoneDetails = SharedComponents.phoneDetails({
number: '1'
}).toDocumentFragment();
assert.isNull(phoneDetails.querySelector('.has-phone-type'));
assert.isNull(phoneDetails.querySelector('.has-phone-carrier'));
assert.equal(
phoneDetails.querySelector('.phone-number').textContent, '1'
);
});
test('no phone carrier', function() {
var phoneDetails = SharedComponents.phoneDetails({
number: '1',
type: 'Mobile'
}).toDocumentFragment();
assert.isNotNull(phoneDetails.querySelector('.has-phone-type'));
assert.isNull(phoneDetails.querySelector('.has-phone-carrier'));
assert.equal(
phoneDetails.querySelector('.phone-number').textContent, '1'
);
assert.equal(
phoneDetails.querySelector('.phone-type').textContent, 'Mobile'
);
});
test('no phone type, but with carrier', function() {
var phoneDetails = SharedComponents.phoneDetails({
number: '1',
carrier: 'MTS'
}).toDocumentFragment();
assert.isNull(phoneDetails.querySelector('.has-phone-type'));
assert.isNotNull(phoneDetails.querySelector('.has-phone-carrier'));
assert.equal(
phoneDetails.querySelector('.phone-number').textContent, '1'
);
assert.equal(
phoneDetails.querySelector('.phone-carrier').textContent, 'MTS'
);
});
test('with phone type and carrier', function() {
var phoneDetails = SharedComponents.phoneDetails({
number: '1',
type: 'Mobile',
carrier: 'MTS'
}).toDocumentFragment();
assert.isNotNull(phoneDetails.querySelector('.has-phone-type'));
assert.isNotNull(phoneDetails.querySelector('.has-phone-carrier'));
assert.equal(
phoneDetails.querySelector('.phone-number').textContent, '1'
);
assert.equal(
phoneDetails.querySelector('.phone-type').textContent, 'Mobile'
);
assert.equal(
phoneDetails.querySelector('.phone-carrier').textContent, 'MTS'
);
});
test('with interpolation options', function() {
this.sinon.spy(Template.prototype, 'prepare');
var interpolationOptions = {
safe: ['number']
};
SharedComponents.phoneDetails({
number: '1',
type: 'Mobile',
carrier: 'MTS'
}, interpolationOptions);
sinon.assert.calledWithMatch(
Template.prototype.prepare,
sinon.match.any,
interpolationOptions
);
});
});
});

View file

@ -0,0 +1,287 @@
/* global MockasyncStorage, SilentSms, MockMozAlarms, MocksHelper,
MockNavigatormozSetMessageHandler
*/
'use strict';
require('/views/shared/test/unit/mock_moz_alarm.js');
require('/views/shared/test/unit/mock_async_storage.js');
require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js');
require('/views/shared/js/task_runner.js');
require('/views/shared/js/silent_sms.js');
var mocksHelperForActivityHandler = new MocksHelper(['asyncStorage']).init();
suite('Silent SMS > ', function() {
mocksHelperForActivityHandler.attachTestHelpers();
var realMozSetMessageHandler,
realMozAlarms,
mozAlarmsId = 0;
var FakePort = function() {};
FakePort.prototype.postMessageToSms = function(e) {
this.onmessage.call(this, e);
};
FakePort.prototype.postMessage = function(e) {};
function MockAlarmRequest(willFail) {
this._willFail = willFail;
}
Object.defineProperty(MockAlarmRequest.prototype, 'onsuccess', {
set: function (callback) {
if (!this._willFail) {
this.result = mozAlarmsId++;
(typeof callback === 'function') &&
callback.call(this, { target: this });
}
}
});
Object.defineProperty(MockAlarmRequest.prototype, 'onerror', {
set: function (callback) {
if (this._willFail) {
this.error = { name: 'error' };
(typeof callback === 'function') &&
callback.call(this, { target: this });
}
}
});
function newSuccessfulRequest() {
return new MockAlarmRequest(false);
}
function newFailingRequest() {
return new MockAlarmRequest(true);
}
suiteSetup(function() {
realMozSetMessageHandler = navigator.mozSetMessageHandler;
navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler;
MockNavigatormozSetMessageHandler.mSetup();
realMozAlarms = window.navigator.mozAlarms;
window.navigator.mozAlarms = MockMozAlarms;
});
suiteTeardown(function() {
navigator.mozSetMessageHandler = realMozSetMessageHandler;
window.navigator.mozAlarms = realMozAlarms;
});
suite('connection from usage', function() {
var request,
message;
var fakePort = new FakePort();
setup(function() {
SilentSms.init();
request = {
keyword: 'costcontrolSmsQuery',
port: fakePort
};
MockNavigatormozSetMessageHandler.mTrigger('connection', request);
message = {
action: 'enable',
smsNumbers: [1234],
type: 'balance'
};
});
teardown(function() {
MockNavigatormozSetMessageHandler.mTeardown();
MockasyncStorage.mTeardown();
message = null;
});
test('silentMode is enabled correctly by IAC', function(done) {
var request = newSuccessfulRequest();
this.sinon.stub(navigator.mozAlarms, 'add').returns(request);
var smsToSilent = [1234, 111, 234];
message.smsNumbers = smsToSilent;
this.sinon.stub(
FakePort.prototype, 'postMessage',
function assertionFunction(e) {
done(function() {
assert.equal(e, 'silentSMSEnabled');
var usageSilentSms = MockasyncStorage.keys.usageSilentSms;
var alarmId = usageSilentSms[message.type].alarmId;
assert.isDefined(alarmId);
assert.deepEqual(usageSilentSms[message.type].smsNumbers,
smsToSilent);
var smsIndex = usageSilentSms.smsIndex;
smsToSilent.forEach(function (silentSmsNumber) {
assert.isTrue(smsIndex.indexOf(silentSmsNumber) !== -1);
});
});
}
);
fakePort.postMessageToSms({
data: message
});
});
test('silentMode is disabled correctly by IAC', function(done) {
var request = newSuccessfulRequest();
this.sinon.stub(navigator.mozAlarms, 'add').returns(request);
var alarmId, usageSilentSms;
this.sinon.stub(
FakePort.prototype, 'postMessage',
function assertionFunction(actionMsg) {
if (actionMsg === 'silentSMSEnabled') {
try {
usageSilentSms = MockasyncStorage.keys.usageSilentSms;
// The Timeout alarm is actived and usageSilentSms has been
// updated
alarmId = usageSilentSms[message.type].alarmId;
assert.ok(alarmId !== undefined);
// Generating call for disable the silentMode for balance
message.action = 'disable';
fakePort.postMessageToSms({
data: message
});
} catch (e) {
done(e);
}
}
if (actionMsg === 'silentSMSDisabled') {
done(function() {
usageSilentSms = MockasyncStorage.keys.usageSilentSms;
// Check the usageSilentSms object was update
assert.isNull(usageSilentSms[message.type]);
});
}
}
);
fakePort.postMessageToSms({
data: message
});
});
test('silentMode is disabled correctly by an alarm', function(done) {
var mAlarmData = {data: {
alarmType: 'silentSmsTimeout',
type: 'balance'
}};
MockasyncStorage.keys.usageSilentSms = {
balance: {smsNumbers:[1234,111,234], alarmId: 1},
smsIndex: [1234,111,234]
};
// Mocking asyncStorage because it's the last method called after an
// alarm is fired.
this.sinon.stub(MockasyncStorage, 'setItem', function(usageSilentSMS) {
done(function() {
usageSilentSMS = MockasyncStorage.keys.usageSilentSms;
// Check the usageSilentSms object was update
assert.isNull(usageSilentSMS.balance);
});
});
// Firing the alarm to disable the balance silentMode
MockNavigatormozSetMessageHandler.mTrigger('alarm', mAlarmData);
});
test('Received two enabled request for the same type before the timeout' +
' alarm is fired', function(done) {
var request = newSuccessfulRequest();
this.sinon.stub(navigator.mozAlarms, 'add').returns(request);
var isFirstTime = true,
alarmId;
this.sinon.stub(
FakePort.prototype, 'postMessage',
function assertionFunction(e) {
assert.equal(e, 'silentSMSEnabled');
var usageSilentSms = MockasyncStorage.keys.usageSilentSms;
if (isFirstTime) {
try {
isFirstTime = false;
alarmId = usageSilentSms[message.type].alarmId;
assert.ok(alarmId !== undefined);
// Launched the second call
fakePort.postMessageToSms({
data: message
});
} catch(error) {
done(error);
}
} else {
done(function() {
// The second call removes the first timeout alarm and adds
// another one
var newAlarmId = usageSilentSms[message.type].alarmId;
assert.notEqual(alarmId, newAlarmId);
});
}
}
);
fakePort.postMessageToSms({
data: message
});
});
test('silentMode is enabled correctly by IAC when addAlarm fails',
function(done) {
var request = newFailingRequest();
this.sinon.stub(navigator.mozAlarms, 'add').returns(request);
var NO_ALARM_ID = -1;
this.sinon.stub(
FakePort.prototype, 'postMessage',
function assertionFunction(e) {
done(function(){
assert.equal(e, 'silentSMSEnabled');
var usageSilentSms = MockasyncStorage.keys.usageSilentSms;
var alarmId = usageSilentSms[message.type].alarmId;
assert.equal(alarmId, NO_ALARM_ID);
});
}
);
fakePort.postMessageToSms({
data: message
});
});
test('Check checkSilentModeFor behaviour', function (done) {
var fakeUsageSilentSms = {
'balance': {
'smsNumbers': [1234, 111, 234],
'alarmId': 0,
'timeAlarm': new Date()
},
'topup': {
'smsNumbers': [888, 222],
'alarmId': 0,
'timeAlarm': new Date()
},
'smsIndex':[1234, 111, 234, 888, 222]
};
MockasyncStorage.keys.usageSilentSms = fakeUsageSilentSms;
// Check a silent sms number
SilentSms.checkSilentModeFor(1234).then(function (isSilent) {
assert.isTrue(isSilent);
}).then(SilentSms.checkSilentModeFor.bind(null, 999))
.then(function (isSilent) {
assert.isFalse(isSilent);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,726 @@
/*global SMIL, MocksHelper,
AssetsHelper,
Promise
*/
'use strict';
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/js/wbmp.js');
require('/views/shared/js/smil.js');
var mocksHelperForSMIL = new MocksHelper([
'Utils'
]);
mocksHelperForSMIL.init();
suite('SMIL', function() {
var testImageBlob;
var testAudioBlob;
var testVideoBlob;
var testWbmpBlob;
var testContactBlob;
suiteSetup(function smil_suiteSetup(done) {
mocksHelperForSMIL.suiteSetup();
var mediafolder = '/views/shared/test/unit/media';
var blobPromises = [
AssetsHelper.generateImageBlob(300, 300, 'image/jpeg', 0.25).then(
(blob) => testImageBlob = blob
),
AssetsHelper.loadFileBlob(`${mediafolder}/grid.wbmp`).then(
(blob) => testWbmpBlob = blob
),
AssetsHelper.loadFileBlob(`${mediafolder}/audio.oga`).then(
(blob) => testAudioBlob = blob
),
AssetsHelper.loadFileBlob(`${mediafolder}/video.ogv`).then(
(blob) => testVideoBlob = blob
),
AssetsHelper.loadFileBlob(`${mediafolder}/contacts.vcf`).then(
(blob) => testContactBlob = blob
)
];
Promise.all(blobPromises).then(() => done(), done);
});
suiteTeardown(function() {
mocksHelperForSMIL.suiteTeardown();
});
setup(function() {
mocksHelperForSMIL.setup();
});
teardown(function() {
mocksHelperForSMIL.teardown();
});
suite('SMIL.parse', function() {
test('Text only message without smil', function(done) {
var text = ['Test text', 'omg!'];
// minimal fake data for text only message without smil
var messageData = {
attachments: [
{content: new Blob([text[0]], {type: 'text/plain'})},
{content: new Blob([text[1]], {type: 'text/plain'})}
]
};
SMIL.parse(messageData).then((output) => {
// one slide for each attachment is returned
assert.equal(output.length, 2);
// the text has not been joined in one slide
assert.equal(output[0].text, text[0]);
assert.equal(output[1].text, text[1]);
}).then(done, done);
});
test('Text and image message without smil', function(done) {
var text = ['Test above image', 'Text below image'];
var messageData = {
attachments: [
{content: new Blob([text[0]], {type: 'text/plain'})},
{
content: testImageBlob,
location: 'example.jpg'
},
{content: new Blob([text[1]], {type: 'text/plain'})},
]
};
SMIL.parse(messageData).then((output) => {
// three slides returned
assert.equal(output.length, 3);
// the order of the attached components should be respected, the text
// is located on two different slides (not joined)
assert.equal(output[0].text, text[0]);
assert.equal(output[1].blob, testImageBlob);
assert.equal(output[1].name, 'example.jpg');
assert.equal(output[2].text, text[1]);
}).then(done, done);
});
test('Image only message without smil', function(done) {
// minimal fake data for text only message without smil
var messageData = {
attachments: [
{
content: testImageBlob,
location: 'example.jpg'
}
]
};
SMIL.parse(messageData).then((output) => {
// one slide returned
assert.equal(output.length, 1);
// no text in this slide !
assert.ok(!output[0].text);
assert.equal(output[0].blob, testImageBlob);
assert.equal(output[0].name, 'example.jpg');
}).then(done, done);
});
test('Minimal SMIL doc', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><img src="example.jpg"/>' +
'<text src="text1"/></par></body></smil>',
attachments: [{
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[0].blob, testImageBlob);
assert.equal(output[0].name, 'example.jpg');
}).then(done, done);
});
test('SMIL doc with 2 text only slides', function(done) {
var text = ['Test slide 1', 'Test slide 2'];
var message = {
smil: '<smil><body><par><text src="cid:1"/></par>' +
'<par><text src="cid:2"/></par></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([text[0]], {type: 'text/plain'})
},{
id: '<2>',
location: 'text2',
content: new Blob([text[1]], {type: 'text/plain'})
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output.length, 2);
assert.equal(output[0].text, text[0]);
assert.equal(output[1].text, text[1]);
}).then(done, done);
});
test('SMIL doc with cid: prefixes on src', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><img src="cid:23"/>' +
'<text src="cid:text1"/></par></body></smil>',
attachments: [{
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<23>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[0].blob, testImageBlob);
assert.equal(output[0].name, 'example.jpg');
}).then(done, done);
});
test('SMIL doc with cid: prefixes on src and no location', function(done) {
// iphone!
// parsed as a non smil message
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><img src="cid:23"/>' +
'<text src="cid:1"/></par></body></smil>',
attachments: [{
location: '<1>',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<23>',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[1].blob, testImageBlob);
assert.isUndefined(output[1].name, 'name is undefined');
}).then(done, done);
});
test('SMIL doc with cid: prefixes on src pointing to ids', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><img src="cid:2"/>' +
'<text src="cid:1"/></par></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<2>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[0].blob, testImageBlob);
assert.equal(output[0].name, 'example.jpg');
}).then(done, done);
});
test('SMIL doc with bad cid: references', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><text src="cid:1"/>' +
'<img src="cid:20"/></par></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<2>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[1].blob, testImageBlob);
assert.equal(output[1].name, 'example.jpg');
}).then(done, done);
});
suite('SMIL doc: audio and image on same <par>', function() {
var testText = 'Testing 1 2 3';
var result;
setup(function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body><par><text src="cid:1"/>' +
'<audio src="cid:2"/><img src="cid:3"/>' +
'</par></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
}, {
id: '<2>',
location: 'audio.oga',
content: testAudioBlob
}, {
id: '<3>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
result = output;
}).then(done, done);
});
test('Results in two "slides"', function() {
assert.equal(result.length, 2);
});
test('First slide does not have text', function() {
assert.equal(result[0].text, undefined);
});
test('First slide contains audio', function() {
assert.equal(result[0].name, 'audio.oga');
assert.equal(result[0].blob, testAudioBlob);
});
test('Second slide contains text', function() {
assert.equal(result[1].text, testText);
});
test('Second slide contains image', function() {
assert.equal(result[1].name, 'example.jpg');
assert.equal(result[1].blob, testImageBlob);
});
});
test('empty SMIL doc with attachments', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<2>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[1].blob, testImageBlob);
assert.equal(output[1].name, 'example.jpg');
}).then(done, done);
});
test('empty SMIL doc with attachments and no location', function(done) {
var testText = 'Testing 1 2 3';
var message = {
smil: '<smil><body></body></smil>',
attachments: [{
id: '<1>',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<2>',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[1].blob, testImageBlob);
assert.isUndefined(output[1].name, 'name is undefined');
}).then(done, done);
});
test('Type of attachment is WBMP format', function(done) {
var message = {
smil: '<smil><body><par><img src="grid.wbmp"/>' +
'</par></body></smil>',
attachments: [{
location: 'grid.wbmp',
content: testWbmpBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].blob.type, 'image/png');
assert.equal(output[0].name, 'grid.png');
}).then(done, done);
});
suite('Type of attachment is vcard format', function() {
var attachments;
var testText = {
location: 'text1',
content: new Blob(['test Text'], {type: 'text/plain'})
};
setup(function() {
attachments = [{
location: 'contacts.vcf',
content: testContactBlob
}];
});
test('only contact in attachment', function(done) {
var message = {
smil: '<smil><body><par><ref src="contacts.vcf"/>' +
'</par></body></smil>',
attachments: attachments
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].blob, testContactBlob);
assert.equal(output[0].name, 'contacts.vcf');
assert.isUndefined(output[0].text);
}).then(done, done);
});
test('contact with text', function(done) {
attachments.push(testText);
var message = {
smil: '<smil><body><par><text src="' + testText.location + '"/>' +
'<ref src="contacts.vcf"/></par></body></smil>',
attachments: attachments
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].blob, testContactBlob);
assert.equal(output[0].name, 'contacts.vcf');
assert.equal(output[0].text, 'test Text');
}).then(done, done);
});
test('contact attachment with no smil', function(done) {
var message = {
attachments: attachments
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].blob, testContactBlob);
assert.equal(output[0].name, 'contacts.vcf');
assert.isUndefined(output[0].text);
}).then(done, done);
});
test('contact attachment/text with no smil', function(done) {
attachments.push(testText);
var message = {
attachments: attachments
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].blob, testContactBlob);
assert.equal(output[0].name, 'contacts.vcf');
assert.equal(output[1].text, 'test Text');
}).then(done, done);
});
});
test('Attachment count and elements in SMIL is unmatched', function(done) {
var testText = 'Test text';
var message = {
smil: '<smil><body><par><text src="cid:1"/>' +
'</par></body></smil>',
attachments: [{
id: '<1>',
location: 'text1',
content: new Blob([testText], {type: 'text/plain'})
},{
id: '<2>',
location: 'example.jpg',
content: testImageBlob
}]
};
SMIL.parse(message).then((output) => {
assert.equal(output[0].text, testText);
assert.equal(output[1].blob, testImageBlob);
}).then(done, done);
});
});
suite('SMIL.generate', function() {
test('Text only message', function(done) {
var smilTest = [{
text: 'This is a test of the SMIL generate method'
}];
var output = SMIL.generate(smilTest);
var doc = (new DOMParser())
.parseFromString(output.smil, 'application/xml')
.documentElement;
// only one attachment
assert.equal(output.attachments.length, 1);
// only one <par> tag in the smil output
assert.equal(doc.getElementsByTagName('par').length, 1);
// check that the content of the text blob is what we said it should be
var textReader = new FileReader();
textReader.onload = function(event) {
assert.equal(event.target.result, smilTest[0].text);
done();
};
textReader.readAsText(output.attachments[0].content);
});
test('Message with image and text', function() {
var smilTest = [{
text: 'Testing a caption',
name: 'kitten-450.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
var doc = (new DOMParser())
.parseFromString(output.smil, 'application/xml')
.documentElement;
// two attachments (text and image)
assert.equal(output.attachments.length, 2);
// only one <par> tag
assert.equal(doc.querySelectorAll('par').length, 1);
// the img is before the text
assert.equal(doc.querySelectorAll('img + text').length, 1);
assert.equal(doc.querySelector('text').getAttribute('region'), 'Text');
assert.equal(doc.querySelector('img').getAttribute('region'), 'Image');
});
test('Message with vcard and text', function() {
var smilTest = [{
text: 'Testing a caption',
name: 'contact.vcf',
blob: testContactBlob
}];
var output = SMIL.generate(smilTest);
var doc = (new DOMParser())
.parseFromString(output.smil, 'application/xml')
.documentElement;
// two attachments (text and contact)
assert.equal(output.attachments.length, 2);
// only one <par> tag
assert.equal(doc.querySelectorAll('par').length, 1);
// the ref is before the text
assert.equal(doc.querySelectorAll('ref + text').length, 1);
assert.equal(doc.querySelector('text').getAttribute('region'), 'Text');
assert.isNull(doc.querySelector('ref').getAttribute('region'));
});
test('Message with only vcard', function() {
var smilTest = [{
name: 'contact.vcf',
blob: testContactBlob
}];
var output = SMIL.generate(smilTest);
var doc = (new DOMParser())
.parseFromString(output.smil, 'application/xml')
.documentElement;
// one attachment (contact)
assert.equal(output.attachments.length, 1);
// only one <par> tag
assert.equal(doc.querySelectorAll('par').length, 1);
// the ref appears
assert.equal(doc.querySelectorAll('ref').length, 1);
assert.isNull(doc.querySelector('text'));
assert.isNull(doc.querySelector('ref').getAttribute('region'));
});
test('Message with path as filename', function() {
var smilTest = [{
text: 'Testing a caption',
name: 'SDCARD/DCIM/kitten-450.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
assert.equal(output.attachments.length, 2);
assert.equal(output.attachments[0].location, 'kitten-450.jpg');
});
test('Message with same filename from different path', function() {
var smilTest = [{
text: 'Testing a caption',
name: 'SDCARD/DCIM/kitten-450.jpg',
blob: testImageBlob
}, {
text: 'Testing a caption',
name: 'SDCARD/DCIM23/kitten-450.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
assert.equal(output.attachments.length, 4);
assert.equal(output.attachments[0].location, 'kitten-450.jpg');
assert.equal(output.attachments[2].location, 'kitten-450_2.jpg');
});
suite('SMIL filename: length limitations', function() {
var bareString, extString;
bareString = 'dfjaksdhfkasjdhfaksjdfhksjdahfaksjdhfdfsfffff';
extString = 'dfjaksdhfkasjdhfaksjdfhksjdahfaksjdhfdfsf';
test('Filenames truncated', function() {
var smilTest = [{
text: 'Truncate filename',
name: bareString,
blob: testImageBlob
},{
text: 'With extension',
name: extString + '.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
assert.equal(
output.attachments[0].location,
bareString.slice(0, 40)
);
assert.equal(
output.attachments[2].location,
extString.slice(0, 40 - 4) + '.jpg'
);
assert.ok(output.attachments.every(function(elem) {
return elem.location.length <= 40;
}));
});
suite('Clashing filenames also truncated', function() {
var smilTest, bare, extension;
var bareString = 'dfjaksdhfkasjdhfaksjdfhksjdahfaksjdhfdf';
var extString = bareString.slice(0, -3);
setup(function() {
smilTest = [{
text: 'Setup clash',
name: bareString,
blob: testImageBlob
},{
text: 'Testing first clash',
name: bareString,
blob: testImageBlob
},{
text: 'Testing second clash',
name: bareString,
blob: testImageBlob
}];
bare = SMIL.generate(smilTest);
smilTest.forEach(function(elem) {
elem.name = extString + '.jpg';
});
extension = SMIL.generate(smilTest);
});
test('First name unchanged', function() {
assert.equal(
bare.attachments[0].location,
bareString
);
assert.equal(
extension.attachments[0].location,
extString + '.jpg'
);
});
test('2nd name shortened due to potential marker length', function() {
assert.equal(
bare.attachments[2].location,
bareString.slice(0, -1)
);
assert.equal(
extension.attachments[2].location,
extString.slice(0, -1) + '.jpg'
);
});
test('Third name shortened/expanded as needed', function() {
assert.equal(
bare.attachments[4].location,
bareString.slice(0, -1) + '_2'
);
assert.equal(
extension.attachments[4].location,
extString.slice(0, -2) + '.jpg'
);
});
test('All combinations <= 40 characters', function() {
assert.ok(bare.attachments.every(function(elem) {
return elem.location.length <= 40;
}));
assert.ok(extension.attachments.every(function(elem) {
return elem.location.length <= 40;
}));
});
});
});
test('Message with duplicate filename', function() {
var smilTest = [{
name: 'kitten-450.jpg',
blob: testImageBlob
}, {
name: 'kitten-450.jpg',
blob: testImageBlob
}, {
name: 'kitten-450.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
var doc = (new DOMParser())
.parseFromString(output.smil, 'application/xml')
.documentElement;
assert.equal(output.attachments.length, 3);
var images = Array.prototype.slice.call(doc.querySelectorAll('img'));
assert.equal(images.length, 3);
// ensure the file names have been properly created
var filenames = [
'kitten-450.jpg',
'kitten-450_2.jpg',
'kitten-450_3.jpg'
];
assert.deepEqual(output.attachments.map(function(attachment) {
return attachment.location;
}), filenames, 'List of filenames matches attachments');
assert.deepEqual(images.map(function(img) {
return img.getAttribute('src');
}), filenames, 'List of filenames matches <img> tags');
});
test('Message with non-ascii filenames', function() {
var smilTest = [{
name: 'kitten♥-450.jpg',
blob: testImageBlob
}, {
// 5 replaced chars
name: '새끼고양이.jpg',
blob: testImageBlob
}, {
// 5 replaced chars
name: 'ἀρετή.jpg',
blob: testImageBlob
}, {
// testing non-replaced chars
name: 'abzABZ019_#.()?&%-.jpg',
blob: testImageBlob
}, {
// quotes MUST be replaced - filename content is used in xml
name: '"\'.jpg',
blob: testImageBlob
}];
var output = SMIL.generate(smilTest);
assert.equal(output.attachments.length, 5);
assert.equal(output.attachments[0].location, 'kitten#-450.jpg');
// output from the next two also tests a clash after replace
assert.equal(output.attachments[1].location, '#####.jpg');
assert.equal(output.attachments[2].location, '#####_2.jpg');
// this one has nothing to replace:
assert.equal(output.attachments[3].location, smilTest[3].name);
// quotes must be replaced
assert.equal(output.attachments[4].location, '##.jpg');
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,159 @@
/*global App,
Drafts,
InboxView,
LazyLoader,
MessageManager,
MocksHelper,
Navigation,
Utils
*/
'use strict';
require('/views/shared/js/app.js');
require('/services/test/unit/mock_message_manager.js');
require('/services/test/unit/mock_drafts.js');
require('/views/shared/test/unit/mock_navigation.js');
require('/views/shared/test/unit/mock_inbox.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/shared/test/unit/mocks/mock_lazy_loader.js');
var mocksHelper = new MocksHelper([
'Drafts',
'MessageManager',
'LazyLoader',
'Navigation',
'InboxView',
'Utils'
]).init();
suite('Startup >', function() {
var originalMozHasPendingMessage;
mocksHelper.attachTestHelpers();
suiteSetup(function(done) {
originalMozHasPendingMessage = navigator.mozHasPendingMessage;
navigator.mozHasPendingMessage = () => {};
sinon.stub(window, 'addEventListener');
require('/views/shared/js/startup.js', done);
});
suiteTeardown(function() {
navigator.mozHasPendingMessage = originalMozHasPendingMessage;
window.addEventListener.restore();
});
setup(function() {
this.sinon.stub(window, 'dispatchEvent');
this.sinon.stub(window, 'removeEventListener');
this.sinon.stub(navigator, 'mozHasPendingMessage').returns(false);
this.sinon.stub(LazyLoader, 'load').returns(Promise.reject());
this.sinon.stub(MessageManager, 'init');
this.sinon.stub(Navigation, 'init');
this.sinon.stub(InboxView, 'init');
this.sinon.spy(InboxView, 'renderThreads');
this.sinon.stub(InboxView, 'once');
this.sinon.stub(Navigation, 'once');
this.sinon.stub(Navigation, 'isDefaultPanel');
this.sinon.stub(App, 'setReady');
this.sinon.spy(Utils, 'initializeShimHost');
this.sinon.stub(Drafts, 'init');
});
test('if target panel is default one', function() {
Navigation.isDefaultPanel.returns(true);
window.addEventListener.withArgs('DOMContentLoaded').yield();
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
// Navigate to Inbox immediately.
sinon.assert.callOrder(
Utils.initializeShimHost,
MessageManager.init,
Drafts.init,
InboxView.init,
Navigation.init,
InboxView.renderThreads
);
sinon.assert.notCalled(LazyLoader.load);
// First page of threads loaded
InboxView.once.withArgs('visually-loaded').yield();
// Lazy load the rest of scripts only once first page of threads is loaded
sinon.assert.calledOnce(LazyLoader.load);
});
test('if first panel is not default one', function() {
Navigation.isDefaultPanel.returns(false);
window.addEventListener.withArgs('DOMContentLoaded').yield();
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
sinon.assert.callOrder(
Utils.initializeShimHost,
MessageManager.init,
InboxView.init,
Navigation.init,
LazyLoader.load
);
sinon.assert.notCalled(InboxView.renderThreads);
sinon.assert.calledOnce(LazyLoader.load);
// Threads should start rendering only once target panel is ready.
Navigation.once.withArgs('navigated').yield();
// Now we have time and resources to render threads.
sinon.assert.calledOnce(InboxView.renderThreads);
// App is marked is ready only when all threads are loaded.
sinon.assert.notCalled(App.setReady);
InboxView.once.withArgs('fully-loaded').yield();
sinon.assert.calledOnce(App.setReady);
});
['notification', 'activity'].forEach((eventName) => {
test('if has pending "' + eventName + '" system message', function() {
Navigation.isDefaultPanel.returns(true);
navigator.mozHasPendingMessage.withArgs(eventName).returns(true);
window.addEventListener.withArgs('DOMContentLoaded').yield();
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
sinon.assert.callOrder(
Utils.initializeShimHost,
MessageManager.init,
InboxView.init,
Navigation.init,
LazyLoader.load
);
sinon.assert.notCalled(InboxView.renderThreads);
sinon.assert.calledOnce(LazyLoader.load);
// Threads should start rendering only once target panel is ready.
Navigation.once.withArgs('navigated').yield();
// Now we have time and resources to render threads.
sinon.assert.calledOnce(InboxView.renderThreads);
// App is marked is ready only when all threads are loaded.
sinon.assert.notCalled(App.setReady);
InboxView.once.withArgs('fully-loaded').yield();
sinon.assert.calledOnce(App.setReady);
});
});
});

View file

@ -0,0 +1,794 @@
/*global Contacts,
MessageManager,
MockContact,
MocksHelper,
MockMessages,
Navigation,
NotificationHelper,
Notify,
Settings,
SilentSms,
SMIL,
SystemMessageHandler,
Utils
*/
'use strict';
require('/shared/test/unit/mocks/mock_notification_helper.js');
require('/views/shared/test/unit/mock_contact.js');
require('/views/shared/test/unit/mock_contacts.js');
require('/views/shared/test/unit/mock_messages.js');
require('/services/test/unit/mock_message_manager.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/test/unit/mock_notify.js');
require('/views/shared/test/unit/mock_navigation.js');
require('/views/shared/test/unit/mock_silent_sms.js');
require('/views/shared/test/unit/mock_smil.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/js/system_message_handler.js');
var mocksHelperForActivityHandler = new MocksHelper([
'Contacts',
'MessageManager',
'Navigation',
'NotificationHelper',
'Notify',
'Settings',
'SilentSms',
'SMIL',
'Utils'
]).init();
suite('SystemMessageHandler', function() {
mocksHelperForActivityHandler.attachTestHelpers();
var isDocumentHidden;
var navigatorMocks = new Map([
['mozSetMessageHandler', () => {}],
['requestWakeLock', () => {}],
['mozApps', { launch() {} }]
]);
suiteSetup(function() {
navigatorMocks.forEach((mock, key) => {
if (!(key in navigator)) {
navigator[key] = mock;
}
});
Object.defineProperty(document, 'hidden', {
configurable: true,
get: function() {
return isDocumentHidden;
}
});
});
suiteTeardown(function() {
delete document.hidden;
});
setup(function() {
this.sinon.stub(Utils, 'alert').returns(Promise.resolve());
isDocumentHidden = false;
this.sinon.stub(document, 'addEventListener');
SystemMessageHandler.init();
});
test('init()', function() {
this.sinon.stub(navigator, 'mozSetMessageHandler');
SystemMessageHandler.init();
sinon.assert.calledTwice(window.navigator.mozSetMessageHandler);
sinon.assert.calledWith(
window.navigator.mozSetMessageHandler, 'sms-received'
);
sinon.assert.calledWith(
window.navigator.mozSetMessageHandler, 'notification'
);
});
suite('sms-received system message', function() {
var appStub, wakeLockStub, notificationStub;
function assertWakeLockIsRequested() {
sinon.assert.calledOnce(navigator.requestWakeLock);
sinon.assert.calledWith(navigator.requestWakeLock, 'cpu');
// We shouldn't release wake lock until we completely handled new message.
sinon.assert.notCalled(wakeLockStub.unlock);
}
function assertWakeLockIsReleased() {
sinon.assert.calledOnce(wakeLockStub.unlock);
}
function assertUserIsNotified() {
sinon.assert.calledOnce(Notify.ringtone);
sinon.assert.calledOnce(Notify.vibrate);
}
function assertUserIsNotNotified() {
sinon.assert.notCalled(Notify.ringtone);
sinon.assert.notCalled(Notify.vibrate);
}
setup(function() {
appStub = sinon.stub({ launch() {} });
wakeLockStub = sinon.stub({
unlock: () => {}
});
notificationStub = sinon.stub({
addEventListener() {},
close() {}
});
this.sinon.stub(navigator.mozApps, 'getSelf').returns(
Promise.resolve(appStub)
);
this.sinon.stub(navigator, 'requestWakeLock').returns(wakeLockStub);
this.sinon.spy(Notify, 'ringtone');
this.sinon.spy(Notify, 'vibrate');
this.sinon.stub(SilentSms, 'checkSilentModeFor').returns(
Promise.resolve(false)
);
this.sinon.stub(Navigation, 'isCurrentPanel').returns(false);
this.sinon.stub(Navigation, 'toPanel').returns(Promise.resolve());
this.sinon.stub(NotificationHelper, 'send').returns(notificationStub);
this.sinon.stub(NotificationHelper, 'getIconURI').returns(null);
NotificationHelper.getIconURI.withArgs(appStub).returns('iconURI');
this.sinon.stub(Contacts, 'findByAddress').returns(
Promise.resolve([])
);
});
suite('received message from silenced number', function() {
setup(function() {
SilentSms.checkSilentModeFor.returns(Promise.resolve(true));
});
test('does not notify user in any way', function(done) {
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
MockMessages.sms()
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotNotified();
assertWakeLockIsReleased();
sinon.assert.notCalled(NotificationHelper.send);
}).then(done, done);
});
});
suite('received class-0 message', function() {
setup(function() {
this.sinon.stub(MessageManager, 'deleteMessages').returns(
Promise.resolve()
);
});
test('with non-empty content', function(done) {
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
MockMessages.sms({ messageClass: 'class-0' })
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
sinon.assert.calledOnce(appStub.launch);
assertUserIsNotified();
sinon.assert.calledWith(
Utils.alert, { raw: 'body' }, { raw: 'sender' }
);
sinon.assert.notCalled(NotificationHelper.send);
assertWakeLockIsReleased();
}).then(done, done);
});
test('with empty content', function(done) {
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
MockMessages.sms({ body: null, messageClass: 'class-0' })
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
sinon.assert.calledOnce(appStub.launch);
assertUserIsNotified();
sinon.assert.calledWith(
Utils.alert, { raw: '' }, { raw: 'sender' }
);
sinon.assert.notCalled(NotificationHelper.send);
assertWakeLockIsReleased();
}).then(done, done);
});
});
suite('received class-x message', function() {
var smsMessage, mmsMessage;
setup(function() {
smsMessage = MockMessages.sms();
mmsMessage = MockMessages.mms();
});
test('when app is visible and same conversation is opened',
function(done) {
Navigation.isCurrentPanel.withArgs(
'thread', { id: smsMessage.threadId }
).returns(true);
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
smsMessage
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotified();
sinon.assert.notCalled(NotificationHelper.send);
assertWakeLockIsReleased();
}).then(done, done);
});
test('when app is hidden and same conversation is opened',
function(done) {
isDocumentHidden = true;
Navigation.isCurrentPanel.withArgs(
'thread', { id: smsMessage.threadId }
).returns(true);
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
smsMessage
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
assertWakeLockIsReleased();
// Notification should not be closed until document is visible;
sinon.assert.notCalled(notificationStub.close);
isDocumentHidden = false;
document.addEventListener.withArgs('visibilitychange').yield();
}).then(() => {
sinon.assert.calledOnce(notificationStub.close);
}).then(done, done);
});
suite('received SMS', function() {
test('Use sender as notification title if there is no contact',
function(done) {
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
smsMessage
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
assertWakeLockIsReleased();
}).then(done, done);
});
test('Use sender as notification title if contact does not have name',
function(done) {
var contactWithoutName = new MockContact();
contactWithoutName.name = [];
Contacts.findByAddress.withArgs(smsMessage.sender).returns(
Promise.resolve([contactWithoutName])
);
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('Use contact name as notification title if there is a contact',
function(done) {
Contacts.findByAddress.withArgs(smsMessage.sender).returns(
Promise.resolve(MockContact.list())
);
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: 'Pepito O\'Hare' },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('Use empty string as notification body if message lacks one',
function(done) {
smsMessage.body = null;
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: '' },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
});
suite('received MMS', function() {
test('if message pending on server', function(done) {
mmsMessage.deliveryInfo[0].deliveryStatus = 'pending';
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
mmsMessage
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotNotified();
sinon.assert.notCalled(NotificationHelper.send);
assertWakeLockIsReleased();
}).then(done, done);
});
test('if message should be manually downloaded', function(done) {
mmsMessage.deliveryInfo[0].deliveryStatus = 'manual';
var handlingPromise = SystemMessageHandler.onSmsReceivedSystemMessage(
mmsMessage
);
assertWakeLockIsRequested();
handlingPromise.then(() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: mmsMessage.sender },
{
icon: 'iconURI',
bodyL10n: 'notDownloaded-title',
tag: 'threadId:' + mmsMessage.threadId,
data: { id: mmsMessage.id, threadId: mmsMessage.threadId },
closeOnClick: false
}
);
assertWakeLockIsReleased();
}).then(done, done);
});
test('if message has subject', function(done) {
mmsMessage.subject = 'message-subject';
SystemMessageHandler.onSmsReceivedSystemMessage(mmsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: mmsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: 'message-subject' },
tag: 'threadId:' + mmsMessage.threadId,
data: { id: mmsMessage.id, threadId: mmsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('if message does not have subject, but has text slide',
function(done) {
this.sinon.stub(SMIL, 'parse').withArgs(mmsMessage).returns(
Promise.resolve([{ text: 'some text' }])
);
SystemMessageHandler.onSmsReceivedSystemMessage(mmsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: mmsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: 'some text' },
tag: 'threadId:' + mmsMessage.threadId,
data: { id: mmsMessage.id, threadId: mmsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('if message does not have subject, nor text slide',
function(done) {
SystemMessageHandler.onSmsReceivedSystemMessage(mmsMessage).then(
() => {
assertUserIsNotNotified();
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: mmsMessage.sender },
{
icon: 'iconURI',
bodyL10n: 'mms-message',
tag: 'threadId:' + mmsMessage.threadId,
data: { id: mmsMessage.id, threadId: mmsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
});
suite('handling notification click', function() {
var notificationClickHandler;
setup(function(done) {
this.sinon.stub(MessageManager, 'getMessage').withArgs(
smsMessage.id
).returns(Promise.resolve(smsMessage));
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
// We don't want to use "yield" here because we'd like to have
// promise-based result of yielded function.
notificationClickHandler = notificationStub.addEventListener.
withArgs('click').lastCall.args[1];
}
).then(done, done);
});
test('if appropriate conversation is already opened', function(done) {
Navigation.isCurrentPanel.withArgs(
'thread', { id: smsMessage.threadId }
).returns(true);
notificationClickHandler().then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
test('if appropriate conversation is not opened', function(done) {
notificationClickHandler().then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.calledWith(
Navigation.toPanel, 'thread', { id: smsMessage.threadId }
);
}).then(done, done);
});
test('if message has been deleted', function(done) {
MessageManager.getMessage.withArgs(smsMessage.id).returns(
Promise.reject('deleted')
);
notificationClickHandler().then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.calledWith(Utils.alert, 'deleted-sms');
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
});
suite('dual SIM behavior', function() {
setup(function() {
smsMessage.iccId = '200';
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
this.sinon.stub(Utils, 'getSimNameByIccId').withArgs('200').returns(
Promise.resolve('SIM 2')
);
});
test('message has iccId, but only one SIM is presented',
function(done) {
Settings.hasSeveralSim.returns(false);
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('message iccId is unknown and several SIMs are presented',
function(done) {
smsMessage.iccId = null;
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
sinon.assert.calledWith(
NotificationHelper.send,
{ raw: smsMessage.sender },
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('message has iccId and several SIMs are presented',
function(done) {
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
sinon.assert.calledWith(
NotificationHelper.send,
{
id: 'dsds-notification-title-with-sim',
args: { sim: 'SIM 2', sender: smsMessage.sender }
},
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
test('message from contact with iccId and several SIMs are presented',
function(done) {
Contacts.findByAddress.withArgs(smsMessage.sender).returns(
Promise.resolve(MockContact.list())
);
SystemMessageHandler.onSmsReceivedSystemMessage(smsMessage).then(
() => {
sinon.assert.calledWith(
NotificationHelper.send,
{
id: 'dsds-notification-title-with-sim',
args: { sim: 'SIM 2', sender: 'Pepito O\'Hare' }
},
{
icon: 'iconURI',
bodyL10n: { raw: smsMessage.body },
tag: 'threadId:' + smsMessage.threadId,
data: { id: smsMessage.id, threadId: smsMessage.threadId },
closeOnClick: false
}
);
}
).then(done, done);
});
});
});
});
suite('notification system message', function() {
var appStub, smsMessage;
setup(function() {
smsMessage = MockMessages.sms();
appStub = sinon.stub({ launch() {} });
this.sinon.stub(navigator.mozApps, 'getSelf').returns(
Promise.resolve(appStub)
);
this.sinon.stub(Navigation, 'init');
this.sinon.stub(Navigation, 'isCurrentPanel').returns(false);
this.sinon.stub(Navigation, 'toPanel').returns(Promise.resolve());
this.sinon.stub(Navigation, 'hasPendingInit').returns(false);
this.sinon.stub(MessageManager, 'getMessage').withArgs(
smsMessage.id
).returns(Promise.resolve(smsMessage));
});
test('nothing is done when notification is removed', function(done) {
SystemMessageHandler.onNotificationSystemMessage({
// When notification is removed "clicked" property is "false".
clicked: false,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(() => {
sinon.assert.notCalled(appStub.launch);
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
test('close the app when notification is removed with pending navigation',
function(done) {
Navigation.hasPendingInit.returns(true);
this.sinon.stub(window, 'close');
SystemMessageHandler.onNotificationSystemMessage({
// When notification is removed "clicked" property is "false".
clicked: false,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(
() => new Error('Should not enter resolved case'),
(error) => {
sinon.assert.called(window.close);
assert.equal(error.message, 'Notification has been dismissed.');
}
).then(done, done);
});
test('if appropriate conversation is already opened', function(done) {
Navigation.isCurrentPanel.withArgs(
'thread', { id: smsMessage.threadId }
).returns(true);
SystemMessageHandler.onNotificationSystemMessage({
clicked: true,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
test('if appropriate conversation is not opened', function(done) {
SystemMessageHandler.onNotificationSystemMessage({
clicked: true,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.calledWith(
Navigation.toPanel, 'thread', { id: smsMessage.threadId }
);
}).then(done, done);
});
test('if message has been deleted', function(done) {
MessageManager.getMessage.withArgs(smsMessage.id).returns(
Promise.reject('deleted')
);
SystemMessageHandler.onNotificationSystemMessage({
clicked: true,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.calledWith(Utils.alert, 'deleted-sms');
sinon.assert.notCalled(Navigation.toPanel);
}).then(done, done);
});
test('if message has been deleted with pending navigation', function(done) {
MessageManager.getMessage.withArgs(smsMessage.id).returns(
Promise.reject('deleted')
);
Navigation.hasPendingInit.returns(true);
SystemMessageHandler.onNotificationSystemMessage({
clicked: true,
data: { id: smsMessage.id, threadId: smsMessage.threadId }
}).then(() => {
sinon.assert.called(appStub.launch);
sinon.assert.calledWith(Utils.alert, 'deleted-sms');
sinon.assert.notCalled(Navigation.toPanel);
sinon.assert.called(Navigation.init);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,41 @@
/* global TaskRunner, Promise */
'use strict';
require('/views/shared/js/task_runner.js');
suite('SMS App Unit-Test', function() {
test('runs operations serially', function(done) {
var taskQueue = new TaskRunner(),
resolvers = [],
promises = [],
results = [];
promises.push(new Promise(function(resolve, reject) {
resolvers.push(resolve);
}));
promises.push(new Promise(function(resolve, reject) {
resolvers.push(resolve);
}));
promises.push(new Promise(function(resolve, reject) {
resolvers.push(resolve);
}));
promises.forEach(function(promise, i) {
taskQueue.push(function() {
results.push(i);
return promise;
});
});
resolvers[1]();
resolvers[0]();
resolvers[2]();
taskQueue.push(function() {
Promise.resolve().then(function(resolve, reject) {
assert.deepEqual(results, [0, 1, 2]);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,94 @@
/*global getMockupedDate */
/*exported MockThreadList */
'use strict';
function MockThreadList(options) {
// These Threads are not sorted in order to check that rendering
// is working as expected, adding each thread to the right container.
var threadsMockup = [
{
id: 1,
participants: ['1977'],
lastMessageType: 'sms',
body: 'Alo, how are you today, my friend? :)',
timestamp: +getMockupedDate(0),
unreadCount: 0
},
{
id: 2,
participants: ['436797'],
lastMessageType: 'sms',
body: 'Sending :)',
timestamp: +getMockupedDate(2),
unreadCount: 0
},
{
id: 3,
participants: ['197746797'],
lastMessageType: 'sms',
body: 'Recibido!',
timestamp: +getMockupedDate(1),
unreadCount: 0
},
{
id: 4,
participants: ['1977436979'],
lastMessageType: 'mms',
body: 'Nothing :)',
timestamp: +getMockupedDate(2),
unreadCount: 2
},
{
id: 5,
participants: ['999', '888', '777'],
lastMessageType: 'sms',
body: '...',
timestamp: +getMockupedDate(3),
unreadCount: 0
},
{
id: 6,
participants: ['1977'],
lastMessageType: 'sms',
body: 'Alo, how are you today, my friend? :)',
timestamp: +getMockupedDate(0),
unreadCount: 0
},
{
id: 7,
participants: ['436797'],
lastMessageType: 'sms',
body: 'Sending :)',
timestamp: +getMockupedDate(2),
unreadCount: 0
},
{
id: 8,
participants: ['197746797'],
lastMessageType: 'sms',
body: 'Recibido!',
timestamp: +getMockupedDate(1),
unreadCount: 0
},
{
id: 9,
participants: ['1977436979'],
lastMessageType: 'mms',
body: 'Nothing :)',
timestamp: +getMockupedDate(2),
unreadCount: 2
},
{
id: 10,
participants: ['999', '888', '777'],
lastMessageType: 'sms',
body: '...',
timestamp: +getMockupedDate(3),
unreadCount: 0
}
];
return options && options.fullList ?
threadsMockup : threadsMockup.slice(0, 5);
}

View file

@ -0,0 +1,434 @@
/*global TimeHeaders, MockL10n */
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/js/utils.js');
require('/views/shared/js/time_headers.js');
suite('TimeHeaders > ', function() {
var realMozL10n;
suiteSetup(function() {
realMozL10n = document.l10n;
document.l10n = MockL10n;
});
suiteTeardown(function() {
document.l10n = realMozL10n;
realMozL10n = null;
});
suite('TimeHeaders.updateAll', function() {
var existingHeaders, existingTimes;
setup(function() {
this.sinon.useFakeTimers(Date.parse('2013-01-01'));
this.sinon.spy(TimeHeaders, 'update');
var additionalDataset = [
'data-date-only="true"',
'data-hour-only="true"',
''
];
var mockThreadListMarkup = '';
additionalDataset.forEach(function(dataset, i) {
var mockDate = Date.now();
dataset += ' data-time-update="repeat"' +
' data-time="' + mockDate + '"';
mockThreadListMarkup +=
'<header ' + dataset + '>header ' + i + '</header>' +
'<ul>' +
'<li id="thread-1" data-time="' + mockDate + '">' +
'<p><time data-time-update="true" ' +
'data-time-only="true" data-time="' + mockDate +
'"></time></p>' +
'</li>' +
'<li id="thread-2" data-time="' + mockDate + '">' +
'<p><time data-time-update="true" ' +
'data-time-only="true" data-time="' + mockDate +
'"></time></p>' +
'</li>' +
'</ul>';
});
document.body.innerHTML = mockThreadListMarkup;
TimeHeaders.updateAll();
existingHeaders = [];
existingTimes = [];
var headers = document.querySelectorAll('header');
for (var i = 0; i < headers.length; i++) {
existingHeaders[i] = document.l10n.getAttributes(headers[i]);
}
var timeElements = document.querySelectorAll('time');
for (var j = 0; j < timeElements.length; j++) {
existingTimes[j] = timeElements[j].textContent;
}
});
teardown(function() {
document.body.innerHTML = '';
});
suite('timezone was not changed', function() {
test('calling at same time should not update headers ' +
'and time elements', function() {
var i = 0, l = 0;
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll('header[data-time-update]');
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.deepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
for (i = 0, l = timeElements.length; i < l; i++) {
assert.equal(timeElements[i].textContent, existingTimes[i]);
}
});
test('calling after one hour should not update time headers ' +
'and time elements', function() {
var i = 0, l = 0;
this.sinon.clock.tick(60 * 60 * 1000);
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll('header[data-time-update]');
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.deepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
for (i = 0, l = timeElements.length; i < l; i++) {
assert.equal(timeElements[i].textContent, existingTimes[i]);
}
});
test('calling after one day should update all date headers', function() {
var i = 0, l = 0;
this.sinon.clock.tick(24 * 60 * 60 * 1000);
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll('header[data-time-update]');
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.notDeepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
for (i = 0, l = timeElements.length; i < l; i++) {
assert.equal(timeElements[i].textContent, existingTimes[i]);
}
//should call update total 12 times
//setup:3(headers)+6(times), test:3(headers)
assert.equal(TimeHeaders.update.callCount, 12);
});
});
suite('timezone was changed', function() {
//When timezones was changed, 'visibilitychange'.
//Therefore 'TimeHeaders.updateAll()' is called by argument 'false'.
test('calling at same time should not update headers ' +
'and time elements', function() {
var i = 0, l = 0;
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll();
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.deepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
for (i = 0, l = timeElements.length; i < l; i++) {
assert.equal(timeElements[i].textContent, existingTimes[i]);
}
});
test('calling after one hour should update all time elements',
function() {
var i = 0, l = 0;
//since we can't change timezones easily,
//we simulate this by changing the stored 'time' dataset
this.sinon.clock.tick(60 * 60 * 1000);
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll();
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.deepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
for (i = 0, l = timeElements.length; i < l; i++) {
assert.notEqual(timeElements[i].textContent, existingTimes[i]);
}
});
test('calling after one day should update all date headers ' +
'and time elements', function() {
var i = 0, l = 0;
//since we can't change timezones easily,
//we simulate this by changing the stored 'time' dataset
this.sinon.clock.tick(24 * 60 * 60 * 1000);
var timeElements = document.querySelectorAll('time');
for (i = 0, l = timeElements.length; i < l; i++) {
timeElements[i].dataset.time = Date.now();
}
TimeHeaders.updateAll();
var headers = document.querySelectorAll('header');
for (i = 0, l = headers.length; i < l; i++) {
assert.notDeepEqual(
document.l10n.getAttributes(headers[i]),
existingHeaders[i]
);
}
//should call update total 18 times
//setup:3(headers)+6(times), test:3(headers)+6(times)
assert.equal(TimeHeaders.update.callCount, 18);
});
});
});
suite('TimeHeaders.update', function() {
var subject, time;
setup(function() {
subject = document.createElement('header');
subject.dataset.timeUpdate = 'repeat';
time = Date.parse('2013-01-01');
subject.dataset.time = time;
});
test('date and time header', function() {
TimeHeaders.update(subject);
var formatter = new Intl.DateTimeFormat(navigator.languages, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric'
});
var content = subject.textContent;
assert.equal(content, formatter.format(time));
});
test('date header', function() {
subject.dataset.dateOnly = 'true';
TimeHeaders.update(subject);
var formatter = new Intl.DateTimeFormat(navigator.languages, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
});
var content = subject.textContent;
assert.equal(content, formatter.format(time),
'subject.textContent should not contain formattedTime');
});
test('time header', function() {
subject.dataset.timeOnly = 'true';
TimeHeaders.update(subject);
var formatter = new Intl.DateTimeFormat(navigator.languages, {
hour12: navigator.mozHour12,
hour: 'numeric',
minute: 'numeric'
});
var content = subject.textContent;
assert.equal(
content, formatter.format(time),
'subject.textContent should not contain formattedDate'
);
});
});
suite('TimeHeaders.startScheduler', function() {
setup(function() {
this.callTimes = [];
this.sinon.useFakeTimers();
this.updateStub = this.sinon.stub(TimeHeaders, 'updateAll',
function() {
this.callTimes.push(Date.now());
}.bind(this));
});
test('timeout on minute boundary', function() {
// "Fri Jul 12 2013 16:01:54 GMT-0400 (EDT)"
var start = 1373659314572;
this.sinon.clock.tick(start);
TimeHeaders.startScheduler();
this.sinon.clock.tick(100 * 1000);
// Fri Jul 12 2013 16:02:00 GMT-0400 (EDT)
assert.equal(this.callTimes[0], 1373659320000);
// Fri Jul 12 2013 16:03:00 GMT-0400 (EDT)
assert.equal(this.callTimes[1], 1373659380000);
});
test('multiple calls converge', function() {
this.updateStub.reset();
for (var i = 0; i < 100; i++) {
TimeHeaders.startScheduler();
}
this.sinon.clock.tick(60 * 1000);
assert.equal(this.updateStub.callCount, 1);
});
});
suite('TimeHeaders.stopScheduler', function() {
setup(function() {
this.sinon.spy(window, 'clearTimeout');
TimeHeaders.stopScheduler();
});
test('stopScheduler called', function() {
assert.ok(window.clearTimeout.called);
});
});
suite('TimeHeaders.init', function() {
var isDocumentHidden;
suiteSetup(function() {
Object.defineProperty(document, 'hidden', {
configurable: true,
get: function() {
return isDocumentHidden;
}
});
});
suiteTeardown(function() {
delete document.hidden;
});
setup(function() {
this.sinon.spy(TimeHeaders, 'startScheduler');
this.sinon.spy(TimeHeaders, 'stopScheduler');
this.sinon.spy(TimeHeaders, 'updateAll');
});
suite('init itself', function() {
setup(function() {
isDocumentHidden = false;
this.sinon.spy(TimeHeaders, 'init');
TimeHeaders.init();
});
test('startScheduler is called', function() {
// Because in startScheduler, we will still call stopScheduler,
// that's why I use this check to make sure we call startScheduler
// correctly.
assert.ok(TimeHeaders.startScheduler.calledBefore(
TimeHeaders.stopScheduler));
assert.ok(TimeHeaders.updateAll.called);
});
});
suite('when document is hidden', function() {
setup(function() {
isDocumentHidden = false;
TimeHeaders.init();
TimeHeaders.startScheduler.reset();
TimeHeaders.stopScheduler.reset();
TimeHeaders.updateAll.reset();
isDocumentHidden = true;
document.dispatchEvent(new CustomEvent('visibilitychange'));
});
test('TimeHeaders.stopScheduler is called', function() {
assert.ok(TimeHeaders.stopScheduler.called);
assert.ok(!TimeHeaders.startScheduler.called);
assert.ok(!TimeHeaders.updateAll.called);
});
});
suite('when document is not hidden', function() {
setup(function() {
isDocumentHidden = true;
TimeHeaders.init();
TimeHeaders.startScheduler.reset();
TimeHeaders.stopScheduler.reset();
isDocumentHidden = false;
document.dispatchEvent(new CustomEvent('visibilitychange'));
});
test('TimeHeaders.startScheduler is called', function() {
assert.ok(TimeHeaders.updateAll.called);
assert.ok(TimeHeaders.startScheduler.calledBefore(
TimeHeaders.stopScheduler));
});
});
});
});

View file

@ -0,0 +1,11 @@
/*exported getMockupedDate */
'use strict';
function getMockupedDate(diffDays) {
var year = 2013;
var month = 1;
var day = 13;
var hour = 1, minute = 1;
return new Date(year, month, day - diffDays, hour, minute, 0);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
/*global WBMP,
AssetsHelper,
Promise
*/
'use strict';
require('/views/shared/js/wbmp.js');
suite('wbmp.js >', function() {
var wbmpBlob, wbmpAsPngBlob;
suiteSetup(function(done) {
var mediaFolder = '/views/shared/test/unit/media';
var blobPromises = [
AssetsHelper.loadFileBlob(`${mediaFolder}/grid.png`).then(
(blob) => wbmpAsPngBlob = blob
),
AssetsHelper.loadFileBlob(`${mediaFolder}/grid.wbmp`).then(
(blob) => wbmpBlob = blob
)
];
Promise.all(blobPromises).then(() => done(), done);
});
function writeBlobToCanvas(blob, callback) {
var canvas = document.createElement('canvas');
var image = new Image();
var blobUrl = window.URL.createObjectURL(blob);
image.src = blobUrl;
image.onload = function() {
window.URL.revokeObjectURL(this.src);
canvas.height = image.naturalHeight;
canvas.width = image.naturalWidth;
var context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
callback(canvas);
};
}
function assertPngEquals(pngBlob1, pngBlob2, done) {
var canvases = [];
function canvasReady(canvas) {
canvases.push(canvas);
if (canvases.length === 2) {
done(compareCanvases);
}
}
function compareCanvases() {
var c1 = canvases[0];
var c2 = canvases[1];
assert.equal(c1.width, c2.width);
assert.equal(c1.height, c2.height);
var id1 = c1.getContext('2d').getImageData(0, 0, c1.width, c1.height);
var id2 = c2.getContext('2d').getImageData(0, 0, c2.width, c2.height);
var data1 = id1.data;
var data2 = id2.data;
assert.equal(data1.length, data2.length);
for (var i = 0, l = data1.length; i < l; i++) {
assert.equal(data1[i], data2[i]);
}
}
writeBlobToCanvas(pngBlob1, canvasReady);
writeBlobToCanvas(pngBlob2, canvasReady);
}
test('Decode correctly', function(done) {
var reader = new FileReader();
reader.readAsArrayBuffer(wbmpBlob);
reader.onload = function() {
var arrayBuffer = this.result;
WBMP.decode(arrayBuffer, function(blob) {
assertPngEquals(blob, wbmpAsPngBlob, done);
});
};
});
});