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

1
gaia/apps/sms/.gitignore vendored Executable file
View file

@ -0,0 +1 @@
bower_components

64
gaia/apps/sms/build/build.js Executable file
View file

@ -0,0 +1,64 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
function removeDesktopOnlyFolder(appStageDir) {
var desktopOnlyDir = utils.getFile(appStageDir, 'desktop-mock');
if (desktopOnlyDir.exists()) {
desktopOnlyDir.remove(true);
}
}
function removeDesktopOnlyScripts(appStageDir) {
// Picking every file specifically since we may have "index.html" files from
// a bunch of places we don't want process to (shared, node modules, bower..).
[
'index.html',
'views/inbox/index.html',
'views/conversation/index.html',
'views/shared/shim_host.html'
].forEach((indexFilePath) => {
var indexFile = utils.getFile(appStageDir, indexFilePath);
var indexDocument = utils.getDocument(utils.getFileContent(indexFile));
var mockScripts = indexDocument.querySelectorAll(
'script[src*="desktop-mock"]'
);
for (var mockScript of mockScripts) {
mockScript.parentNode.removeChild(mockScript);
}
utils.writeContent(indexFile, utils.serializeDocument(indexDocument));
});
}
function prepareBridgeLibs(options) {
var bridgeLibsPath = utils.joinPath(options.STAGE_APP_DIR, 'lib/bridge');
['service.min.js', 'client.min.js'].forEach(function(optimizedLib) {
var optimizedLibPath = utils.joinPath(bridgeLibsPath, optimizedLib);
// If we want to have only optimized libs, let's replace non-optimized
// version with optimized one.
if (options.GAIA_OPTIMIZE === '1') {
utils.copyFileTo(
optimizedLibPath, bridgeLibsPath, optimizedLib.replace('.min', '')
);
}
utils.deleteFile(optimizedLibPath);
});
}
exports.execute = function(options) {
utils.copyToStage(options);
prepareBridgeLibs(options);
removeDesktopOnlyFolder(options.STAGE_APP_DIR);
removeDesktopOnlyScripts(options.STAGE_APP_DIR);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,82 @@
(function(exports) {
'use strict';
// Fake drafts stored in local store
const timestamp = Date.now(),
store = new Map();
const draftIndex = [
[11, [{
recipients: ['555', '666'],
subject: '',
content: ['This is a draft message'],
timestamp: timestamp - (3600000 * 24),
threadId: 11,
type: 'sms'
}]],
[null, [{
recipients: [],
subject: '',
content: ['This is a draft SMS, with no recipient'],
timestamp: timestamp,
threadId: null,
type: 'sms'
}, {
recipients: ['555-666-1234'],
subject: '',
content: ['This is a draft SMS, with a recipient, but no thread'],
timestamp: timestamp - 3600000,
threadId: null,
type: 'sms'
}]],
[8, [{
recipients: ['123456'],
subject: '',
content: [
'This is a draft MMS...',
{
blob: {
type: 'audio/ogg',
size: 12345
},
name: 'audio.oga'
},
'...with a recipient and a thread'
],
timestamp: timestamp - (3600000 * 2),
threadId: 8,
type: 'mms'
}]]
];
store.set('draft index', draftIndex);
function callCallback(value, callback) {
if (typeof callback === 'function') {
setTimeout(callback.bind(null, value), 0);
}
}
Object.defineProperty(exports, 'asyncStorage', {
value: {
getItem: function asm_getItem(key, callback) {
callCallback(store.get(key) || null, callback);
},
setItem: function asm_setItem(key, value, callback) {
callCallback(store.set(key, value), callback);
},
removeItem: function asm_removeItem(key, callback) {
callCallback(store.delete(key), callback);
},
clear: function asm_clear(callback) {
callCallback(store.clear(), callback);
},
length: function(callback) {
callCallback(store.size, callback);
},
key: function() {
throw new Error('Not Implemented');
}
}
});
})(window);

View file

@ -0,0 +1,464 @@
/*global Utils, Promise */
'use strict';
/* ***********************************************************
Code below is for desktop testing!
Adapted from:
https://mxr.mozilla.org/mozilla-b2g18/source/dom/contacts/fallback/ContactDB.jsm
https://mxr.mozilla.org/mozilla-b2g18/source/dom/contacts/ContactManager.js
apps/sms/js/contacts.js
*********************************************************** */
(function(window) {
function getAsset(filename) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', filename, true);
req.responseType = 'blob';
req.onload = function() {
resolve(req.response);
};
req.send();
});
}
function stringOrBust(aObj) {
if (typeof aObj !== 'string') {
return undefined;
} else {
return aObj;
}
}
function sanitizeStringArray(aArray) {
if (!Array.isArray(aArray)) {
aArray = [aArray];
}
return aArray.map(stringOrBust).filter(function(el) {
return el !== undefined;
});
}
function _isVanillaObj(aObj) {
return Object.prototype.toString.call(aObj) == '[object Object]';
}
function Normalize(opts) {
opts.fields.forEach(function(field) {
var value = opts.record[field.name];
this[field.name] = typeof value !== 'undefined' ?
field.filter(value) : null;
}, this);
}
function ContactField(field) {
Normalize.call(this, {
fields: [
{ name: 'type', filter: sanitize },
{ name: 'value', filter: stringOrBust }
],
record: field
});
}
function ContactTelField(tel) {
Normalize.call(this, {
fields: [
{ name: 'type', filter: sanitize },
{ name: 'value', filter: stringOrBust },
{ name: 'carrier', filter: stringOrBust }
],
record: tel
});
}
function ContactAddress(address) {
Normalize.call(this, {
fields: [
{ name: 'type', filter: sanitize },
{ name: 'streetAddress', filter: stringOrBust },
{ name: 'locality', filter: stringOrBust },
{ name: 'region', filter: stringOrBust },
{ name: 'postalCode', filter: stringOrBust },
{ name: 'countryName', filter: stringOrBust }
],
record: address
});
}
function multiValue(source, ctor) {
var result = [];
source = Array.isArray(source) ? source : [source];
for (var entry of source) {
if (_isVanillaObj(entry)) {
result.push(new ctor(entry));
}
}
return result;
}
function Contact(contact) {
Normalize.call(this, {
fields: [
{ name: 'name', filter: sanitize },
{ name: 'honorificPrefix', filter: sanitize },
{ name: 'honorificSuffix', filter: sanitize },
{ name: 'givenName', filter: sanitize },
{ name: 'additionalName', filter: sanitize },
{ name: 'familyName', filter: sanitize },
{ name: 'nickname', filter: sanitize },
{ name: 'email', filter: sanitize },
{ name: 'photo', filter: sanitize },
{ name: 'url', filter: sanitize },
{ name: 'category', filter: sanitize },
{ name: 'org', filter: sanitize },
{ name: 'jobTitle', filter: sanitize },
{ name: 'note', filter: sanitize },
{ name: 'impp', filter: sanitize }
],
record: contact
});
this.id = ++cid;
this.name = [
this.givenName[0] + ' ' + this.familyName[0]
];
this.email = typeof contact.email !== 'undefined' ?
multiValue(contact.email, ContactField) : null;
this.adr = typeof contact.adr !== 'undefined' ?
multiValue(contact.adr, ContactAddress) : null;
this.tel = typeof contact.tel !== 'undefined' ?
multiValue(contact.tel, ContactTelField) : null;
this.url = typeof contact.url !== 'undefined' ?
multiValue(contact.url, ContactField) : null;
this.bday = typeof contact.bday !== 'undefined' ?
new Date(contact.bday) : null;
this.anniversary = contact.anniversary ?
new Date(contact.anniversary) : null;
}
var ContactsDB = [];
var sanitize = sanitizeStringArray;
var cid = 0;
var methods = {
contains: 'startsWith',
match: 'includes',
equals: 'includes'
};
function isMatch(contact, filter) {
/**
* Validation Strategy
*
* 1. Let _found_ be a register of confirmed terms
* 2. Let _contact_ be a contact record.
* 3. For each _term_ [...input list], with the label _outer_
* - For each _field_ of [givenName, familyName]
* - For each _value_ in _contact_[ _field_ ]
* - Let _found_[ _term_ ] be the result of calling
* _value_.startsWith( _term_ )
* - If _found_[ _term_ ] is **true**, continue to
* loop labelled _outer_
* 4. If every value of _key_ in _found_ is **true** return **true**,
* else return **false**
*/
var found = {};
var method = methods[filter.filterOp];
var term = filter.filterValue.toLowerCase();
// The outer loop is specifically labelled to allow the
// nested condition a way out of the second and third loop.
outer:
for (var field of filter.filterBy) {
if (!contact[field]) {
continue;
}
for (var value of contact[field]) {
if (typeof value !== 'string') {
value = value.value;
}
if ((found[term] = value.toLowerCase()[method](term))) {
return true;
}
if (field === 'tel') {
if (Utils.probablyMatches(value, term)) {
return true;
}
}
}
}
return false;
}
// // Do not use the native API.
navigator.mozContacts = {
addEventListener: function(type, handler, capture) {
return this;
},
find: function(filter) {
// attribute DOMString filterValue; // e.g. 'Tom'
// attribute DOMString filterOp; // e.g. 'contains'
// attribute DOMString[] filterBy; // e.g. 'givenName'
// attribute DOMString sortBy; // e.g. 'givenName'
// attribute DOMString sortOrder; // e.g. 'descending'
// attribute unsigned long filterLimit;
// Implement a mock that gives results that appear to
// match the real behaviour of filtered contacts results
var result = ContactsDB.filter(function(contact) {
return isMatch(contact, filter);
});
return readyPromise.then(() => result);
}
};
ContactsDB.push(
new Contact({
familyName: 'Turing',
givenName: 'Alan',
tel: {
value: '101',
type: 'Mobile',
carrier: 'Telco'
}
})
);
/**
// 102 is omitted intentionally.
ContactsDB.push(
new Contact({
familyName: '',
givenName: '',
tel: {
value: '102'
}
})
);
*/
ContactsDB.push(
new Contact({
familyName: 'Shannon',
givenName: 'Claude',
tel: {
value: '103',
type: 'Mobile'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Lovelace',
givenName: 'Ada',
tel: {
value: '104'
},
email: {
value: 'ada@lovelace.com'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Tesla',
givenName: 'Nikola',
email: {
value: 'nikola@tesla.com'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Babbage',
givenName: 'Charles',
tel: {
value: '105'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Backus',
givenName: 'John',
tel: {
value: '106'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'McCarthy',
givenName: 'John',
tel: {
value: '107'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Steele',
givenName: 'Guy',
tel: {
value: '108'
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Kay',
givenName: 'Alan',
tel: {
value: '109',
type: 'Mobile',
carrier: 'Nynex'
}
})
);
var grace = new Contact({
familyName: 'Hopper',
givenName: 'Grace',
tel: {
value: '999'
}
});
var tom = new Contact({
familyName: 'O\'Hare',
givenName: 'Tom',
tel: {
value: '123456',
type: 'Mobile',
carrier: 'Nynex'
}
});
var longname = new Contact({
familyName: 'Taumatawhakatangihangakoauauota',
givenName: 'Mateapokaiwhenuakitanatahu',
tel: [{
value: '+18001114321',
type: ['Mobile']
}]
});
var rtlName = new Contact({
familyName: 'الخيام',
givenName: 'عمر',
tel: [{
value: '+97121111111',
type: ['Mobile'],
carrier: 'اتصالات'
}]
});
var readyPromise = Promise.all([
getAsset('/desktop-mock/assets/photo-man.jpg'),
getAsset('/desktop-mock/assets/photo-woman.jpg'),
getAsset('/desktop-mock/assets/photo-man-bowtie.jpg')
]).then(function gotall(blobs) {
tom.photo = [blobs[0]];
grace.photo = [blobs[1]];
longname.photo = [blobs[2]];
});
ContactsDB.push(grace);
ContactsDB.push(tom);
ContactsDB.push(longname);
ContactsDB.push(rtlName);
ContactsDB.push(
new Contact({
familyName: 'Carrier',
givenName: 'Igotno',
tel: {
value: '436797',
type: 'Mobile',
carrier: null
}
})
);
ContactsDB.push(
new Contact({
familyName: 'Jekyll',
givenName: 'Doctor',
tel: [
{
value: '+12125551234',
type: ['Mobile']
},
{
value: '+15551237890',
type: ['Home']
}
]
})
);
ContactsDB.push(
new Contact({
familyName: 'Non-digit',
givenName: 'Multiple',
tel: [
{
value: '900-BUY-A-CAR',
type: ['Mobile'],
carrier: 'Megaphones'
},
{
value: '800-BUY-A-CAR',
type: ['Home'],
carrier: 'Nynex'
}
]
})
);
ContactsDB.push(
new Contact({
familyName: '',
givenName: 'Bob',
tel: [
{
value: '555-666-1234',
type: ['Mobile'],
carrier: 'ScamCo'
}
]
})
);
// console.log( ContactsDB );
}(window));

View file

@ -0,0 +1,30 @@
'use strict';
(function(window) {
if (navigator.mozMobileConnections) {
console.error('navigator.mozMobileConnections exists!');
return;
}
navigator.mozMobileConnections = {
0: {
iccId: 'SIM 1',
addEventListener: () => {},
removeEventListener: () => {},
data: {
state: 'searching'
}
},
1: {
iccId: 'SIM 2',
addEventListener: () => {},
removeEventListener: () => {},
data: {
state: 'searching'
}
},
length: 2
};
}(window));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
'use strict';
/* ***********************************************************
Workaround to force the mozMobileMessage in shim context use window one.
Need to be removed once all the event listeners are ready for shim.
*********************************************************** */
(function(window) {
if (!parent.navigator.mozMobileMessage) {
console.error('mozMobileMessage for main window context is not ready!');
return;
}
navigator.mozMobileMessage = parent.navigator.mozMobileMessage;
}(window));

View file

@ -0,0 +1,39 @@
(function() {
'use strict';
if (navigator.mozSettings) {
return;
}
const KNOWN_SETTINGS = new Map();
KNOWN_SETTINGS.set('locale.hour12', true);
navigator.mozSettings = {
createLock: function() {
return {
get: function(key) {
var request = {
addEventListener: function(eventName, callback) {
request[eventName] = callback;
}
};
window.setTimeout(function() {
if (typeof request.onsuccess === 'function') {
request.result = {};
request.result[key] = KNOWN_SETTINGS.get(key);
request.onsuccess.call(request);
}
}, 0);
return request;
},
set() {}
};
},
addObserver: function() {}
};
})();

View file

@ -0,0 +1,18 @@
'use strict';
/* ***********************************************************
Workaround to force the mozSettings in shim context use window one.
Need to be removed once all the event listeners are ready for shim.
*********************************************************** */
(function(window) {
if (!parent.navigator.mozSettings) {
console.error('mozSettings for main window context is not ready!');
return;
}
navigator.mozSettings = parent.navigator.mozSettings;
}(window));

View file

@ -0,0 +1,11 @@
(function() {
'use strict';
if (!navigator.mozHasPendingMessage) {
navigator.mozHasPendingMessage = () => false;
}
if (!navigator.mozSetMessageHandler) {
navigator.mozSetMessageHandler = () => {};
}
})();

BIN
gaia/apps/sms/icons/sms_126.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
gaia/apps/sms/icons/sms_142.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
gaia/apps/sms/icons/sms_189.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
gaia/apps/sms/icons/sms_284.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
gaia/apps/sms/icons/sms_84.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

645
gaia/apps/sms/index.html Executable file
View file

@ -0,0 +1,645 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<meta name="theme-group" content="theme-communications">
<meta name="theme-color" content="var(--header-background)">
<title>Messages</title>
<!--App Style root.css-->
<link rel="stylesheet" type="text/css" href="views/shared/style/root.css">
<!-- Stable Building blocks -->
<link rel="stylesheet" type="text/css" href="shared/style/animations.css">
<link rel="stylesheet" type="text/css" href="shared/style/action_menu.css">
<link rel="stylesheet" type="text/css" href="shared/style/option_menu.css">
<link rel="stylesheet" type="text/css" href="shared/style/buttons.css">
<link rel="stylesheet" type="text/css" href="shared/style/lists.css">
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css">
<!-- App styles -->
<link rel="stylesheet" type="text/css" href="views/shared/style/sms.css">
<link rel="stylesheet" type="text/css" href="views/shared/style/panel.css">
<link rel="stylesheet" type="text/css" href="views/shared/style/notification.css">
<link rel="stylesheet" type="text/css" href="views/shared/style/common.css">
<link rel="stylesheet" type="text/css" href="views/inbox/style/inbox.css">
<!-- Lazy load styles
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css">
<link rel="stylesheet" type="text/css" href="shared/style/edit_mode.css">
<link rel="stylesheet" type="text/css" href="shared/style/input_areas.css">
<link rel="stylesheet" type="text/css" href="shared/style/switches.css">
<link rel="stylesheet" type="text/css" href="shared/style/progress_activity.css">
<link rel="stylesheet" type="text/css" href="shared/style/status.css">
<link rel="stylesheet" type="text/css" href="shared/style/tabs.css" >
<link rel="stylesheet" type="text/css" href="views/shared/style/edit-mode.css">
<link rel="stylesheet" type="text/css" href="views/shared/style/composer.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/message.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/report_view.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/attachment.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/conversation.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/edit-mode.css">
<link rel="stylesheet" type="text/css" href="views/new_message/style/new_message.css">
<link rel="stylesheet" type="text/css" href="views/new_message/style/recipients.css">
<link rel="stylesheet" type="text/css" href="views/inbox/style/edit-mode.css">
-->
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/sms.{locale}.properties">
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
<link rel="localization" href="/shared/locales/intl/unitformat.{locale}.properties">
<!-- ICC manager shim for desktop and B2GDroid -->
<script defer src="mocks/navigator_moz_icc_manager.js"></script>
<!-- Firefox Desktop shims -->
<script defer src="desktop-mock/navigator_moz_system_messages.js"></script>
<script defer src="desktop-mock/navigator_moz_settings.js"></script>
<script defer src="desktop-mock/navigator_moz_mobilemessage.js"></script>
<script defer src="desktop-mock/navigator_moz_contacts.js"></script>
<script defer src="desktop-mock/async_storage.js"></script>
<!-- Web Components -->
<script defer src="/shared/elements/config.js"></script>
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
<script async src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
<link rel="localization" href="/shared/elements/gaia_sim_picker/locales/gaia_sim_picker.{locale}.properties">
<!--
<script defer src="shared/js/component_utils.js"></script>
<script defer src="shared/elements/gaia_menu/script.js"></script>
<script defer src="shared/elements/gaia_sim_picker/script.js"></script>
<link rel="stylesheet" type="text/css" href="shared/elements/gaia_menu/style.css">
<link rel="stylesheet" type="text/css" href="shared/elements/gaia_sim_picker/style.css">
-->
<!-- Shared code -->
<script defer src="shared/js/intl/l20n.js"></script>
<!-- Should be removed when bug 1172732 lands -->
<script defer src="shared/js/date_time_helper.js"></script>
<script defer src="shared/js/template.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="shared/js/lazy_loader.js"></script>
<script defer src="shared/js/sticky_header.js"></script>
<script defer src="shared/js/contact_photo_helper.js"></script>
<script defer src="shared/js/fb/fb_request.js"></script>
<script defer src="shared/js/fb/fb_data_reader.js"></script>
<script defer src="shared/js/fb/fb_reader_utils.js"></script>
<script defer src="shared/js/event_dispatcher.js"></script>
<script defer src="views/shared/js/utils.js"></script>
<script defer src="views/shared/js/navigation.js"></script>
<script defer src="views/shared/js/inter_instance_event_dispatcher.js"></script>
<script defer src="views/shared/js/time_headers.js"></script>
<script defer src="views/shared/js/contacts.js"></script>
<script defer src="services/js/drafts.js"></script>
<script defer src="services/js/threads.js"></script>
<script defer src="views/inbox/js/inbox.js"></script>
<script defer src="views/shared/js/settings.js"></script>
<script defer src="services/js/message_manager.js"></script>
<script defer src="views/shared/js/app.js"></script>
<script defer src="views/shared/js/startup.js"></script>
<!-- Lazy load scripts
<script defer src="shared/js/notification_helper.js"></script>
<script defer src="shared/js/gesture_detector.js"></script>
<script defer src="shared/js/settings_url.js"></script>
<script defer src="shared/js/mime_mapper.js"></script>
<script defer src="shared/js/mobile_operator.js"></script>
<script defer src="shared/js/settings_listener.js"></script>
<script defer src="shared/js/multi_sim_action_button.js"></script>
<script defer src="shared/js/option_menu.js"></script>
<script defer src="shared/js/image_utils.js"></script>
<script defer src="views/shared/js/errors.js"></script>
<script defer src="views/shared/js/shared_components.js"></script>
<script defer src="views/shared/js/dialog.js"></script>
<script defer src="views/shared/js/error_dialog.js"></script>
<script defer src="views/shared/js/task_runner.js"></script>
<script defer src="views/shared/js/silent_sms.js"></script>
<script defer src="views/conversation/js/recipients.js"></script>
<script defer src="views/conversation/js/attachment.js"></script>
<script defer src="views/conversation/js/attachment_renderer.js"></script>
<script defer src="views/conversation/js/conversation.js"></script>
<script defer src="views/conversation/js/subject_composer.js"></script>
<script defer src="views/conversation/js/compose.js"></script>
<script defer src="views/shared/js/waiting_screen.js"></script>
<script defer src="views/shared/js/activity_picker.js"></script>
<script defer src="views/shared/js/wbmp.js"></script>
<script defer src="views/shared/js/smil.js"></script>
<script defer src="views/conversation/js/link_helper.js"></script>
<script defer src="views/conversation/js/link_action_handler.js"></script>
<script defer src="views/shared/js/notify.js"></script>
<script defer src="lib/bridge/client.js"></script>
<script defer src="services/js/activity/activity_client.js"></script>
<script defer src="services/js/messaging/messaging_client.js"></script>
<script defer src="services/js/moz_mobile_connections/moz_mobile_connections_client.js"></script>
<script defer src="views/shared/js/activity_handler.js"></script>
<script defer src="views/shared/js/system_message_handler.js"></script>
<script defer src="views/shared/js/contact_renderer.js"></script>
<script defer src="views/shared/js/localization_helper.js"></script>
<script defer src="views/conversation/js/information.js"></script>
-->
</head>
<body role="application" class="skin-comms theme-communications">
<iframe src="/views/shared/shim_host.html" class="shim-host" hidden></iframe>
<article id="main-wrapper" class="wrapper">
<section class="panel panel-InboxView panel-hidden" role="region" aria-hidden="true">
<!-- title-start and title-end indicates the button widths respectively
at the start and end of the line -->
<!-- see https://github.com/gaia-components/gaia-header/ for more information -->
<!-- WARNING: the actual value depends on the actual CSS being used in gaia-header -->
<gaia-header class="view-header regular-header" title-start="0" title-end="100">
<h1 data-l10n-id="messages"></h1>
<a href="#/composer" id="threads-composer-link" data-icon="compose" data-l10n-id="composeLink"></a>
<a id="threads-options-button" data-icon="more" data-l10n-id="actionsButton"></a>
<a id="threads-settings-button" data-icon="settings" data-l10n-id="settingsButton"></a>
</gaia-header>
<article id="threads-container" class="view-body edit-container" data-type="list">
</article>
<div id="threads-no-messages">
<div id="no-result-message">
<p data-l10n-id="noMessages-title">No messages</p>
<p data-l10n-id="noMessages-text-2">Start a conversation now</p>
</div>
</div>
<form id="threads-edit-form" role="dialog" data-type="edit" class="hide">
<section role="region" class="theme-settings">
<gaia-header id="threads-edit-header" action="close" no-font-fit>
<h1 id="threads-edit-mode" data-l10n-id="selectThreads-title"></h1>
<button id="threads-check-uncheck-all-button" class="edit-button" data-l10n-id="select-all"></button>
</gaia-header>
</section>
<div class="bottom-tab" role="region" >
<ul class="bb-tablist">
<li role="presentation">
<button id="threads-delete-button" class="disable-when-dialog-animates" data-action="delete-button"></button>
</li>
<li role="presentation">
<button id="threads-read-unread-button" data-action="mark-as-read"></button>
</li>
</ul>
</div>
</form>
<section id="threads-draft-saved-banner" class="hide" role="status">
<p data-l10n-id="message-draft-saved">Message saved as draft</p>
</section>
<div id="sticky"></div>
</section>
<section class="panel panel-ConversationView panel-hidden editable-select-mode" role="region" aria-hidden="true">
<gaia-header id="messages-header" class="view-header regular-header" action="back" no-font-fit>
<h1 id="messages-header-text"></h1>
<button id="messages-call-number-button" data-icon="call" class="hide" data-l10n-id="callButton"></button>
<button id="messages-options-button" data-icon="more" data-l10n-id="actionsButton"></button>
</gaia-header>
<div id="messages-subheader" class="subheader">
<section id="messages-to-field">
<section data-l10n-id="to" id="to-label">
To:
</section>
<!-- When overflow occurs in the recipients list, scrolling is
commonly triggered automatically through focus and keyboard events.
In these cases, white space described by CSS `padding-bottom` will
not be automatically scrolled into view (because the element will
only scroll far enough to bring the cursor into view). A container
element is necessary to consistently render this vertical space
(instead realized with CSS `margin-bottom`). -->
<div id="messages-recipients-list-container" class="bb-editable"
role="group" aria-labelledby="to-label">
<section id="messages-recipients-list" class="singleline">
</section>
</div>
<menu type="toolbar">
<a id="messages-contact-pick-button">
<span class="icon icon-user"></span>
</a>
</menu>
</section>
<div id="contact-carrier" class="carrier-wrapper"></div>
<!-- Notifications in this container will be rendered on top of the
Compose field. These notifications should be hidden automatically so
they do not prevent the user from inputting text. -->
<div class="overlay">
<section id="messages-subject-max-length-notice"
role="notification"
class="hide">
<p data-l10n-id='messages-max-subject-length-text'></p>
</section>
<section id="messages-max-length-notice"
role="notification"
class="hide">
<p></p>
</section>
<section id="messages-sms-counter-notice"
role="notification"
class="hide">
<p></p>
</section>
<section id="messages-convert-notice"
role="notification"
class="hide">
<p></p>
</section>
<section id="messages-resize-notice"
role="notification"
class="hide">
<p data-l10n-id="resize-image">Resizing the attached image</p>
</section>
<section id="messages-new-message-notice"
role="notification"
class="hide">
<div class="text-container">
<div data-l10n-id="new-message-notice-text">You have new messages</div>
<div dir="auto" class="contact">Contact</div>
</div>
<div class="button-container">
<button class="notice-button" data-l10n-id="new-message-notice-a-text">read it</button>
</div>
</section>
</div>
</div>
<article id="composer-container" class="view-body">
<div class="flex-composer">
<div id="messages-container" data-type="list" class="article-list"></div>
<form id="messages-compose-form" role="search" class="bottom" data-message-type="sms">
<section class="mms-label-line">
<section class="subject-composer js-subject-composer hide">
<div contentEditable="true" class="subject-composer-input"
role="textbox"
aria-multiline="false">
</div>
<div class="subject-composer-placeholder"
data-l10n-id="messagesSubjectInput_placeholder">
</div>
</section>
<span class="mms-label"
data-l10n-id="mms-label">MMS</span>
</section>
<section class="content-composer">
<div class="messages-attach-container">
<button id="messages-attach-button" aria-label="Add attachment" data-l10n-id="attach-button" type="button"></button>
</div>
<div class="composer-message-input-container">
<div id="messages-input" contentEditable="true" name="message"
x-inputmode="-moz-sms" data-l10n-id="messagesInput"
role="textbox" aria-multiline="true">
</div>
</div>
<div class="composer-button-container">
<span class="message-counter js-message-counter"></span>
<button id="messages-send-button" class="icon" aria-label="Send" data-l10n-id="send-button" type="submit">
<div class="js-sim-indication sim-indication" data-l10n-id="send-button-sim-indication"></div>
</button>
</div>
</section>
</form>
</div>
</article>
<form role="dialog" id="messages-edit-form" data-type="edit" class="hide">
<section role="region" class="theme-settings">
<gaia-header id="messages-edit-header" action="close" no-font-fit>
<h1 id="messages-edit-mode" data-l10n-id="deleteMessages-title"></h1>
<button id="messages-check-uncheck-all-button" class="edit-button" data-l10n-id="select-all"></button>
</gaia-header>
</section>
<div class="bottom-tab" role="region" >
<ul class="bb-tablist">
<li role="presentation">
<button id="messages-delete-button" class="disable-when-dialog-animates" data-action="delete-button"></button>
</li>
</ul>
</div>
</form>
<section id="messages-recipient-suggestions" class="article-list hide" data-type="list">
<ul class="contact-list recipient-suggestions-list" role="listbox"></ul>
</section>
</section>
<section class="panel panel-GroupView panel-hidden" role="region" aria-hidden="true">
<gaia-header id="information-group-header" class="header" action="back" no-font-fit>
<h1 data-l10n-id="participant" class="header-text"></h1>
</gaia-header>
<article class="view-body article-list" data-type="list">
<ul class="contact-list"></ul>
</article>
</section>
<section class="panel panel-ReportView panel-hidden" role="region" aria-hidden="true">
<gaia-header id="information-report-header" class="header" action="close" no-font-fit>
<h1 data-l10n-id="message-report"></h1>
</gaia-header>
<article class="view-body article-list report-view container" data-type="list">
<div class="description">
<p class="type"></p>
<p class="subject">
<span data-l10n-id="message-subject"></span>
<span class="detail"></span>
</p>
<p class="sent-block">
<span class="sent-title"></span>
<span class="sent-timestamp l10n-contains-date detail"></span>
</p>
<p class="received-block">
<span data-l10n-id="message-received"></span>
<span class="received-timestamp l10n-contains-date detail"></span>
</p>
<p class="lateness-block">
<span data-l10n-id="message-lateness"></span>
<span class="lateness">
<span class="lateness-month detail"></span>
<span class="lateness-day detail"></span>
<span class="lateness-hour detail"></span>
<span class="lateness-minute detail"></span>
<span class="lateness-second detail"></span>
</span>
</p>
<p class="size-block">
<span data-l10n-id="message-size-was"></span>
<span class="size detail"></span>
</p>
<p class="sim-info hide">
<span data-l10n-id="message-via"></span>
<span class="sim-detail detail"></span>
</p>
</div>
<span class="contact-title"></span>
<ul class="contact-list"></ul>
<button class="resend-btn" data-l10n-id="message-resend">
</button>
</article>
</section>
</article>
<article id="loading">
<form role="dialog" data-type="confirm" class="loading-container">
<section>
<p class="loading-header" data-l10n-id="deleting-messages">Deleting messages...</p>
<div role="status">
<progress class="small"></progress>
</div>
</section>
</form>
</article>
<!--
- Templates
-->
<div id="contact-suggestion-tmpl" class="hide">
<!--
<li role="presentation">
<a class="suggestion js-contact-info" role="option"
data-number="${number}" data-source="contacts" data-name="${name}"
data-type="${type}" data-carrier="${carrier}">
${photoHTML}
<p class="name"><bdi class="ellipsis-dir-fix">${nameHTML}</bdi></p>
<p class="number">${phoneDetailsHTML}</p>
</a>
</li>
-->
</div>
<div id="contact-report-tmpl" class="hide">
<!--
<li>
<div class="js-contact-info">
${photoHTML}
<p class="name"><bdi class="ellipsis-dir-fix">${nameHTML}</bdi></p>
<p class="number">${phoneDetailsHTML}</p>
</div>
</li>
-->
</div>
<div id="contact-suggestion-unknown-tmpl" class="hide">
<!--
<li role="presentation">
<a class="suggestion" role="option"
data-number="${number}" data-source="contacts" data-name="${name}"
data-type="${type}" data-carrier="${carrier}">
${photoHTML}
<p class="name">${phoneDetailsHTML}</p>
<p class="number">
<span data-l10n-id="unknown-contact-title">Unknown Contact</span>
</p>
</a>
</li>
-->
</div>
<div id="contact-prompt-tmpl" class="hide">
<!--
<div class="contact-header">
${photoHTML}
<p class="name"><bdi class="ellipsis-dir-fix">${name}</bdi></p>
<p class="details">${phoneDetailsHTML}</p>
</div>
-->
</div>
<div id="contact-photo-tmpl" class="hide">
<!--
<aside role="img" data-l10n-id="contact-photo" aria-label="Contact photo" class="pack-end">
<span class="contact-photo" data-type="img"></span>
</aside>
-->
</div>
<div id="contact-highlight-tmpl" class="hide">
<!--
<span class="highlight">${str}</span>
-->
</div>
<div id="messages-number-tmpl" class="hide">
<!--
<div class="suggestion js-contact-info" data-number="${number}" data-source="manual" data-name="${number}">
<p class="name"><bdi class="ellipsis-dir-fix">${number}</bdi></p>
</div>
-->
</div>
<div id="information-report-tmpl" class="hide">
<!--
<span class="icon"></span>
<span data-l10n-id="${titleL10n}"></span>
<span class="timestamp l10n-contains-date"
data-l10n-date="${timestamp}"
data-l10n-date-format="${messageL10nDateFormat}"
>${reportDateL10n}</span>
-->
</div>
<div id="messages-not-downloaded-tmpl" class="hide">
<!--
<span class="not-downloaded-message l10n-contains-date"
data-l10n-id="${messageL10nId}"
data-l10n-args="${messageL10nArgs}"
data-l10n-date="${messageL10nDate}"
data-l10n-date-format="${messageL10nDateFormat}"></span>
<button type="button" class="download" data-l10n-id="${downloadL10nId}"></button>
-->
</div>
<div id="messages-date-group-tmpl" class="hide">
<!--
<div id="${id}" class="messages-date-group" data-timestamp="${timestamp}">
<header
class="messages-date-group-header"
data-time-update="${headerTimeUpdate}"
data-time="${headerTime}"
data-date-only="${headerDateOnly}">
</header>
<ul class="message-list"></ul>
</div>
-->
</div>
<div id="messages-message-tmpl" class="hide">
<!--
<label class="pack-checkbox danger">
<input type="checkbox" value="${id}">
<span></span>
</label>
<section class="bubble">
<h1 class="message-subject">
${subject}
</h1>
<div class="message-content">
<p class="message-content-body" tabindex="-1">${bodyHTML}</p>
<div class="message-details">
${simInformationHTML}
<time data-time-update="true" data-time-only="true" data-time="${timestamp}"></time>
${messageStatusHTML}
</div>
</div>
</section>
-->
</div>
<div id="messages-message-sim-information-tmpl" class="hide">
<!--
<span class="message-sim-information"
data-l10n-id="sim-id-label"
data-l10n-args="${simNumberL10nArgs}">
</span>
-->
</div>
<div id="messages-message-status-tmpl" class="hide">
<!--
<div class="message-status" data-l10n-id="${statusL10nId}"
aria-label="Message status"
role="status">
</div>
-->
</div>
<div id="messages-late-arrival-notice-tmpl" class="hide">
<!--
<div class="late-arrival-icon" data-l10n-id="late-notice-label" aria-label="Late arrival"></div>
<div class="late-arrival-notice"></div>
-->
</div>
<div id="messages-thread-tmpl" class="hide">
<!--
<label class="pack-checkbox negative">
<input type="checkbox" value="${id}" data-mode="${mode}">
<span></span>
</label>
<a href="${hash}">
<aside role="note" class="icon icon-unread icon-draft js-conversation-state"></aside>
<aside class="pack-end threadlist-item-picture"
role="img" aria-label="Contact photo"
data-l10n-id="contact-photo">
<span data-type="img"></span>
</aside>
<p class="threadlist-item-title"><bdi class="ellipsis-dir-fix"></bdi></p>
<p class="summary">
<time data-time-update="true" data-time-only="true" data-time="${timestamp}"></time>
<span class="mms-icon" data-l10n-id="mms-label">MMS</span>
<span dir="auto" class="body-text js-conversation-body ellipsis-dir-fix"></span>
</p>
</a>
-->
</div>
<div id="messages-recipient-tmpl" class="hide">
<!--
<span class="${className}" x-inputmode="verbatim"
contenteditable="${editable}" role="${role}"
data-number="${number}" data-source="${source}" data-name="${name}"
data-carrier="${carrier}" data-type="${type}" dir="auto">
${name}
</span>
-->
</div>
<div id="attachment-nopreview-tmpl" class="hide">
<!--
<div class="attachment ${errorClass}" data-attachment-type="${type}">
<div class="thumbnail-placeholder ${type}-placeholder">
</div>
<div class="js-size-indicator size-indicator" data-l10n-id="${sizeL10nId}" data-l10n-args="${sizeL10nArgs}"></div>
</div>
<div class="file-name" dir="auto"></div>
-->
</div>
<div id="attachment-preview-tmpl" class="hide">
<!--
<div class="attachment">
<div class="thumbnail"></div>
<div class="js-size-indicator size-indicator" data-l10n-id="${sizeL10nId}" data-l10n-args="${sizeL10nArgs}"></div>
</div>
<div class="file-name" dir="auto"></div>
-->
</div>
<div id="attachment-draft-tmpl" class="hide">
<!--
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<base href="${baseURL}">
<link rel="stylesheet" type="text/css" href="views/conversation/style/attachment_draft.css">
<link rel="stylesheet" type="text/css" href="views/conversation/style/attachment.css">
</head>
<body class="attachment-draft ${cssClass}">${baseHTML}</body>
-->
</div>
<!--
Please, don't change template markup alignment, it's done this way to
remove any whitespace symbols between inline elements.
-->
<div id="phone-details-tmpl" class="hide">
<!--
<span class="${hasTypeClass} ${hasCarrierClass}"><span
dir="auto" class="phone-type"
data-l10n-id="${type}">${type}</span><span
class="phone-type-separator"
data-l10n-id="phone-type-separator"></span><span
class="phone-number" dir="ltr">${number}</span><span
class="phone-carrier-separator"
data-l10n-id="phone-carrier-separator"></span><span
class="phone-carrier" dir="auto">${carrier}</span>
</span>
-->
</div>
<div id="messages-group-header-tmpl" class="hide">
<!--
<span class="group-header-title"><bdi class="ellipsis-dir-fix">${name}</bdi>&nbsp;<bdi dir="ltr">(+${participantCount})</bdi></span>
-->
</div>
<div id="messages-header-tmpl" class="hide">
<!--
<bdi class="ellipsis-dir-fix">${name}</bdi>
-->
</div>
<gaia-sim-picker id="sim-picker" tabindex="-1" class="hide"></gaia-sim-picker>
</body>
</html>

24
gaia/apps/sms/jsdoc.json Executable file
View file

@ -0,0 +1,24 @@
{
"name": "sms",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": [
"apps/sms/views/conversation/js/**/*.js",
"apps/sms/views/inbox/js/**/*.js",
"apps/sms/views/new_message/js/**/*.js",
"apps/sms/views/shared/js/**/*.js",
"apps/sms/services/js/**/*.js"
]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/sms"
}
}

1344
gaia/apps/sms/lib/bridge/client.js Executable file

File diff suppressed because it is too large Load diff

29
gaia/apps/sms/lib/bridge/client.min.js vendored Executable file
View file

@ -0,0 +1,29 @@
var $jscomp={scope:{},global:this},Symbol;$jscomp.initSymbol=function(){$jscomp.global.Symbol||(Symbol=$jscomp.Symbol);$jscomp.initSymbol=function(){}};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(h){return"jscomp_symbol_"+h+$jscomp.symbolCounter_++};$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();Symbol.iterator||(Symbol.iterator=Symbol("iterator"));$jscomp.initSymbolIterator=function(){}};
$jscomp.makeIterator=function(h){$jscomp.initSymbolIterator();if(h[Symbol.iterator])return h[Symbol.iterator]();if(!(h instanceof Array||"string"==typeof h||h instanceof String))throw new TypeError(h+" is not iterable");var d=0;return{next:function(){return d==h.length?{done:!0}:{done:!1,value:h[d++]}}}};$jscomp.arrayFromIterator=function(h){for(var d,g=[];!(d=h.next()).done;)g.push(d.value);return g};$jscomp.arrayFromIterable=function(h){return h instanceof Array?h:$jscomp.arrayFromIterator($jscomp.makeIterator(h))};
$jscomp.arrayFromArguments=function(h){for(var d=[],g=0;g<h.length;g++)d.push(h[g]);return d};$jscomp.inherits=function(h,d){function g(){}g.prototype=d.prototype;h.prototype=new g;h.prototype.constructor=h;for(var l in d)if($jscomp.global.Object.defineProperties){var c=$jscomp.global.Object.getOwnPropertyDescriptor(d,l);void 0!==c&&$jscomp.global.Object.defineProperty(h,l,c)}else h[l]=d[l]};
(function(h){"object"===typeof exports&&"undefined"!==typeof module?module.exports=h():"function"===typeof define&&define.amd?define([],h):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).bridge=h()})(function(){return function d(g,l,c){function m(f,r){if(!l[f]){if(!g[f]){var k="function"==typeof require&&require;if(!r&&k)return k(f,!0);if(n)return n(f,!0);k=Error("Cannot find module '"+f+"'");throw k.code="MODULE_NOT_FOUND",k;}k=l[f]={exports:{}};
g[f][0].call(k.exports,function(b){var a=g[f][1][b];return m(a?a:b)},k,k.exports,d,g,l,c)}return l[f].exports}for(var n="function"==typeof require&&require,f=0;f<c.length;f++)m(c[f]);return m}({1:[function(d,g,l){var c=g.exports=self.bridge||{};c.client=d("../src/client");"u"!="undefined"[0]?(void 0)([],function(){return c}):self.bridge=c},{"../src/client":2}],2:[function(d,g,l){function c(a,t,b){if(!(this instanceof c))return new c(a,t,b);"object"==typeof a&&(t=a.endpoint,b=a.timeout,a=a.service);
this.id=r();this.service=a;this.timeout=b;this.endpoint=t||this.endpoint;if(!this.endpoint)throw m(1);this.setPort(this.endpoint);this.pending=new Set;this.receiver=q.receiver(this.id).on("_push",this.onPush.bind(this));k("initialized",a)}function m(a,b){for(var e=[],p=1;p<arguments.length;++p)e[p-1]=arguments[p];return Error({1:"an endpoint must be defined",2:'Unable to establish a connection with "'+e[0]+'". Either the target endpoint is not alive or the Service is not `.listen()`ing.',3:'Method "'+
e[0]+"\" didn't get a response. Either the target endpoint is not alive or the Service is not `.listen()`ing."}[a])}var n=d("./message/port-adaptors"),f=d("./emitter"),q=d("./message"),r=d("./utils").uuid;g.exports=c;var k={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Client] - "+a)},2:function(a,b){for(var e=[],p=1;p<arguments.length;++p)e[p-1]=arguments[p];console.log.apply(console,[].concat(["[Client]"+("["+self.constructor.name+"]["+location.pathname+"]")+
' - "'+a+'"'],$jscomp.arrayFromIterable(e)))}}[0],b=self.constructor.name;c.prototype={connect:function(){var a=this;k("connect");if(this.connected)return this.connected;k("connecting...",this.service);var t=new MessageChannel;this.channel=t.port1;this.channel.start();var e={clientId:this.id,service:this.service,originEnv:b};return this.connected=this.message("_connect").set("transfer",[t.port2]).set("data",e).listen(t.port1).send().then(function(b){k("connected",b);b.event.target===a.channel?a.setPort(a.channel):
(a.channel.close(),delete a.channel);a.receiver.listen(a.port)}).catch(function(b){"timeout"==(b&&b.message)&&(b=m(2,a.service),console.error(b.message));throw b;})},disconnect:function(a){var b=this;if(!this.connected)return Promise.resolve();k("disconnecting ...");a={noRespond:a&&a.noRespond,data:this.id};this.cancelPending();return this.message("_disconnect").set(a).send().then(function(){return b.onDisconnected()})},method:function(a,b){for(var e=[],c=1;c<arguments.length;++c)e[c-1]=arguments[c];
var d=this;return this.connect().then(function(){k("method",a);return d.message("_method").set({recipient:d.service,data:{name:a,args:e}}).send()}).then(function(a){return a.value}).catch(function(b){"timeout"==(b&&b.message)&&(b=m(3,a),console.error(b.message));throw b;})},plugin:function(a){a(this,{Emitter:f,uuid:r});return this},message:function(a){var b=this;k("create message",a);var e=q(a).set("port",this.port).set("timeout",this.timeout).on("response",function(){return b.pending.delete(e)}).on("cancel",
function(){return b.pending.delete(e)});this.pending.add(e);return e},cancelPending:function(){k("cancel pending");this.pending.forEach(function(a){a.cancel()});this.pending.clear()},pendingResponded:function(){var a=[];this.pending.forEach(function(b){return a.push(b.responded)});return Promise.all(a)},onPush:function(a){k("on push",a.data);this._emit(a.data.type,a.data.data)},onDisconnected:function(){var a=this;delete this.connected;this.pendingResponded().then(function(){k("disconnected");a.channel&&
a.channel.close();a._emit("disconnected")})},setPort:function(a){k("set port");this.port=n(a)},destroy:function(){var a=this;return this.disconnect().then(function(){a.destroyed||(k("destroy"),a.destroyed=!0,a.receiver.destroy(),a._off(),a.port=a.endpoint=a.receiver=null)})},_on:f.prototype.on,_off:f.prototype.off,_emit:f.prototype.emit};c.prototype.on=function(a,b){var e=this;this.connect().then(function(){k("bind on",a);f.prototype.on.call(e,a,b);e.message("_on").set("noRespond",!0).set("data",
{name:a,clientId:e.id}).send(e.port)});return this};c.prototype.off=function(a,b){var e=this;this.connect().then(function(){f.prototype.off.call(e,a,b);e.message("_off").set("noRespond",!0).set("data",{name:a,clientId:e.id}).send(e.port)});return this}},{"./emitter":3,"./message":4,"./message/port-adaptors":5,"./utils":6}],3:[function(d,g,l){function c(d){if(d)return Object.assign(d,c.prototype)}g.exports=c;c.prototype={on:function(c,d){this._callbacks||(this._callbacks={});this._callbacks[c]||(this._callbacks[c]=
[]);this._callbacks[c].push(d);return this},off:function(c,d){if(this._callbacks)switch(arguments.length){case 0:this._callbacks={};break;case 1:delete this._callbacks[c];break;default:var f=this._callbacks[c];if(!f)return;var g=f.indexOf(d);~g&&f.splice(g,1)}return this},emit:function(c,d){if(this._callbacks)for(var f=this._callbacks[c]||[],f=f.concat(this._callbacks["*"]||[]),g=0;g<f.length;g++)f[g].call(this,d,c);return this}};d=c.prototype;d.off=d.off;d.on=d.on},{}],4:[function(d,g,l){function c(a){this.cancelled=
!1;this.listeners=[];this.deferred=r();this.listen=this.listen.bind(this);this.onMessage=this.onMessage.bind(this);this.onTimeout=this.onTimeout.bind(this);"object"===typeof a?this.setupInbound(a):this.setupOutbound(a);b("initialized",this.type)}function m(a){this.name=a;this.ports=new Set;this.onMessage=this.onMessage.bind(this);this.listen=this.listen.bind(this);this.unlisten=this.unlisten.bind(this);b("receiver initialized",a)}function n(a,b){for(var e=1;e<arguments.length;++e);return Error({1:".send() can only be called once",
2:"response already sent for this message",3:"a port must be defined",4:"timeout"}[a])}var f=d("./port-adaptors"),q=d("../emitter");d=d("../utils");var r=d.deferred,k=d.uuid;l=g.exports=function(a){return new c(a)};l.receiver=function(a,b){return new m(a,b)};l.Receiver=m;l.Message=c;var b={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Message] - "+a)},2:function(a,b){for(var e=[],c=1;c<arguments.length;++c)e[c-1]=arguments[c];console.log.apply(console,[].concat(["[Message]"+
("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+a+'"'],$jscomp.arrayFromIterable(e)))}}[0];c.prototype={setupOutbound:function(a){this.id=k();this.type=a;this.sent=!1;this.recipient="*"},setupInbound:function(a){b("inbound");this.hasResponded=!1;this.setSourcePort(a.source||a.target);this.event=a;Object.assign(this,a.data)},setSourcePort:function(a){b("set source",a.constructor.name);this.sourcePort=f(a,{ready:!0});return this},set:function(a,c){b("set",a,c);"object"==typeof a?Object.assign(this,
a):this[a]=c;return this},serialize:function(){return{id:this.id,type:this.type,data:this.data,recipient:this.recipient,noRespond:this.noRespond}},preventDefault:function(){b("prevent default");this.defaultPrevented=!0},send:function(a){b("send",this.type);if(this.sent)throw n(1);var c=this.serialize(),e=!this.noRespond;this.port=a?f(a):this.port;if(!this.port)throw n(3);e?(this.listen(this.port),this.setResponseTimeout()):this.deferred.resolve();this.port.postMessage(c,this.getTransfer());b("sent",
c);return this.deferred.promise},setResponseTimeout:function(){!1!==this.timeout&&(this._timer=setTimeout(this.onTimeout,this.timeout||2E3))},clearResponseTimeout:function(){clearTimeout(this._timer)},getTransfer:function(){return this.transfer||this.event&&this.event.ports},onMessage:function(a){if(a.data.response&&a.data.id===this.id&&!this.cancelled)this.onResponse(a)},onTimeout:function(){b("response timeout",this.type);this.silentTimeout||this.deferred.reject(n(4));this.teardown()},listen:function(a){b("add response listener",
a);a=f(a);a.addListener(this.onMessage,this.listen);this.listeners.push(a);return this},unlisten:function(){var a=this;b("remove response listeners");this.listeners.forEach(function(b){return b.removeListener(a.onMessage)});this.listeners=[]},cancel:function(){this.teardown();this.cancelled=!0;this.emit("cancel")},teardown:function(){this.clearResponseTimeout();this.unlisten()},respond:function(a){function c(a){b("resolve",a);d({type:"resolve",value:a})}function e(a){a:switch(a&&a.constructor.name){case "DOMException":case "Error":a=
{message:a.message};break a;case "DOMError":a={message:a.message,name:a.name};break a}b("reject",a);d({type:"reject",value:a})}function d(a){f.response=a;f.sourcePort.postMessage({id:f.id,response:a},f.transfer);b("responded with:",a)}b("respond",a,this.id);if(this.hasResponded)throw n(2);if(this.sourcePort&&!this.noRespond){this.hasResponded=!0;var f=this;this.error&&e(this.error);Promise.resolve(a).then(c,e).catch(e)}},forward:function(a){var c=this;b("forward");return this.set("silentTimeout",
!0).send(a).then(function(a){return c.respond(a.value)})},onResponse:function(a){b("on response",a.data);var c=a.data.response,e="reject"==c.type?c.value:c;c.event=a;this.response=c;this.teardown();this.deferred[this.response.type](e);this.emit("response",c)}};q(c.prototype);m.prototype={listen:function(a){b("listen");a=f(a||self,{receiver:!0});if(!this.ports.has(a))return a.addListener(this.onMessage,this.listen),this.ports.add(a),this},unlisten:function(){var a=this;b("unlisten");this.ports.forEach(function(b){return b.removeListener(a.onMessage)})},
onMessage:function(a){if(a.data.id&&a.data.type&&this.isRecipient(a.data.recipient)&&(b("receiver on message",a.data),a=new c(a),this.emit("message",a),!a.defaultPrevented))try{this.emit(a.type,a)}catch(d){throw a.error=d,a.respond(),d;}},isRecipient:function(a){return a==this.name||"*"==a||"*"==this.name},destroy:function(){this.unlisten();delete this.name;return this}};q(m.prototype)},{"../emitter":3,"../utils":6,"./port-adaptors":5}],5:[function(d,g,l){function c(b){this.target=b}function m(b,
a,c){b.addEventListener(a,c)}function n(b,a,c){a={data:a,source:self};c&&(a.ports=c);b.dispatchEvent(new MessageEvent("message",a))}var f=d("../utils").deferred;g.exports=function(b,a){if(!b)throw Error({1:"target is undefined"}[1]);if(b&&b.addListener)return b;var d=r[b.constructor.name];return d?d(b,a):new c(b,a)};var q=c.prototype={constructor:c,addListener:function(b){this.target.addEventListener("message",b)},removeListener:function(b){this.target.removeEventListener("message",b)},postMessage:function(b,
a){this.target.postMessage(b,a)}},r={HTMLIFrameElement:function(b){var a=k(b);return{addListener:function(a){window.addEventListener("message",a)},removeListener:function(a){window.removeEventListener("message",a)},postMessage:function(c,e){a.then(function(){return n(b.contentWindow,c,e)})}}},BroadcastChannel:function(b,a){function c(){var a=f();b.postMessage("ready?");m(b,"message",function u(c){"ready"==c.data&&(b.removeEventListener("message",u),a.resolve())});return a.promise}var e=a&&a.receiver,
d=a&&a.ready,d=d||e?Promise.resolve():c();e&&(b.postMessage("ready"),m(b,"message",function(a){"ready?"==a.data&&b.postMessage("ready")}));return{target:b,addListener:q.addListener,removeListener:q.removeListener,postMessage:function(a,c){d.then(function(){return b.postMessage(a,c)})}}},Window:function(b,a){var c=a&&a.ready||b===parent||b===self,c=c?Promise.resolve():k(b);return{addListener:function(a){window.addEventListener("message",a)},removeListener:function(a){window.removeEventListener("message",
a)},postMessage:function(a,d){c.then(function(){return n(b,a,d)})}}},SharedWorker:function(b){b.port.start();return new c(b.port)},SharedWorkerGlobalScope:function(){var b=[];return{postMessage:function(){},addListener:function(a,c){this.onconnect=function(a){a=a.ports[0];b.push(a);a.start();c(a)};self.addEventListener("connect",this.onconnect)},removeListener:function(a){self.removeEventListener("connect",this.onconnect);b.forEach(function(b){b.close();b.removeEventListener("message",a)})}}}},k=
function(){if("undefined"!=typeof window){var b=window.opener||window.parent,a=new WeakSet;b!=self&&m(window,"DOMContentLoaded",function e(){window.removeEventListener("DOMContentLoaded",e);n(b,"load")});m(self,"message",function(b){return"load"==b.data&&a.add(b.source)});return function(b){var c=b.contentWindow||b;if(a.has(c)||c==window.parent)return Promise.resolve();var d=f();m(window,"message",function u(a){"load"==a.data&&a.source==c&&(window.removeEventListener("message",u),d.resolve())});return d.promise}}}()},
{"../utils":6}],6:[function(d,g,l){l.uuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var d=16*Math.random()|0;return("x"==c?d:d&3|8).toString(16)})};l.deferred=function(){var c={};c.promise=new Promise(function(d,g){c.resolve=d;c.reject=g});return c}},{}]},{},[1])(1)});

View file

@ -0,0 +1,197 @@
!((define)=>{define((require,exports,module)=>{
/**
* Mini Logger
*
* @type {Function}
*/
var debug = 0 ? function(arg1, ...args) {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[ClientStream]${type} - "${arg1}"`, ...args);
} : () => {};
/**
* Exports
*/
module.exports = (client, utils) => {
client._activeStreams = [];
client.on('streamevent', onStreamEvent);
client.stream = function(name) {
debug('stream open', name);
var args = [].slice.call(arguments, 1);
var stream = new ClientStream(client, utils);
var self = this;
this.method('_stream', name, stream.id, this.id, args)
.then(() => {
debug('stream connected', name);
stream._connected.resolve();
})
.catch(err => {
debug('error', err);
onStreamEvent({
type: 'abort',
id: stream.id,
data: err
});
});
this._activeStreams[stream.id] = stream;
stream.emitter.on('cancel', onEnd);
stream.emitter.on('abort', onEnd);
stream.emitter.on('close', onEnd);
function onEnd() {
debug('end');
delete self._activeStreams[stream.id];
stream.destroy();
}
return stream;
};
/**
* Called every time the service calls
* write/abort/close on the ServiceStream
*
* @param {Object} broadcast
* @param {String} broadcast.id Stream ID
* @param {String} broadcast.type Event type ('write', 'abort' or 'close')
* @private
*/
function onStreamEvent(data) {
debug('stream event', data.type);
var stream = client._activeStreams[data.streamId];
stream.emitter.emit(data.type, data.data);
}
};
/**
* Readable stream instance returned by
* a `client.stream('methodName')` call.
*
* @param {Object} options
* @param {String} options.id Stream Id, used to match client/service streams
* @param {Client} options.client Client instance
*/
function ClientStream(client, utils) {
this.id = utils.uuid();
this.client = client;
this.emitter = new utils.Emitter();
this.emitter.on('close', this.onClose.bind(this));
this.emitter.on('abort', this.onAbort.bind(this));
this._connected = defer();
this.connected = this._connected.promise;
this._closed = defer();
this.closed = this._closed.promise;
debug('initialized');
}
/**
* Prototype assigned to variable
* to improve compression.
*
* @type {Object}
*/
ClientStream.prototype = {
/**
* Add a listener that will be called
* every time the service broadcasts
* a new chunk of data.
*
* @param {Function} callback
*/
listen(callback) {
debug('listen', callback);
this.emitter.on('write', callback);
},
/**
* Removes 'data' listener
*
* @param {Function} callback
*/
unlisten(callback) {
debug('unlisten', callback);
this.emitter.off('write', callback);
},
/**
* Notify the service that
* action should be canceled
*
* @param {*} [reason] Optional data to be sent to service.
*/
cancel(reason) {
debug('cancel', reason);
return this.client.method('_streamcancel', this.id, reason)
.then(result => {
debug('cancelled', result);
this.emitter.emit('cancel');
return result;
})
.catch(e => {
this.emitter.emit('cancel');
});
},
/**
* Used internally by Client when
* it receives an 'abort' event
* from the service.
*
* @private
*/
onAbort(reason) {
debug('abort', reason);
this._closed.reject(reason);
},
/**
* Used internally by Client when
* it receives a 'close' event
* from the service.
*
* @private
*/
onClose() {
debug('close');
this._closed.resolve();
},
destroy() {
debug('destroy');
this.emitter.off();
}
};
/**
* Utils
*/
function defer() {
var result = {};
result.promise = new Promise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
});})((typeof define)[0]=='f'&&define.amd?define:((n,w)=>{return(typeof
module)[0]=='o'?c=>{c(require,exports,module);}:(c)=>{var m={exports:{}};
c(n=>w[n],m.exports,m);w[n]=m.exports;};})('streamClient',this));

View file

@ -0,0 +1,206 @@
!((define)=>{define((require,exports,module)=>{
/**
* Mini Logger
*
* @type {Function}
*/
var debug = 0 ? function(arg1, ...args) {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[ServiceStream]${type} - "${arg1}"`, ...args);
} : () => {};
module.exports = function(service) {
debug('attaching plugin');
service
.method('_stream', onStream)
.method('_streamcancel', onStreamCancel);
service._streamHandlers = {};
service._activeStreams = [];
service.stream = function(name, fn) {
debug('register stream', name);
this._streamHandlers[name] = fn;
return this;
};
function onStream(name, id, clientId, args) {
debug('on stream open', name);
var e = {
preventDefault: () => e.defaultPrevented = true,
arguments: arguments
};
// Allow other plugins to override default behaviour
service.emit('before-stream', e);
if (e.defaultPrevented) return;
var fn = service._streamHandlers[name];
if (!fn) throw error(1, name);
var stream = new ServiceStream(id, service, clientId);
service._activeStreams[id] = stream;
// Always pass stream object as first
// argument to simplify the process
fn.apply(service, [stream].concat(args));
}
function onStreamCancel(id, reason) {
debug('stream cancel');
var stream = service._activeStreams[id];
delete service._activeStreams[id];
return stream.cancel(reason);
}
};
/**
* Writable Stream instance passed to the
* `service.stream` implementation
*
* @param {Object} options
* @param {String} options.id Stream ID used to sync client and service streams
* @param {BroadcastChannel} options.channel Channel used to postMessage
* @param {String} options.serviceId ID of the service
* @param {String} options.clientId ID of client that should receive message
*/
function ServiceStream(streamId, service, clientId) {
this.id = streamId;
this.service = service;
this.clientId = clientId;
this.writable = true;
debug('initialized');
}
/**
* Prototype assigned to variable
* to improve compression.
*
* @type {Object}
*/
var ServiceStreamPrototype = ServiceStream.prototype;
/**
* Services that allows clients to
* cancel the operation before it's
* complete should override the
* `stream.cancel` method.
*
* @param {*} [reason] Data sent from client about the cancellation
* @returns {(Promise|*)}
*/
ServiceStreamPrototype.cancel = function(reason) {
var err = new TypeError('service should implement stream.cancel()');
return Promise.reject(err);
};
/**
* Signal to client that action was
* aborted during the process, this
* should be used as a way to
* communicate errors.
*
* @param {*} [data] Reason of failure
* @returns {Promise}
* @public
*/
ServiceStreamPrototype.abort = function(data) {
debug('abort', data);
this._post('abort', data);
this.writable = false;
};
/**
* Sends a chunk of data to the client.
*
* @param {*} data Chunk of data to be sent to client.
* @returns {Promise}
* @public
*/
ServiceStreamPrototype.write = function(data) {
debug('write', data);
this._post('write', data);
};
/**
* Closes the stream, signals that
* action was completed with success.
*
* According to whatwg streams spec,
* WritableStream#close() doesn't send data.
*
* @returns {Promise}
*/
ServiceStreamPrototype.close = function() {
debug('close');
this._post('close');
this.writable = false;
};
/**
* Validate the current state and
* call cancel on the target stream.
*
* Called by the Service when client
* sends a 'streamcancel' message.
*
* @param {*} [reason] Reason for cancelation sent by the client
* @returns {Promise}
* @private
*/
ServiceStreamPrototype._cancel = function(reason) {
debug('cancel', reason);
return this.cancel(reason)
.then(result => {
this.writable = false;
return result;
});
};
/**
* Validate the current state and post message to client.
*
* @param {String} type 'write', 'abort' or 'close'
* @param {String} state 'writable', 'aborted' or 'closed'
* @param {*} [data] Data to be sent to the client
* @returns {Promise}
* @private
*/
ServiceStreamPrototype._post = function(type, data) {
debug('post', type, data, this.writable);
if (!this.writable) return Promise.reject();
this.service.broadcast('streamevent', {
streamId: this.id,
type: type,
data: data
}, [this.clientId]);
};
/**
* Create new `Error` from registery.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id) {
var args = [].slice.call(arguments, 1);
return new Error({
1: 'stream "' + args[0] + '" doesn\'t exist'
}[id]);
}
});})((typeof define)[0]=='f'&&define.amd?define:((n,w)=>{return(typeof
module)[0]=='o'?c=>{c(require,exports,module);}:(c)=>{var m={exports:{}};
c(n=>w[n],m.exports,m);w[n]=m.exports;};})('streamService',this));

File diff suppressed because it is too large Load diff

27
gaia/apps/sms/lib/bridge/service.min.js vendored Executable file
View file

@ -0,0 +1,27 @@
var $jscomp={scope:{},global:this},Symbol;$jscomp.initSymbol=function(){$jscomp.global.Symbol||(Symbol=$jscomp.Symbol);$jscomp.initSymbol=function(){}};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(k){return"jscomp_symbol_"+k+$jscomp.symbolCounter_++};$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();Symbol.iterator||(Symbol.iterator=Symbol("iterator"));$jscomp.initSymbolIterator=function(){}};
$jscomp.makeIterator=function(k){$jscomp.initSymbolIterator();if(k[Symbol.iterator])return k[Symbol.iterator]();if(!(k instanceof Array||"string"==typeof k||k instanceof String))throw new TypeError(k+" is not iterable");var c=0;return{next:function(){return c==k.length?{done:!0}:{done:!1,value:k[c++]}}}};$jscomp.arrayFromIterator=function(k){for(var c,e=[];!(c=k.next()).done;)e.push(c.value);return e};$jscomp.arrayFromIterable=function(k){return k instanceof Array?k:$jscomp.arrayFromIterator($jscomp.makeIterator(k))};
$jscomp.arrayFromArguments=function(k){for(var c=[],e=0;e<k.length;e++)c.push(k[e]);return c};$jscomp.inherits=function(k,c){function e(){}e.prototype=c.prototype;k.prototype=new e;k.prototype.constructor=k;for(var l in c)if($jscomp.global.Object.defineProperties){var a=$jscomp.global.Object.getOwnPropertyDescriptor(c,l);void 0!==a&&$jscomp.global.Object.defineProperty(k,l,a)}else k[l]=c[l]};
(function(k){"object"===typeof exports&&"undefined"!==typeof module?module.exports=k():"function"===typeof define&&define.amd?define([],k):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).bridge=k()})(function(){return function c(e,l,a){function m(h,f){if(!l[h]){if(!e[h]){var g="function"==typeof require&&require;if(!f&&g)return g(h,!0);if(p)return p(h,!0);g=Error("Cannot find module '"+h+"'");throw g.code="MODULE_NOT_FOUND",g;}g=l[h]={exports:{}};
e[h][0].call(g.exports,function(d){var b=e[h][1][d];return m(b?b:d)},g,g.exports,c,e,l,a)}return l[h].exports}for(var p="function"==typeof require&&require,n=0;n<a.length;n++)m(a[n]);return m}({1:[function(c,e,l){var a=e.exports=self.bridge||{};a.service=c("../src/service");"u"!="undefined"[0]?(void 0)([],function(){return a}):self.bridge=a},{"../src/service":5}],2:[function(c,e,l){function a(c){if(c)return Object.assign(c,a.prototype)}e.exports=a;a.prototype={on:function(a,c){this._callbacks||(this._callbacks=
{});this._callbacks[a]||(this._callbacks[a]=[]);this._callbacks[a].push(c);return this},off:function(a,c){if(this._callbacks)switch(arguments.length){case 0:this._callbacks={};break;case 1:delete this._callbacks[a];break;default:var e=this._callbacks[a];if(!e)return;var h=e.indexOf(c);~h&&e.splice(h,1)}return this},emit:function(a,c){if(this._callbacks)for(var e=this._callbacks[a]||[],e=e.concat(this._callbacks["*"]||[]),h=0;h<e.length;h++)e[h].call(this,c,a);return this}};c=a.prototype;c.off=c.off;
c.on=c.on},{}],3:[function(c,e,l){function a(b){this.cancelled=!1;this.listeners=[];this.deferred=f();this.listen=this.listen.bind(this);this.onMessage=this.onMessage.bind(this);this.onTimeout=this.onTimeout.bind(this);"object"===typeof b?this.setupInbound(b):this.setupOutbound(b);d("initialized",this.type)}function m(b){this.name=b;this.ports=new Set;this.onMessage=this.onMessage.bind(this);this.listen=this.listen.bind(this);this.unlisten=this.unlisten.bind(this);d("receiver initialized",b)}function p(b,
d){for(var a=1;a<arguments.length;++a);return Error({1:".send() can only be called once",2:"response already sent for this message",3:"a port must be defined",4:"timeout"}[b])}var n=c("./port-adaptors"),h=c("../emitter");c=c("../utils");var f=c.deferred,g=c.uuid;l=e.exports=function(b){return new a(b)};l.receiver=function(b,d){return new m(b,d)};l.Receiver=m;l.Message=a;var d={0:function(){},1:function(b){return performance.mark("["+self.constructor.name+"][Message] - "+b)},2:function(b,d){for(var a=
[],f=1;f<arguments.length;++f)a[f-1]=arguments[f];console.log.apply(console,[].concat(["[Message]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+b+'"'],$jscomp.arrayFromIterable(a)))}}[0];a.prototype={setupOutbound:function(b){this.id=g();this.type=b;this.sent=!1;this.recipient="*"},setupInbound:function(b){d("inbound");this.hasResponded=!1;this.setSourcePort(b.source||b.target);this.event=b;Object.assign(this,b.data)},setSourcePort:function(b){d("set source",b.constructor.name);this.sourcePort=
n(b,{ready:!0});return this},set:function(b,a){d("set",b,a);"object"==typeof b?Object.assign(this,b):this[b]=a;return this},serialize:function(){return{id:this.id,type:this.type,data:this.data,recipient:this.recipient,noRespond:this.noRespond}},preventDefault:function(){d("prevent default");this.defaultPrevented=!0},send:function(b){d("send",this.type);if(this.sent)throw p(1);var a=this.serialize(),f=!this.noRespond;this.port=b?n(b):this.port;if(!this.port)throw p(3);f?(this.listen(this.port),this.setResponseTimeout()):
this.deferred.resolve();this.port.postMessage(a,this.getTransfer());d("sent",a);return this.deferred.promise},setResponseTimeout:function(){!1!==this.timeout&&(this._timer=setTimeout(this.onTimeout,this.timeout||2E3))},clearResponseTimeout:function(){clearTimeout(this._timer)},getTransfer:function(){return this.transfer||this.event&&this.event.ports},onMessage:function(b){if(b.data.response&&b.data.id===this.id&&!this.cancelled)this.onResponse(b)},onTimeout:function(){d("response timeout",this.type);
this.silentTimeout||this.deferred.reject(p(4));this.teardown()},listen:function(b){d("add response listener",b);b=n(b);b.addListener(this.onMessage,this.listen);this.listeners.push(b);return this},unlisten:function(){var b=this;d("remove response listeners");this.listeners.forEach(function(d){return d.removeListener(b.onMessage)});this.listeners=[]},cancel:function(){this.teardown();this.cancelled=!0;this.emit("cancel")},teardown:function(){this.clearResponseTimeout();this.unlisten()},respond:function(b){function a(b){d("resolve",
b);c({type:"resolve",value:b})}function f(b){a:switch(b&&b.constructor.name){case "DOMException":case "Error":b={message:b.message};break a;case "DOMError":b={message:b.message,name:b.name};break a}d("reject",b);c({type:"reject",value:b})}function c(b){g.response=b;g.sourcePort.postMessage({id:g.id,response:b},g.transfer);d("responded with:",b)}d("respond",b,this.id);if(this.hasResponded)throw p(2);if(this.sourcePort&&!this.noRespond){this.hasResponded=!0;var g=this;this.error&&f(this.error);Promise.resolve(b).then(a,
f).catch(f)}},forward:function(b){var a=this;d("forward");return this.set("silentTimeout",!0).send(b).then(function(b){return a.respond(b.value)})},onResponse:function(b){d("on response",b.data);var a=b.data.response,f="reject"==a.type?a.value:a;a.event=b;this.response=a;this.teardown();this.deferred[this.response.type](f);this.emit("response",a)}};h(a.prototype);m.prototype={listen:function(b){d("listen");b=n(b||self,{receiver:!0});if(!this.ports.has(b))return b.addListener(this.onMessage,this.listen),
this.ports.add(b),this},unlisten:function(){var b=this;d("unlisten");this.ports.forEach(function(d){return d.removeListener(b.onMessage)})},onMessage:function(b){if(b.data.id&&b.data.type&&this.isRecipient(b.data.recipient)&&(d("receiver on message",b.data),b=new a(b),this.emit("message",b),!b.defaultPrevented))try{this.emit(b.type,b)}catch(f){throw b.error=f,b.respond(),f;}},isRecipient:function(b){return b==this.name||"*"==b||"*"==this.name},destroy:function(){this.unlisten();delete this.name;return this}};
h(m.prototype)},{"../emitter":2,"../utils":6,"./port-adaptors":4}],4:[function(c,e,l){function a(d){this.target=d}function m(d,b,a){d.addEventListener(b,a)}function p(d,b,a){b={data:b,source:self};a&&(b.ports=a);d.dispatchEvent(new MessageEvent("message",b))}var n=c("../utils").deferred;e.exports=function(d,b){if(!d)throw Error({1:"target is undefined"}[1]);if(d&&d.addListener)return d;var g=f[d.constructor.name];return g?g(d,b):new a(d,b)};var h=a.prototype={constructor:a,addListener:function(d){this.target.addEventListener("message",
d)},removeListener:function(d){this.target.removeEventListener("message",d)},postMessage:function(d,b){this.target.postMessage(d,b)}},f={HTMLIFrameElement:function(d){var b=g(d);return{addListener:function(b){window.addEventListener("message",b)},removeListener:function(b){window.removeEventListener("message",b)},postMessage:function(a,f){b.then(function(){return p(d.contentWindow,a,f)})}}},BroadcastChannel:function(a,b){function f(){var b=n();a.postMessage("ready?");m(a,"message",function q(f){"ready"==
f.data&&(a.removeEventListener("message",q),b.resolve())});return b.promise}var g=b&&b.receiver,c=b&&b.ready,c=c||g?Promise.resolve():f();g&&(a.postMessage("ready"),m(a,"message",function(b){"ready?"==b.data&&a.postMessage("ready")}));return{target:a,addListener:h.addListener,removeListener:h.removeListener,postMessage:function(b,f){c.then(function(){return a.postMessage(b,f)})}}},Window:function(a,b){var f=b&&b.ready||a===parent||a===self,f=f?Promise.resolve():g(a);return{addListener:function(b){window.addEventListener("message",
b)},removeListener:function(b){window.removeEventListener("message",b)},postMessage:function(b,g){f.then(function(){return p(a,b,g)})}}},SharedWorker:function(f){f.port.start();return new a(f.port)},SharedWorkerGlobalScope:function(){var a=[];return{postMessage:function(){},addListener:function(b,f){this.onconnect=function(b){b=b.ports[0];a.push(b);b.start();f(b)};self.addEventListener("connect",this.onconnect)},removeListener:function(b){self.removeEventListener("connect",this.onconnect);a.forEach(function(a){a.close();
a.removeEventListener("message",b)})}}}},g=function(){if("undefined"!=typeof window){var a=window.opener||window.parent,b=new WeakSet;a!=self&&m(window,"DOMContentLoaded",function r(){window.removeEventListener("DOMContentLoaded",r);p(a,"load")});m(self,"message",function(a){return"load"==a.data&&b.add(a.source)});return function(a){var f=a.contentWindow||a;if(b.has(f)||f==window.parent)return Promise.resolve();var d=n();m(window,"message",function q(b){"load"==b.data&&b.source==f&&(window.removeEventListener("message",
q),d.resolve())});return d.promise}}}()},{"../utils":6}],5:[function(c,e,l){function a(f){if(!(this instanceof a))return new a(f);n.Receiver.call(this,f);this.clients={};this.methods={};this.on("_disconnect",this.onDisconnect.bind(this)).on("_connect",this.onConnect.bind(this)).on("_method",this.onMethod.bind(this)).on("_off",this.onOff.bind(this)).on("_on",this.onOn.bind(this));this.destroy=this.destroy.bind(this);h("initialized",f)}function m(a){var g=[].slice.call(arguments,1);return Error({4:'method "'+
g[0]+"\" doesn't exist"}[a])}var p=c("./utils").uuid,n=c("./message");c=n.Receiver;e.exports=a;var h={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Service] - "+a)},2:function(a,g){for(var d=[],b=1;b<arguments.length;++b)d[b-1]=arguments[b];console.log.apply(console,[].concat(["[Service]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+a+'"'],$jscomp.arrayFromIterable(d)))}}[0];a.prototype=Object.create(c.prototype);a.prototype.inWindow="Window"===
constructor.name;a.prototype.method=function(a,g){this.methods[a]=g;return this};a.prototype.broadcast=function(a,g,d){var b=this;h("broadcast",a,g,d);this.eachClient(function(c){if(!d||~d.indexOf(c.id))h("broadcasting to",c.id),b.push(a,g,c.id,{noRespond:!0})});return this};a.prototype.push=function(a,g,d,b){b=b&&b.noRespond;var c=this.getClient(d);return n("_push").set({recipient:d,noRespond:b,data:{type:a,data:g}}).send(c.port)};a.prototype.eachClient=function(a){for(var c in this.clients)a(this.clients[c])};
a.prototype.getClient=function(a){return this.clients[a]};a.prototype.onConnect=function(a){h("connection attempt",a.data,this.name);var c=a.data,d=c.clientId;d&&c.service===this.name&&!this.clients[d]&&(this.emit("before-connect",a),a.defaultPrevented||(this.upgradeChannel(a),this.addClient(d,a.sourcePort),a.respond(),this.emit("connected",d),h("connected",d)))};a.prototype.upgradeChannel=function(a){if(!this.inWindow||"Window"!==a.data.originEnv){var c=a.event.ports;if(c=c&&c[0])a.setSourcePort(c),
this.listen(c),c.start();h("channel upgraded")}};a.prototype.onDisconnect=function(a){var c=this.clients[a.data];c&&(this.emit("before-disconnect",a),a.defaultPrevented||(this.removeClient(c.id),a.respond(),this.emit("disconnected",c.id),h("disconnected",c.id)))};a.prototype.onMethod=function(a){h("on method",a.data);this.emit("before-method",a);if(!a.defaultPrevented){var c=a.data,d=c.name,b=this.methods[d],e;if(!b)throw m(4,d);try{e=b.apply(this,c.args)}catch(l){a.error=l}a.respond(e)}};a.prototype.onOn=
function(a){h("on on",a.data);this.emit("on",a.data)};a.prototype.onOff=function(a){h("on off");this.emit("off",a.data)};a.prototype.addClient=function(a,c){this.clients[a]={id:a,port:c}};a.prototype.removeClient=function(a){delete this.clients[a]};a.prototype.plugin=function(a){a(this,{uuid:p});return this};a.prototype.disconnect=function(a){this.removeClient(a.id);n("disconnect").set({recipient:a.id,noRespond:!0}).send(a.port)};a.prototype.destroy=function(){delete this.clients;this.unlisten();
this.off()};e=a.prototype;e.broadcast=e.broadcast;e.destroy=e.destroy;e.method=e.method;e.plugin=e.plugin},{"./message":3,"./utils":6}],6:[function(c,e,l){l.uuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var c=16*Math.random()|0;return("x"==a?c:c&3|8).toString(16)})};l.deferred=function(){var a={};a.promise=new Promise(function(c,e){a.resolve=c;a.reject=e});return a}},{}]},{},[1])(1)});

671
gaia/apps/sms/lib/sww.js Executable file
View file

@ -0,0 +1,671 @@
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var sww = require('./lib/sww.js');
self.ServiceWorkerWare = sww.ServiceWorkerWare;
self.StaticCacher = sww.StaticCacher;
self.SimpleOfflineCache = sww.SimpleOfflineCache;
},{"./lib/sww.js":5}],2:[function(require,module,exports){
'use strict';
// Inspired by expressjs and shed (https://github.com/wibblymat/shed)
function Router(options) {
this.options = options;
this.stack = [];
}
Router.prototype.ALL_METHODS = 'all';
Router.prototype.methods = ['get', 'post', 'put', 'delete', 'head',
Router.prototype.ALL_METHODS];
/**
* Add a new route to the stack.
* @param method (String) http verb to handle the request
* @param path (Regexp) string or regexp to match urls
* @param handler (Function) payload to be executed if url matches.
*/
Router.prototype.add = function r_add(method, path, handler) {
var pathRegex;
method = this._sanitizeMethod(method);
// Parse simle string path into regular expression for path matching
pathRegex = this._parseSimplePath(path);
this.stack.push({
method: method,
path: pathRegex,
handler: handler
});
};
/**
* Create the utility methods .get .post ... etc.
*/
Router.prototype.methods.forEach(function(method) {
Router.prototype[method] = function(path, handler) {
return this.add(method, path, handler);
};
});
Router.prototype.proxyMethods = function r_proxyPrototype(obj) {
var self = this;
this.methods.forEach(function(method) {
obj[method] = function(path, mw) {
if (typeof mw.onFetch !== 'function' && typeof mw !== 'function') {
throw new Error('This middleware cannot handle fetch request');
}
var handler = typeof mw.onFetch !== 'undefined' ?
mw.onFetch.bind(mw) : mw;
self.add(method, path, handler);
};
});
};
/**
* Matches the given url and methods with the routes stored in
* the stack.
*/
Router.prototype.match = function r_match(method, url) {
method = this._sanitizeMethod(method);
var matches = [];
var self = this;
this.stack.forEach(function eachRoute(route) {
if (!(method === route.method || route.method === self.ALL_METHODS)) {
return;
}
if (route.path.test(url)) {
matches.push(route.handler);
}
});
return matches;
};
Router.prototype._sanitizeMethod = function(method) {
var sanitizedMethod = method.toLowerCase().trim();
if (this.methods.indexOf(sanitizedMethod) === -1) {
throw new Error('Method "' + method + '" is not supported');
}
return sanitizedMethod;
};
/**
* Simple path-to-regex translation based on the Express "string-based path"
* syntax.
*/
Router.prototype._parseSimplePath = function(path) {
// Check for named placeholder crowding
if (/\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+/g.test(path)) {
throw new Error('Invalid usage of named placeholders');
}
// Check for mixed placeholder crowdings
var mixedPlaceHolders =
/(\*\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\:[a-zA-Z0-9]+)|(\:[a-zA-Z0-9]+\*)/g;
if (mixedPlaceHolders.test(path.replace(/\\\*/g,''))) {
throw new Error('Invalid usage of named placeholders');
}
// Try parsing the string and converting special characters into regex
try {
// Parsing anonymous placeholders with simple backslash-escapes
path = path.replace(/(.|^)\*/g, function(m,escape) {
return escape==='\\' ? '\\*' : (escape+'(.*?)');
});
// Parsing named placeholders with backslash-escapes
path = path.replace(/(.|^)\:([a-zA-Z0-9]+)/g, function(m,escape,tag) {
return escape==='\\' ? (':'+tag) : (escape+'(.+?)');
});
return new RegExp(path + '$');
}
// Failed to parse final path as a RegExp
catch (ex) {
throw new Error('Invalid path specified');
}
};
module.exports = Router;
},{}],3:[function(require,module,exports){
/* global Promise */
'use strict';
var cacheHelper = require('sw-cache-helper');
var debug = 0 ? console.log.bind(console, '[SimpleOfflineCache]') :
function(){};
// Default Match options, not exposed.
var DEFAULT_MATCH_OPTIONS = {
ignoreSearch: false,
ignoreMethod: false,
ignoreVary: false
};
var DEFAULT_MISS_POLICY = 'fetchAndChace';
// List of different policies
var MISS_POLICIES = [
DEFAULT_MISS_POLICY
];
/**
* Constructor for the middleware that serves the content of a
* cache specified by it's name.
* @param {string} cacheName Name of the cache that will be serving the content
* @param {object} [options] Object use to setup the cache matching alternatives
* @param {string} [missPolicy] Name of the policy to follow if a request miss
* when hitting the cache.
*/
function SimpleOfflineCache(cacheName, options, missPolicy) {
this.cacheName = cacheName || cacheHelper.defaultCacheName;
this.options = options || DEFAULT_MATCH_OPTIONS;
this.missPolicy = missPolicy || DEFAULT_MISS_POLICY;
if (MISS_POLICIES.indexOf(this.missPolicy) === -1) {
console.warn('Policy ' + missPolicy + ' not supported');
this.missPolicy = DEFAULT_MISS_POLICY;
}
}
SimpleOfflineCache.prototype.onFetch = function soc_onFetch(request, response) {
// If another middleware layer already have a response, the simple cache
// just pass through the response and does nothing.
if (response) {
return Promise.resolve(response);
}
var clone = request.clone();
var _this = this;
debug('Handing fetch event: %s', clone.url);
return this.ensureCache().then(function(cache) {
return cache.match(request.clone(), _this.options).then(function(res) {
if (res) {
return res;
}
// So far we just support one policy
switch(_this.missPolicy) {
case DEFAULT_MISS_POLICY:
return cacheHelper.fetchAndCache(request, cache);
}
});
});
};
SimpleOfflineCache.prototype.ensureCache = function soc_ensureCache() {
return cacheHelper.getCache(this.cacheName).then(function(cache) {
return cache;
});
};
module.exports = SimpleOfflineCache;
},{"sw-cache-helper":6}],4:[function(require,module,exports){
/* globals caches, Promise, Request */
'use strict';
function StaticCacher(fileList) {
if (!Array.isArray(fileList) || fileList.length === 0) {
throw new Error('Invalid file list');
}
this.files = fileList;
}
StaticCacher.prototype.onInstall = function sc_onInstall() {
var self = this;
return this.getDefaultCache().then(function(cache) {
return self.addAll(cache, self.files);
});
};
StaticCacher.prototype.getDefaultCache = function sc_getDefaultCache() {
return caches.open('offline');
};
StaticCacher.prototype.addAll = function(cache, urls) {
if (!cache) {
throw new Error('Need a cache to store things');
}
// Polyfill until chrome implements it
if (typeof cache.addAll !== 'undefined') {
return cache.addAll(urls);
}
var promises = [];
var self = this;
urls.forEach(function(url) {
promises.push(self.fetchAndCache(new Request(url), cache));
});
return Promise.all(promises);
};
StaticCacher.prototype.fetchAndCache =
function sc_fetchAndCache(request, cache) {
return fetch(request.clone()).then(function(response) {
if (parseInt(response.status) < 400) {
cache.put(request.clone(), response.clone());
}
return response;
});
};
module.exports = StaticCacher;
},{}],5:[function(require,module,exports){
/* global fetch, BroadcastChannel, clients, Promise, Request, Response */
'use strict';
var debug = 1 ? console.log.bind(console, '[ServiceWorkerWare]') : function(){};
var StaticCacher = require('./staticcacher.js');
var SimpleOfflineCache = require('./simpleofflinecache.js');
var Router = require('./router.js');
var ERROR = 'error';
var CONTINUE = 'continue';
var TERMINATE = 'terminate';
var TERMINATION_TOKEN = {};
function DEFAULT_FALLBACK_MW(request) {
return fetch(request);
}
function ServiceWorkerWare(fallbackMw) {
this.middleware = [];
this.router = new Router({});
this.router.proxyMethods(this);
this.fallbackMw = fallbackMw || DEFAULT_FALLBACK_MW;
}
ServiceWorkerWare.prototype.init = function sww_init() {
// lifecycle events
addEventListener('install', this);
addEventListener('activate', this);
addEventListener('beforeevicted', this);
addEventListener('evicted', this);
// network events
addEventListener('fetch', this);
// misc events
addEventListener('message', this);
// XXX: Add default configuration
};
/**
* Handle and forward all events related to SW
*/
ServiceWorkerWare.prototype.handleEvent = function sww_handleEvent(evt) {
debug('Event received: ' + evt.type);
switch(evt.type) {
case 'install':
this.onInstall(evt);
break;
case 'fetch':
this.onFetch(evt);
break;
case 'activate':
case 'message':
case 'beforeevicted':
case 'evicted':
this.forwardEvent(evt);
break;
default:
debug('Unhandled event ' + evt.type);
}
};
ServiceWorkerWare.prototype.onFetch = function sww_onFetch(evt) {
var steps = this.router.match(evt.request.method, evt.request.url);
// Push the fallback middleware at the end of the list.
// XXX bug 1165860: Decorating fallback MW with `stopIfResponse` until
// 1165860 lands
steps.push((function(req, res) {
if (res) {
return Promise.resolve(res);
}
return this.fallbackMw(req, res);
}).bind(this));
evt.respondWith(this.executeMiddleware(steps, evt.request));
};
/**
* Run the middleware pipeline and inform if errors preventing respondWith()
* to swallow the error.
*
* @param {Array} the middleware pipeline
* @param {Request} the request for the middleware
*/
ServiceWorkerWare.prototype.executeMiddleware = function (middleware, request) {
var response = this.runMiddleware(middleware, 0, request, null);
response.catch(function (error) { console.error(error); });
return response;
};
/**
* Pass through the middleware pipeline, executing each middleware in a
* sequence according to the result from each execution.
*
* Each middleware will be passed with the request and response from the
* previous one in the pipeline. The response from the latest one will be
* used to answer from the service worker. The middleware will receive,
* as the last parameter, a function to stop the pipeline and answer
* immediately.
*
* A middleware run can lead to continuing execution, interruption of the
* pipeline or error. The next action to be performed is calculated according
* the conditions of the middleware execution and its return value.
* See normalizeMwAnswer() for details.
*
* @param {Array} middleware pipeline.
* @param {Number} middleware to execute in the pipeline.
* @param {Request} the request for the middleware.
* @param {Response} the response for the middleware.
*/
ServiceWorkerWare.prototype.runMiddleware =
function (middleware, current, request, response) {
if (current >= middleware.length) {
return Promise.resolve(response);
}
var mw = middleware[current];
var endWith = ServiceWorkerWare.endWith;
var answer = mw(request, response, endWith);
var normalized =
ServiceWorkerWare.normalizeMwAnswer(answer, request, response);
return normalized.then(function (info) {
switch (info.nextAction) {
case TERMINATE:
return Promise.resolve(info.response);
case ERROR:
return Promise.reject(info.error);
case CONTINUE:
var next = current + 1;
var request = info.request;
var response = info.response;
return this.runMiddleware(middleware, next, request, response);
}
}.bind(this));
};
/**
* A function to force interruption of the pipeline.
*
* @param {Response} the response object that will be used to answer from the
* service worker.
*/
ServiceWorkerWare.endWith = function (response) {
if (arguments.length === 0) {
throw new Error('Type error: endWith() must be called with a value.');
}
return [TERMINATION_TOKEN, response];
};
/**
* A middleware is supposed to return a promise resolving in a pair of request
* and response for the next one or to indicate that it wants to answer
* immediately.
*
* To allow flexibility, the middleware is allowed to return other values
* rather than the promise. For instance, it is allowed to return only a
* request meaning the next middleware will be passed that request but the
* previous response untouched.
*
* The function takes into account all the scenarios to compute the request
* and response for the next middleware or the intention to terminate
* immediately.
*
* @param {Any} non normalized answer from the middleware.
* @param {Request} request passed as parameter to the middleware.
* @param {Response} response passed as parameter to the middleware.
*/
ServiceWorkerWare.normalizeMwAnswer = function (answer, request, response) {
if (!answer || !answer.then) {
answer = Promise.resolve(answer);
}
return answer.then(function (value) {
var nextAction = CONTINUE;
var error, nextRequest, nextResponse;
var isArray = Array.isArray(value);
if (isArray && value[0] === TERMINATION_TOKEN) {
nextAction = TERMINATE;
nextRequest = request;
nextResponse = value[1] || response;
}
else if (value === null) {
nextRequest = request;
nextResponse = null;
}
else if (isArray && value.length === 2) {
nextRequest = value[0];
nextResponse = value[1];
}
else if (value instanceof Response) {
nextRequest = request;
nextResponse = value;
}
else if (value instanceof Request) {
nextRequest = value;
nextResponse = response;
}
else {
var msg = 'Type error: middleware must return a Response, ' +
'a Request, a pair [Response, Request] or a Promise ' +
'resolving to one of these types.';
nextAction = ERROR;
error = new Error(msg);
}
return {
nextAction: nextAction,
request: nextRequest,
response: nextResponse,
error: error
};
});
};
/**
* Walk all the middle ware installed asking if they have prerequisites
* (on the way of a promise to be resolved) when installing the SW
*/
ServiceWorkerWare.prototype.onInstall = function sww_oninstall(evt) {
var waitingList = [];
this.middleware.forEach(function(mw) {
if (typeof mw.onInstall !== 'undefined') {
waitingList.push(mw.onInstall());
}
});
evt.waitUntil(Promise.all(waitingList));
};
/**
* Register a new middleware layer, they will treat the request in
* the order that this layers have been defined.
* A middleware layer can behave in the ServiceWorker in two ways:
* - Listening to SW lifecycle events (install, activate, message).
* - Handle a request.
* To handle each case (or both) the middleware object should provide
* the following methods:
* - on<SW LiveCiclyeEvent>: for listening to SW lifeciclye events
* - onFetch: for handling fetch urls
*/
ServiceWorkerWare.prototype.use = function sww_use() {
// If the first parameter is not a function we will understand that
// is the path to handle, and the handler will be the second parameter
if (arguments.length === 0) {
throw new Error('No arguments given');
}
var mw = arguments[0];
var path = '*';
var method = this.router.ALL_METHODS;
if (typeof mw === 'string') {
path = arguments[0];
mw = arguments[1];
var kind = typeof mw;
if (!mw || !(kind === 'object' || kind === 'function')) {
throw new Error('No middleware specified');
}
if (Router.prototype.methods.indexOf(arguments[2]) !== -1) {
method = arguments[2];
}
}
this.middleware.push(mw);
// Add to the router just if middleware object is able to handle onFetch
// or if we have a simple function
var handler = null;
if (typeof mw.onFetch === 'function') {
handler = mw.onFetch.bind(mw);
} else if (typeof mw === 'function') {
handler = mw;
}
if (handler) {
this.router.add(method, path, handler);
}
// XXX: Attaching the broadcastMessage to mw that implements onMessage.
// We should provide a way to get a reference to the SWW object and do
// the broadcast from there
if (typeof mw.onMessage === 'function') {
mw.broadcastMessage = this.broadcastMessage;
}
};
/**
* Forward the event received to any middleware layer that has a 'on<Event>'
* handler
*/
ServiceWorkerWare.prototype.forwardEvent = function sww_forwardEvent(evt) {
this.middleware.forEach(function(mw) {
var handlerName = 'on' + evt.type.replace(/^[a-z]/,
function(m){
return m.toUpperCase();
}
);
if (typeof mw[handlerName] !== 'undefined') {
mw[handlerName].call(mw, evt);
}
});
};
/**
* Broadcast a message to all worker clients
* @param msg Object the message
* @param channel String (Used just in Firefox Nightly) using broadcastchannel
* api to deliver the message, this parameter can be undefined as we listen for
* a channel undefined in the client.
*/
ServiceWorkerWare.prototype.broadcastMessage = function sww_broadcastMessage(
msg, channel) {
// XXX: Until https://bugzilla.mozilla.org/show_bug.cgi?id=1130685 is fixed
// we can use BroadcastChannel API in Firefox Nightly
if (typeof BroadcastChannel === 'function') {
var bc = new BroadcastChannel(channel);
bc.postMessage(msg);
bc.close();
return Promise.resolve();
} else {
// This is suppose to be the way of broadcasting a message, unfortunately
// it's not working yet in Chrome Canary
return clients.matchAll().then(function(consumers) {
consumers.forEach(function(client) {
client.postMessage(msg);
});
});
}
};
ServiceWorkerWare.decorators = {
ifNoResponse: function (mw) {
return function (req, res, endWith) {
if (res) { return [req, res]; }
return mw(req, res, endWith);
};
},
stopAfter: function (mw) {
return function (req, res, endWith) {
var answer = mw(req, res, endWith);
var normalized = ServiceWorkerWare.normalizeMwAnswer(answer, req, res);
return normalized.then(function (info) {
if (info.nextAction === 'error') {
return Promise.reject(info.error);
}
return endWith(info.response);
});
};
}
};
module.exports = {
ServiceWorkerWare: ServiceWorkerWare,
StaticCacher: StaticCacher,
SimpleOfflineCache: SimpleOfflineCache
};
},{"./router.js":2,"./simpleofflinecache.js":3,"./staticcacher.js":4}],6:[function(require,module,exports){
/* global caches, fetch, Promise, Request, module*/
(function() {
'use strict';
var CacheHelper = {
defaultCacheName: 'offline',
getCache: function getCache(name) {
return caches.open(name);
},
getDefaultCache: function getDefaultCache() {
return this.getCache(this.defaultCacheName);
},
fetchAndCache: function fetchAndChache(request, cache) {
return fetch(request.clone()).then(function(response) {
var clone = response.clone();
if (parseInt(clone.status) < 400) {
cache.put(request.clone(), response.clone());
}
return response.clone();
});
},
addAll: function addAll(cache, urls) {
if (!cache) {
throw new Error('Need a cache to store things');
}
// Polyfill until chrome implements it
if (typeof cache.addAll !== 'undefined') {
return cache.addAll(urls);
}
var promises = [];
var self = this;
urls.forEach(function(url) {
promises.push(self.fetchAndCache(new Request(url), cache));
});
return Promise.all(promises);
}
};
module.exports = CacheHelper;
})();
},{}]},{},[1])
//# sourceMappingURL=sww.js.map

View file

@ -0,0 +1,331 @@
# Headers
messages = Messages
deleteMessages-title = Delete messages
selectThreads-title = Select
# Edit Mode
edit = Edit
back = Back
cancel = Cancel
undo = Undo
done = Done
delete = Delete
delete-thread = Delete thread
mark-as-read = Mark as read
mark-as-unread = Mark as unread
deleting-messages = Deleting…
select-all = Select all
deselect-all = Deselect all
selected-threads = {[ plural(n) ]}
selected-threads[zero] = none selected
selected-threads[one] = {{n}} selected
selected-threads[two] = {{n}} selected
selected-threads[few] = {{n}} selected
selected-threads[many] = {{n}} selected
selected-threads[other] = {{n}} selected
selected-messages = {[ plural(n) ]}
selected-messages[zero] = none selected
selected-messages[one] = {{n}} selected
selected-messages[two] = {{n}} selected
selected-messages[few] = {{n}} selected
selected-messages[many] = {{n}} selected
selected-messages[other] = {{n}} selected
# Options menu
selectThreads-label = Select Threads
selectMessages-label = Select Messages
settings = Settings
add-subject = Add subject
remove-subject = Remove subject
includeSomeoneElse-label = Include someone else
# New Message
to = To:
new = New
message = Message
messagesInput.title = Message
messagesSubjectInput_placeholder = Subject
converted-to-mms = Converting to multimedia message.
converted-to-sms = Converting to text message.
messages-max-length-text = You’ve reached the maximum length.
messages-max-subject-length-text = Maximum length of subject reached.
# LOCALIZATION NOTE(multimedia-message-exceeded-max-length): shown when size
# of single multimedia message exceeds {{ mmsSize }}. The parameter {{ mmsSize }}
# contains the maximum size for a MMS with the current carrier.
multimedia-message-exceeded-max-length = You’ve exceeded the {{mmsSize}} KB size limit of a single multimedia message. Remove attachments or shorten the message to send it.
newMessage = New message
sendMessage = Send message
# LOCALIZATION NOTE(sendMMSToEmail): shown when clicking on an e-mail inside
# the body of a message to provide an option to send a multimedia message.
sendMMSToEmail = Send multimedia message
no-recipient = (No recipient)
recipient = {[ plural(n) ]}
recipient[one] = {{n}} recipient
recipient[two] = {{n}} recipients
recipient[few] = {{n}} recipients
recipient[many] = {{n}} recipients
recipient[other] = {{n}} recipients
attachmentSizeB = {{n}} B
attachmentSizeKB = {{n}} KB
attachmentSizeMB = {{n}} MB
attachmentOpenError = There is no application available to open this file type.
# Dialog to disambiguate in a new message
remove = Remove
# Group participants
participant = {[ plural(n) ]}
participant[one] = {{n}} participant
participant[two] = {{n}} participants
participant[few] = {{n}} participants
participant[many] = {{n}} participants
participant[other] = {{n}} participants
# Message report
message-report = Message report
message-type-sms = Text Message (SMS)
message-type-mms = Multimedia Message (MMS)
sms-report-description = Your Text Message (SMS), was sent on {{datetime}}
mms-report-description = Your Multimedia Message (MMS), subject {{subject}}, was sent on {{datetime}}
mms-no-subject-report-description = Your Multimedia Message (MMS), was sent on {{datetime}}
message-subject = Subject:
report-status-error = Message could not be sent
report-status-not-downloaded = not downloaded
report-status-delivered = Delivered:
report-status-read = Read:
report-status-pending = Report requested
# LOCALIZATION NOTE(report-status-rejected): used to describe the outgoing mms
# could not be downloaded by receiver(might be an invalid number or the number
# is not allowed to download mms)
report-status-rejected = Message rejected
message-size-was = Message size:
report-to-title = To
report-from-title = From
message-sent = Sent:
message-sending = {{message-delivery-status-sending}}
message-received = Received:
message-via = Via:
message-resend = Resend
message-lateness = Lateness:
# Search functionality
search = Search
unknown-contact-title = Unknown Contact
# LOCALIZATION NOTE(phone-type-separator): used to separate phone type from
# the number itself. It should be consistent with the characters used in
# "thread- header", \u20 is a blank space (,\u20 is equivalent to ", "). It can
# be used together with "phone-carrier-separator" if carrier is available.
# Resulting example for en-US: "Mobile, +123456789" or if carrier is available
# "Mobile, +123456789 Carrier".
phone-type-separator = ,\u20
# LOCALIZATION NOTE(phone-carrier-separator): used to separate carrier's name
# from the number itself. \u20 is a blank space. Resulting example for en-US:
# "+123456789 Verizon".
phone-carrier-separator = \u20
# LOCALIZATION NOTE(thread-participant-separator): used to separate list of
# contact names and phone numbers of any group message thread. Example for
# en-US: "Bob, +123456789, Alice".
thread-participant-separator = ,\u20
# Modal Dialogs
discard-new-message = Discard the message you’re composing and open the new message?
resend-confirmation = The message could not be sent. Try again?
resend-message = Resend this message
deleteMessage-confirmation = Delete selected message?
deleteMessages-confirmation-message = {[ plural(n) ]}
deleteMessages-confirmation-message[zero] = Please select messages to be deleted.
deleteMessages-confirmation-message[one] = Delete selected message?
deleteMessages-confirmation-message[two] = Delete {{n}} selected messages?
deleteMessages-confirmation-message[few] = Delete {{n}} selected messages?
deleteMessages-confirmation-message[many] = Delete {{n}} selected messages?
deleteMessages-confirmation-message[other] = Delete {{n}} selected messages?
deleteThreads-confirmation-message = {[ plural(n) ]}
deleteThreads-confirmation-message[zero] = Please select message threads to be deleted.
deleteThreads-confirmation-message[one] = Delete selected message thread?
deleteThreads-confirmation-message[two] = Delete {{n}} selected message threads?
deleteThreads-confirmation-message[few] = Delete {{n}} selected message threads?
deleteThreads-confirmation-message[many] = Delete {{n}} selected message threads?
deleteThreads-confirmation-message[other] = Delete {{n}} selected message threads?
new-message-notice-text = New message from
new-message-notice-a-text = Read it
noMessages-title = No messages
noMessages-text-2 = Start a conversation now
sendDefaultErrorTitle = Message not sent
sendDefaultErrorBody = There was a problem sending the message. Please try again.
sendDefaultErrorBtnOk = OK
sendMissingSimCardTitle = Missing SIM card
sendMissingSimCardBody = Insert a valid SIM card to continue.
sendMissingSimCardBtnOk = OK
sendFlightModeTitle = Airplane mode activated
sendFlightModeBody = Turn off airplane mode to send messages.
sendFlightModeBtnOk = OK
radioDisabledToDownloadTitle = {{sendFlightModeTitle}}
radioDisabledToDownloadBody = Turn off airplane mode to download attachments.
radioDisabledToDownloadBtnOk = {{sendFlightModeBtnOk}}
fdnErrorTitle = FDN active
# LOCALIZATION NOTE (sms-counter-notice-label): if typed message exceeds the
# character limit, more segments (messages) will be sent. {{number}} is the
# number of current segment.
# Example: Limit 160 characters, toaster will show up when 161 characters
# typed, and string in toaster will be 'Editing SMS 2' (editing 2nd segment).
sms-counter-notice-label = Editing SMS {{number}}
#LOCALIZATION NOTE (fdnErrorBody): the English translation does not need the
# actual number of phone numbers. If your translation needs it, you can still add it
# using the parameter {{ n }}.
fdnErrorBody = {[ plural(n) ]}
fdnErrorBody[one] = FDN (Fixed Dialing Numbers) is activated. Make sure the SMS center and the following recipient are in your FDN list:
fdnErrorBody[two] = FDN (Fixed Dialing Numbers) is activated. Make sure the SMS center and the following recipients are in your FDN list:
fdnErrorBody[few] = FDN (Fixed Dialing Numbers) is activated. Make sure the SMS center and the following recipients are in your FDN list:
fdnErrorBody[many] = FDN (Fixed Dialing Numbers) is activated. Make sure the SMS center and the following recipients are in your FDN list:
fdnErrorBody[other] = FDN (Fixed Dialing Numbers) is activated. Make sure the SMS center and the following recipients are in your FDN list:
fdnErrorBtnOk = OK
switchSimToRetrieveTitle = Switch data connection
switchSimToRetrieveBody = Switch the data connection to SIM {{ nonActiveSimId }} to retrieve the MMS. The current data transfer on SIM {{ activeSimId }} will be suspended. Continue anyway?
switchSimToRetrieveBtnOk = Later
switchSimToRetrieveConfirm = OK
# no need to retranslate title and buttons if not necessary
switchSimToSendTitle = {{switchSimToRetrieveTitle}}
switchSimToSendBody = Switch the data connection to SIM {{ nonActiveSimId }} to send the MMS. The current data transfer on SIM {{ activeSimId }} will be suspended. Continue anyway?
switchSimToSendBtnOk = {{switchSimToRetrieveBtnOk}}
switchSimToSendConfirm = {{switchSimToRetrieveConfirm}}
sendNoSignalErrorTitle = No network coverage
sendNoSignalErrorBody = Make sure you have a mobile signal and try again.
sendNoSignalErrorBtnOk = OK
sendNotFoundErrorTitle = Message not found
sendNotFoundErrorBody = The message you’re looking for is no longer available.
sendNotFoundErrorBtnOk = OK
sendInvalidAddressErrorTitle = Invalid number
sendInvalidAddressErrorBody = Make sure the mobile number you’re sending to is valid.
sendInvalidAddressErrorBtnOk = OK
simNotMatchedErrorTitle = Unable to download
simNotMatchedErrorBody = Please make sure the SIM card inserted is the one used to receive this message.
simNotMatchedErrorBtnOk = OK
deleted-sms = This message has already been deleted.
view-attachment-image = View
view-attachment-audio = Listen
view-attachment-video = View
view-attachment-other = View
remove-attachment-image = Remove image
remove-attachment-audio = Remove audio
remove-attachment-video = Remove video
remove-attachment-other = Remove attachment
replace-attachment-image = Replace image
replace-attachment-audio = Replace audio
replace-attachment-video = Replace video
replace-attachment-other = Replace attachment
message-options = Message options
thread-options = Thread options
forward = Forward
view-message-report = View message report
select-text = Select text
# Labels for contact fields
mobile = Mobile
home = Home
work = Work
personal = Personal
faxHome = Fax home
faxOffice = Fax office
faxOther = Fax other
another = Another
other = Other
# Date and time
today = today
todayWithTime = today {{ time }}
yesterday = yesterday
yesterdayWithTime = yesterday {{ time }}
#Phone Context Menu
addToExistingContact = Add to an existing contact
createNewContact = Create new contact
viewContact = View contact
call = Call
sendEmail = Send email
# MMS message
mms-message = MMS message
notDownloaded-title = Message is not downloaded yet.
# LOCALIZATION NOTE (tobedownloaded-attachment, expired-attachment):
# {{date}} is replaced at run-time with a date. The format used to display
# this date is specified in the string with ID 'expiry-date-format'.
tobedownloaded-attachment = You received a multimedia message. You can use the link below to download its content until {{date}}.
expired-attachment = Sorry, the message expired on {{date}} and cannot be downloaded.
download-attachment = Download
downloading-attachment = Downloading…
unnamed-attachment = untitled
# LOCALIZATION NOTE (mms-label): This label is used to identify MMS in message
# composer (above the send button) and in the thread list for the thread with
# MMS as the last message.
mms-label = MMS
# LOCALIZATION NOTE (attached-files-too-large): the English translation does not need the
# actual number of files. If your translation needs it, you can still add it
# using the parameter {{ n }}. The parameter {{ mmsSize }} contains the maximum size
# for a MMS with the current carrier.
attached-files-too-large = {[ plural(n) ]}
attached-files-too-large[zero] = The files you have selected exceed the {{mmsSize}} KB size limit of a single multimedia message.
attached-files-too-large[one] = The file you have selected exceeds the {{mmsSize}} KB size limit of a single multimedia message.
attached-files-too-large[two] = The files you have selected exceed the {{mmsSize}} KB size limit of a single multimedia message.
attached-files-too-large[few] = The files you have selected exceed the {{mmsSize}} KB size limit of a single multimedia message.
attached-files-too-large[many] = The files you have selected exceed the {{mmsSize}} KB size limit of a single multimedia message.
attached-files-too-large[other] = The files you have selected exceed the {{mmsSize}} KB size limit of a single multimedia message.
resize-image = Resizing the attached image
no-attachment-text = There is a problem with the attached file and it cannot be downloaded.
# Draft Saved
message-draft-saved = Message saved as draft
# some DSDS related localizations
sim-id-label = SIM {{id}}
sim-detail = SIM {{id}}, {{detailString}}
dsds-unknown-sim = unknown SIM
dsds-notification-title-with-sim = ({{sim}}) {{sender}}
#LOCALIZATION NOTE (send-button): localize only the ariaLabel
send-button.ariaLabel = Send
# LOCALIZATION NOTE (send-button-sim-indication): don't localize the
# sim-indication itself as we use it to show the SIM number only. Yet obviously,
# the "ariaLabel" needs to be localized.
send-button-sim-indication = {{n}}
send-button-sim-indication.ariaLabel = using SIM {{n}}
# Labels for screen reader
attach-button.ariaLabel = Add attachment
contact-photo.ariaLabel = Contact photo
unread-thread.ariaLabel = Unread
has-draft.ariaLabel = Has draft
is-draft.ariaLabel = Draft
actionsButton.ariaLabel = Actions
settingsButton.ariaLabel = Settings
composeLink.ariaLabel = New message
callButton.ariaLabel = Call
# Message statuses
message-delivery-status-sending = Sending…
message-delivery-status-delivered.ariaLabel = Delivered
message-delivery-status-read.ariaLabel = Read
message-delivery-status-error.ariaLabel = Error
# Late arrival notices
late-notice-label.ariaLabel = Late arrival
late-arrival-notice-in-months = {[ plural(lateness) ]}
late-arrival-notice-in-months[zero] = {{ lateness }}mth late
late-arrival-notice-in-months[one] = {{ lateness }}mth late
late-arrival-notice-in-months[two] = {{ lateness }}mths late
late-arrival-notice-in-months[few] = {{ lateness }}mths late
late-arrival-notice-in-months[many] = {{ lateness }}mths late
late-arrival-notice-in-months[other] = {{ lateness }}mths late
late-arrival-notice-in-days = {{ lateness }}d late
late-arrival-notice-in-hours = {{ lateness }}h late
late-arrival-notice-in-minutes = {{ lateness }}m late
# Modal dialog strings
modal-dialog-ok-button = OK
modal-dialog-cancel-button = Cancel
# LOCALIZATION NOTE (modal-dialog-default-title): Here we should have name of
# the Messages app, the same as we have in app manifest.
modal-dialog-default-title = Messages

109
gaia/apps/sms/manifest.webapp Executable file
View file

@ -0,0 +1,109 @@
{
"name": "Messages",
"description": "Gaia Messages",
"type": "certified",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"themeable": {},
"sms": {},
"mobileconnection": {},
"alarms": {},
"contacts": {
"access": "readonly"
},
"settings": {
"access": "readwrite"
},
"audio-channel-notification": {},
"desktop-notification": {},
"phonenumberservice": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"locales": {
"ar": {
"name": "رسائل",
"description": "Gaia رسائل"
},
"en-US": {
"name": "Messages",
"description": "Gaia Messages"
},
"fr": {
"name": "Messages",
"description": "Messages de Gaia"
},
"zh-TW": {
"name": "訊息",
"description": "Gaia 訊息"
}
},
"default_locale": "en-US",
"icons": {
"84": "/icons/sms_84.png",
"126": "/icons/sms_126.png",
"142": "/icons/sms_142.png",
"189": "/icons/sms_189.png",
"284": "/icons/sms_284.png"
},
"orientation": "default",
"activities": {
"new": {
"href": "/index.html#/activity-new",
"filters": {
"type": "websms/sms",
"number": {
"pattern": "[\\w\\s+#*().-]{0,50}"
}
},
"disposition": "inline",
"returnValue": true
},
"share": {
"href": "/index.html#/activity-share",
"filters": {
"type": [
"image/*",
"audio/*",
"video/*",
"url",
"text/vcard"
],
"number": {
"max": 5
}
},
"disposition": "inline",
"returnValue": true
}
},
"messages": [
{
"alarm": "/index.html"
},
{
"sms-received": "/index.html"
},
{
"notification": "/index.html"
}
],
"connections": {
"costcontrolSmsQuery": {
"description": "Communicate between Usage and Sms",
"rules": {
"manifestURLs": [
"app://costcontrol.gaiamobile.org/manifest.webapp"
]
}
}
},
"datastores-access": {
"Gaia_Facebook_Friends": {
"description": "Facebook Friends"
}
}
}

View file

@ -0,0 +1,11 @@
(function() {
'use strict';
if (navigator.mozIccManager) {
return;
}
navigator.mozIccManager = {
iccIds: []
};
})();

17
gaia/apps/sms/package.json Executable file
View file

@ -0,0 +1,17 @@
{
"name": "SMS",
"private": true,
"version": "0.0.1",
"author": "The Gaia Team <dev-gaia@lists.mozilla.org>",
"dependencies": {
"lodash": "^3.0.0"
},
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=sms"],
"cwd": "../.."
},
"packageDir": "../../build_stage/sms/"
}
}

View file

@ -0,0 +1,123 @@
/* global bridge,
BroadcastChannel,
EventDispatcher
*/
(function(exports) {
'use strict';
/**
* Name of the service that is responsible for managing activities.
* @type {string}
*/
const SERVICE_NAME = 'activity-service';
/**
* Reference to active bridge client instance. For every app instance that is
* run as inline activity we have only one bridge service (hosted in the main
* window context) and only one bridge client (hosted in the same context).
* Client should only communicate with the service that holds reference to
* the corresponding activity request. Since we can potentially have more than
* one inline activity instance we should use unique bridge service name for
* every app instance to avoid message collision.
*
* @type {Client}
*/
var client;
/**
* Indicates if there is pending activity request.
* @type {boolean}
*/
var hasPendingRequest;
/**
* ActivityClient is a wrapper around bridge client connected to the bridge
* service hosted in the context that can handle system messages. It allows
* to access "activity" request coming from system message when consumer can't
* receive it directly (document in a different URL or even worker).
* @type {Object}
*/
var ActivityClient = {
/**
* Initialized activity bridge client.
* @param {string} appInstanceId Unique identifier of app instance where
* client resides in.
*/
init(appInstanceId) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}
client = bridge.client({
service: SERVICE_NAME,
endpoint: new BroadcastChannel(
`${SERVICE_NAME}-channel-${appInstanceId}`
),
// Disable service response timeouts.
timeout: false
});
hasPendingRequest = false;
client.on('activity-request', (request) => {
// This case should never happen since we use "inline" activities only.
if (hasPendingRequest) {
this.postError('Concurrent activity requests are not allowed');
return;
}
hasPendingRequest = true;
this.emit(request.name + '-activity-request', request.data);
});
},
/**
* Posts results to the activity caller.
* @param {*?} result
* @returns {Promise} Promise that resolves once operation is completed.
*/
postResult(result) {
if (!hasPendingRequest) {
return Promise.reject(
new Error('There is no any activity request to post result to!')
);
}
hasPendingRequest = false;
return client.method('postResult', result);
},
/**
* Notifies activity caller that exception occurred.
* @param {*?} error
* @returns {Promise} Promise that resolves once operation is completed.
*/
postError(error) {
if (!hasPendingRequest) {
return Promise.reject(
new Error('There is no any activity request to post error to!')
);
}
hasPendingRequest = false;
console.error('Activity request was rejected with "%s" reason', error);
return client.method('postError', error);
},
/**
* Checks if client has pending activity request.
* @returns {boolean}
*/
hasPendingRequest() {
return hasPendingRequest;
}
};
exports.ActivityClient = Object.freeze(
EventDispatcher.mixin(
ActivityClient, ['new-activity-request', 'share-activity-request']
)
);
})(window);

View file

@ -0,0 +1,144 @@
/* global BridgeServiceMixin,
BroadcastChannel
*/
/* exported ActivityShim */
'use strict';
(function(exports) {
const priv = Object.freeze({
request: Symbol('request'),
setMessageHandler: Symbol('setMessageHandler'),
onActivityRequest: Symbol('onActivityRequest')
});
/**
* Name of the service that is responsible for managing activities.
* @type {string}
*/
const SERVICE_NAME = 'activity-service';
/**
* Name of the system message that is leveraged by activities.
* @type {string}
*/
const SYSTEM_MESSAGE_NAME = 'activity';
/**
* List of methods exposed by shim.
* @type {Array.<string>}
*/
const METHODS = Object.freeze(['postResult', 'postError']);
/**
* List of event that can be broadcasted by shim.
* @type {Array.<string>}
*/
const EVENTS = Object.freeze(['activity-request']);
/**
* ActivityShim is a shim around "activity" system message handling code that
* allows to access activity request data by the consumer that can't handle
* system messages directly (document hosted in a different URl or worker) via
* exposing bridge service with required methods.
* @type {Object}
*/
var ActivityShim = {
/**
* Reference to currently active activity request.
* @type {ActivityRequestHandler}
* @private
*/
[priv.request]: null,
/**
* Reference to mozSetMessageHandler function.
* @type {Function}
* @private
*/
[priv.setMessageHandler]: null,
/**
* Initialized activity shim bridge service.
* @param {string} appInstanceId Unique identifier of app instance where
* shim resides in.
*/
init(appInstanceId, setMessageHandler) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}
this[priv.setMessageHandler] = setMessageHandler;
// We may consider using exports.parent as endpoint instead in patch for
// bug 1223363, once it is unblocked.
this.initService(
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
);
},
/**
* Method is called by bridge when a new client is connected to the service.
* @private
*/
onConnected() {
// We subscribe for the system message only when we have client connected,
// to avoid the case when service broadcasts message, but no one listens
// for it yet.
this[priv.setMessageHandler](
SYSTEM_MESSAGE_NAME, this[priv.onActivityRequest].bind(this)
);
},
/**
* Posts activity request result.
* @param {*} result Data to post as activity request result.
* @private
*/
postResult(result) {
var request = this[priv.request];
if (!request) {
throw new Error('There is no any activity request to post result to!');
}
request.postResult(result || { success: true });
this[priv.request] = null;
},
/**
* Posts activity request error.
* @param {*} error Error to post as activity request error.
* @private
*/
postError(error) {
var request = this[priv.request];
if (!request) {
throw new Error('There is no any activity request to post error to!');
}
request.postError(error);
this[priv.request] = null;
},
/**
* Handler that fires once app receives activity request via system message.
* @param {ActivityRequestHandler} request Activity request.
* @private
*/
[priv.onActivityRequest](request) {
this[priv.request] = request;
this.broadcast('activity-request', request.source);
}
};
exports.ActivityShim = Object.seal(
BridgeServiceMixin.mixin(
ActivityShim,
SERVICE_NAME,
{ methods: METHODS, events: EVENTS }
)
);
})(window);

View file

@ -0,0 +1,110 @@
/*global bridge,
streamService
*/
/*jshint esnext: true */
'use strict';
(function(exports) {
const priv = Object.freeze({
methods: Symbol('methods'),
streams: Symbol('streams'),
events: Symbol('events'),
service: Symbol('service')
});
/**
* This mixin allows to expose easily a service from an object.
*
* This needs the following properties to work properly:
*
* - SERVICE_NAME holds the service name.
* - METHODS holds the methods that will be exposed by the service.
* - STREAMS holds the methods that handle streams exposed by the service.
*
* EVENTS, METHODS and STREAMS are all optional.
*/
var serviceMixin = {
/**
* Initialize service to listen on the default context and given endpoint.
* @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint
* The context/thread that service could listen for.
*/
initService(endpoint) {
if (!('bridge' in self) || !('service' in bridge)) {
importScripts('/lib/bridge/service.js');
}
var service = bridge.service(this[priv.name]);
this[priv.methods].forEach((exposedMethod) => {
service.method(exposedMethod, this[exposedMethod].bind(this));
});
var streams = this[priv.streams];
if (streams.length > 0) {
if (!('streamService' in self)) {
importScripts('/lib/bridge/plugins/stream/service.js');
}
service.plugin(streamService);
streams.forEach((exposedStream) => {
service.stream(exposedStream, this[exposedStream].bind(this));
});
}
if (this.onConnected) {
service.on('connected', this.onConnected.bind(this));
}
service.listen();
endpoint && service.listen(endpoint);
this[priv.service] = service;
},
/**
* Broadcast an event to all listeners.
* @param {String} eventName The event name to send.
* @param {any} data Data to send with the event.
*/
broadcast(eventName, data) {
if (this[priv.events].indexOf(eventName) < 0) {
throw new Error('Event "' + eventName + '" is not allowed!');
}
this[priv.service].broadcast(eventName, data);
}
};
exports.BridgeServiceMixin = {
mixin(target, name, { methods, streams, events }) {
if (!name) {
throw new Error(
'A service name is mandatory to define a service.'
);
}
Object.keys(serviceMixin).forEach(function(method) {
if (typeof this[method] !== 'undefined') {
throw new Error(
'Object to mix into already has "' + method + '" property defined!'
);
}
this[method] = serviceMixin[method];
}, target);
target[priv.service] = null;
target[priv.name] = name;
target[priv.methods] = methods || [];
target[priv.streams] = streams || [];
target[priv.events] = events || [];
return target;
}
};
})(self);

View file

@ -0,0 +1,99 @@
/* global bridge,
streamClient,
Thread
*/
(function(exports) {
'use strict';
const debug = 0 ?
(arg1, ...args) => console.log(`[ConversationClient] ${arg1}`, ...args):
() => {};
const mark = 0 ?
(...args) => exports.performance.mark(`[ConversationClient] ${args}`):
() => {};
const priv = {
client: Symbol('client'),
appInstanceId: Symbol('appInstanceId')
};
/**
* Name of the service that is responsible for managing conversation.
* @type {string}
*/
const SERVICE_NAME = 'conversation-service';
/**
* ConversationClient is a wrapper around bridge client connected to the
* bridge service hosted in a Shared Worker.
* @type {Object}
*/
var ConversationClient = {
/**
* Reference to active bridge client instance.
* @type {Client}
*/
[priv.client]: null,
/**
* Unique identifier of app instance where client resides in.
* @type {string}
*/
[priv.appInstanceId]: null,
/**
* Initialized conversation service client bridge.
* @param {string} appInstanceId Unique identifier of app instance where
* client resides in.
*/
init(appInstanceId) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}
this[priv.client] = bridge.client({
service: SERVICE_NAME,
endpoint: new SharedWorker(
'/services/js/conversation/conversation_service.js'
),
timeout: false
}).plugin(streamClient);
this[priv.appInstanceId] = appInstanceId;
},
/**
* Retrieves all conversations and drafts.
* @param {Function} onConversationRetrieved Callback that is called for
* every retrieved conversation.
* @returns {Promise} Promise is resolved once all conversations are
* retrieved and rejected if conversation stream is closed unexpectedly.
*/
getAllConversations(onConversationRetrieved) {
mark('start retrieving conversations');
var getAllConversationsStream = this[priv.client].stream(
'getAllConversations', this[priv.appInstanceId]
);
var index = 0;
getAllConversationsStream.listen((thread) => {
mark(++index, ' conversation retrieved');
debug('Conversation with id %s is retrieved from stream', thread.id);
onConversationRetrieved(new Thread(thread));
});
return getAllConversationsStream.closed.then(() => {
mark('all conversations retrieved');
}).catch((e) => {
console.error('Conversation stream is closed unexpectedly', e);
throw e;
});
}
};
exports.ConversationClient = Object.seal(ConversationClient);
})(window);

View file

@ -0,0 +1,234 @@
/*global BridgeServiceMixin,
Draft,
Drafts,
MozMobileMessageClient
*/
'use strict';
/**
* Type description for the ConversationSummary object that is return from the
* Conversation service.
* @typedef {Object} ConversationSummary
* @property {number} id Unique identifier of the the conversation.
* @property {string} body Short content of the last message in the
* conversation.
* @property {Array.<string>} participants List of the conversation
* participants.
* @property {Date} timestamp Conversation timestamp.
* @property {{ hasUnread: boolean, hasNewError: boolean }} status Conversation
* status that indicates whether conversation has unread messages or/and failed
* messages that user hasn't seen yet.
* @property {string} lastMessageType Type of the last message in the
* conversation. Can be either 'sms' or 'mms'.
* @property {Draft} draft If conversation is draft or has draft, then this
* field contains appropriate draft information, otherwise it's null.
*/
[
['BridgeServiceMixin', '/services/js/bridge_service_mixin.js'],
[
'MozMobileMessageClient',
'/services/js/moz_mobile_message/moz_mobile_message_client.js'
],
['Drafts', '/services/js/drafts.js']
].forEach(([dependencyName, dependencyPath]) => {
if (!(dependencyName in self)) {
importScripts(dependencyPath);
}
});
(function(exports) {
const debug = 0 ?
(arg1, ...args) => console.log(`[ConversationService] ${arg1}`, ...args):
() => {};
// We use @-directive to override marker context that is scoped by
// SharedWorker lifetime by default, but we need it to be aligned with the
// main app context.
const mark = 0 ?
(...args) => exports.performance.mark(
`[ConversationService] ${args}@sms.gaiamobile.org`
):
() => {};
const SERVICE_CONTRACT = Object.freeze({
name: 'conversation-service',
streams: Object.freeze(
['getAllConversations', 'getMessagesForConversation']
),
methods: Object.freeze([
'deleteConversations', 'deleteMessages', 'markConversationsAs',
'getConversationSummary', 'getMessage', 'findConversationFromAddress'
]),
events: Object.freeze(['message-change'])
});
function draftToConversationSummary(draft) {
var body = Array.isArray(draft.content) ?
draft.content.find((content) => typeof content === 'string') : '';
return {
id: +draft.id,
participants: Array.isArray(draft.recipients) ? draft.recipients : [],
body: body,
timestamp: new Date(draft.timestamp),
status: { hasUnread: false, hasNewError: false },
lastMessageType: draft.type || 'sms',
draft: new Draft(draft)
};
}
function threadToConversationSummary(thread) {
return {
id: thread.id,
participants: thread.participants,
body: thread.body,
timestamp: thread.timestamp,
status: { hasUnread: thread.unreadCount > 0, hasNewError: false },
lastMessageType: thread.lastMessageType,
draft: Drafts.byThreadId(thread.id)
};
}
var ConversationService = {
/**
* Initializes our service using the Service mixin's initService.
*/
init() {
this.initService();
},
/**
* Stream that returns everything needed to display conversations in the
* inbox view.
* @param {ServiceStream.<ConversationSummary>} serviceStream The stream to
* use in the implementation: it will be passed automatically by the Bridge
* library.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
*/
getAllConversations(serviceStream, appInstanceId) {
mark('start retrieving conversations');
var getThreadsStream = MozMobileMessageClient.forApp(
appInstanceId
).getThreads();
// If conversation stream is cancelled we should cancel internal threads
// stream as well.
serviceStream.cancel = (reason) => {
debug('getAllConversations stream is cancelled: %s', reason);
return getThreadsStream.cancel(reason);
};
var draftsPromise = Drafts.request().then(() => {
// Return draft list that is sorted by timestamp in inverse order.
return Drafts.getAllThreadless().sort(
(draftA, draftB) => draftB.timestamp - draftA.timestamp
);
});
// Assume that we get the data in an inverse sort order.
var index = 0;
getThreadsStream.listen((thread) => {
mark(++index, ' conversation retrieved');
debug('Retrieved conversation with id %s', thread.id);
draftsPromise.then((drafts) => {
// Flush drafts that are created earlier than current thread.
while(drafts.length && drafts[0].timestamp >= thread.timestamp) {
serviceStream.write(draftToConversationSummary(drafts.shift()));
}
serviceStream.write(threadToConversationSummary(thread));
});
});
return getThreadsStream.closed.then(() => draftsPromise).then(
(drafts) => {
for(var draft of drafts) {
serviceStream.write(draftToConversationSummary(draft));
}
mark('All conversations retrieved');
serviceStream.close();
}).catch((e) => {
// Pass error object once the following issue is fixed:
// https://github.com/gaia-components/threads/issues/74
serviceStream.abort(`[${e.name}] ${e.message || ''}`);
});
},
/**
* Stream that returns messages in a conversation, with all suitable
* information to display in the conversation view.
* @param {ServiceStream.<MessageSummary>} The stream to use in the
* implementation: it will be passed automatically by the Bridge library.
* @param {Number} conversationId Conversation to retrieve.
*/
getMessagesForConversation(stream, conversationId) {},
/**
* Returns all information related to a single conversation, to display in
* the conversation view.
* @param {Number} conversationId Conversation to retrieve.
* @returns {ConversationSummary}
*/
getConversationSummary(conversationId) {},
/**
* Deletes one or several conversations, including their messages.
* @param {Array.<Number>} conversationIds ID of the conversations to be
* deleted.
*/
deleteConversations(conversationIds) {},
/**
* Deletes one or several messages.
* @param {Array.<Number>} messageIds ID of the messages to be deleted.
*/
deleteMessages(messageIds) {},
/**
* Marks a conversation as read or unread.
* @param {Object} param
* @param {Array.<Number>} param.conversations Conversation IDs to be
* marked.
* @param {Boolean} param.asRead Specifies whether we mark as read of
* unread.
*/
markConversationsAs({conversations, asRead}) {},
/**
* Fetches a message information.
* @param {Number} messageId The message ID to retrieve.
* @returns {Message} All message information, including contact, delivery,
* read reports, size, timestamps, etc.
*/
getMessage(messageId) {},
/**
* Finds a conversation from an address (that is: a phone number or an
* email).
* @param {String} address This is either a phone number or an email, to
* match an existing conversation from.
* @returns {Number?} The id of the conversation, or null if not found.
*/
findConversationFromAddress(address) {}
};
exports.ConversationService = Object.seal(
BridgeServiceMixin.mixin(
ConversationService, SERVICE_CONTRACT.name, SERVICE_CONTRACT
)
);
// Automatically init service if it's run inside worker.
if (!self.document) {
ConversationService.init();
}
})(self);

View file

@ -0,0 +1,266 @@
/*global asyncStorage,
InterInstanceEventDispatcher,
Utils,
EventDispatcher
*/
(function(exports) {
'use strict';
[
['asyncStorage', '/shared/js/async_storage.js'],
['EventDispatcher', '/shared/js/event_dispatcher.js'],
['Utils', '/views/shared/js/utils.js']
].forEach(([dependencyName, dependencyPath]) => {
if (!(dependencyName in self)) {
importScripts(dependencyPath);
}
});
var draftIndex = new Map();
var deferredDraftRequest = null;
/**
* Drafts
*
* A collection of active Draft objects, indexed by thread id.
*/
var Drafts = {
/**
* size
*
* Returns the number of Draft Lists.
*
* There is one Draft.List per Thread and
* one Draft.List for all threadless Drafts.
*
* @return {Number} Maps to draftIndex size property.
*/
get size() {
return draftIndex.size;
},
/**
* Initialize the InterInstanceEventDispatcher and relay the draft
* saved/deleted events from another instance.
*/
init() {
InterInstanceEventDispatcher.on('draft-saved', (draft) =>
this.request(true).then(() => this.emit('saved', draft))
);
InterInstanceEventDispatcher.on('draft-deleted', (draft) =>
this.request(true).then(() => this.emit('deleted', draft))
);
},
/**
* Pushes a Draft instance to the Drafts in-memory collection and commits
* it to the persistent storage.
* @param {Draft} draft Draft instance.
* @returns {Drafts} Returns the Drafts object.
*/
add: function(draft) {
if (!draft || !(draft instanceof Draft)) {
throw new Error('Draft should be valid Draft instance.');
}
// Remove existing draft.
this.delete(draft);
var drafts = draftIndex.get(draft.threadId) || [];
drafts.push(draft);
draftIndex.set(draft.threadId, drafts);
this.emit('saved', draft);
InterInstanceEventDispatcher.emit('draft-saved', draft);
return this;
},
/**
* Deletes a draft record from Drafts in-memory collection.
* @param {Draft} draft Draft instance to delete.
* @returns {Drafts} Returns the Drafts object.
*/
delete: function(draft) {
if (!draft || !(draft instanceof Draft)) {
throw new Error('Draft should be valid Draft instance.');
}
var drafts = draftIndex.get(draft.threadId);
if (!drafts || !drafts.length) {
return this;
}
// Flag to track that draft was really removed.
var isDeleted = false;
// For drafts bound to thread we should remove all drafts we have, but for
// thread-less drafts we should remove draft with the same id only.
if (draft.threadId) {
drafts.length = 0;
isDeleted = true;
} else {
var indexToRemove = drafts.findIndex(
(storedDraft) => storedDraft.id === draft.id
);
if (indexToRemove > -1) {
isDeleted = true;
drafts.splice(indexToRemove, 1);
}
}
if (isDeleted) {
this.emit('deleted', draft);
InterInstanceEventDispatcher.emit('draft-deleted', draft);
}
return this;
},
/**
* Returns draft for the specified thread id or null if it doesn't exist.
* @param {Number} threadId Thread id to recall draft for.
* @returns {Draft}
*/
byThreadId: function(threadId) {
if (!threadId) {
throw new Error('Thread Id is not defined');
}
var threadDrafts = draftIndex.get(threadId);
return threadDrafts && threadDrafts.length ?
threadDrafts[threadDrafts.length - 1] : null;
},
/**
* Returns the draft object with the specified id.
* @param {Number|String} id Id of the draft to return.
* @returns {Draft} Found draft instance or null if nothing is found.
*/
byDraftId: function(id) {
// Ensure a number is used for the comparison, as this value may come from
// a dataset property.
id = +id;
for (var draft of this.getAllThreadless()) {
if (draft.id === id) {
return draft;
}
}
return null;
},
/**
* Returns iterator for all available drafts. For the drafts associated with
* the thread, only the latest draft is returned.
*/
getAll: function* () {
for (var [threadId, drafts] of draftIndex) {
// For drafts with associated thread we consider only the latest draft.
yield* threadId ? drafts.slice(-1) : drafts;
}
},
/**
* Returns list of all thread less drafts.
* @returns {Array.<Draft>}
*/
getAllThreadless: function () {
return draftIndex.get(null) || [];
},
/**
* clear
*
* Delete drafts from the map.
*
* @return {Drafts} return the Drafts object.
*/
clear: function() {
draftIndex = new Map();
deferredDraftRequest = null;
return this;
},
/**
* Stores drafts that are held in memory to local storage.
*/
store: function() {
var defer = Utils.Promise.defer();
asyncStorage.setItem('draft index', [...draftIndex], () => {
defer.resolve();
});
return defer.promise;
},
/**
* Requests drafts from asyncStorage or in-memory cache. Result is cached.
* @param {boolean?} force Indicates whether we should respect already
* cached result or _force_ asyncStorage request once again.
* @returns {Promise} Promise that resolves when all drafts are retrieved
* from persistent storage.
*/
request: function(force) {
// Loading from storage only happens once or when specifically requested
// with force parameter.
if (deferredDraftRequest && !force) {
return deferredDraftRequest.promise;
}
deferredDraftRequest = Utils.Promise.defer();
asyncStorage.getItem('draft index', function (records) {
// Convert every plain JS draft object into Draft "class" instance.
// record[0] is the threadId or null key, record[1] is the array of
// draft objects associated with that threadId or null key.
records && records.forEach((record) => {
record[1] = record[1].map((draft) => new Draft(draft));
});
draftIndex = new Map(records || []);
deferredDraftRequest.resolve();
});
return deferredDraftRequest.promise;
}
};
exports.Drafts = EventDispatcher.mixin(Drafts, ['saved', 'deleted']);
/**
* Draft
*
* A message-like object containing unsent
* message content to be stored temporarily
* in a Drafts collection.
*
* @param {Object} draft Draft or empty object.
*/
function Draft(opts) {
var draft = opts || {};
if (draft.id && typeof draft.id !== 'number') {
throw new Error('Draft id must be a number');
}
this.id = draft.id || guid();
this.recipients = draft.recipients || [];
this.content = draft.content || [];
this.subject = draft.subject || '';
this.timestamp = +draft.timestamp || Date.now();
this.threadId = draft.threadId || null;
this.type = draft.type;
this.isEdited = false;
}
function guid() {
return +(Date.now() + '' + (Math.random() * 1000 | 0));
}
exports.Draft = Draft;
}(this));

View file

@ -0,0 +1,715 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global EventDispatcher,
Promise,
Settings,
SMIL,
Threads,
Utils
*/
/*exported MessageManager */
/**
* MessageManager wraps around {@link http://mdn.io/MozMobileMessageManager}
* that makes access to SMS/MMS related operations (sending, handling
* read/delivery reports etc.) and respective events easier.
* @module MessageManager
*/
'use strict';
(function(exports) {
var MessageManager = {
/**
* Initializes MessageManager and subscribes for required
* {@link http://mdn.io/MozMobileMessageManager} events.
* Should be always called before calling any other MessageManager methods.
*/
init: function mm_init() {
this._mozMobileMessage = navigator.mozMobileMessage;
this._mozMobileMessage.addEventListener(
'received', this.onMessageReceived.bind(this)
);
this._mozMobileMessage.addEventListener(
'sending', this.onMessageSending.bind(this)
);
this._mozMobileMessage.addEventListener(
'sent', this.onMessageSent.bind(this)
);
this._mozMobileMessage.addEventListener(
'failed', this.onMessageFailed.bind(this)
);
this._mozMobileMessage.addEventListener(
'readsuccess', this.onReadSuccess.bind(this)
);
this._mozMobileMessage.addEventListener(
'deliverysuccess', this.onDeliverySuccess.bind(this)
);
this._mozMobileMessage.addEventListener(
'deleted', this.onDeleted.bind(this)
);
},
/**
* Event occurs when an SMS or MMS enters the send flow.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onMessageSending: function mm_onMessageSending(e) {
Threads.registerMessage(e.message);
this.emit('message-sending', { message: e.message });
},
/**
* Specifies an event listener to receive "failed" events. These events occur
* when sending an MMS or SMS has failed.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onMessageFailed: function mm_onMessageFailed(e) {
this.emit('message-failed-to-send', { message: e.message });
},
/**
* Specifies an event listener to receive "deliverysuccess" events. These
* events occur when an SMS or MMS is delivered.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onDeliverySuccess: function mm_onDeliverySuccess(e) {
this.emit('message-delivered', { message: e.message });
},
/**
* Specifies an event listener to receive "read" events. These events
* occur when MMS is read by recipient (read status isn't supported by SMS).
* @param {MozMmsEvent} e Instance of {@link http://mdn.io/MozMmsEvent}.
*/
onReadSuccess: function mm_onReadSuccess(e) {
this.emit('message-read', { message: e.message });
},
/**
* Specifies an event listener to receive "sent" events. These events occur
* when an SMS or MMS is sent.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onMessageSent: function mm_onMessageSent(e) {
this.emit('message-sent', { message: e.message });
},
/**
* Specifies an event listener to receive "received" events. These events
* occur when an SMS or MMS is received. Event handler executed when new SMS
* or MMS is received.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onMessageReceived: function mm_onMessageReceived(e) {
var message = e.message;
if (message.messageClass && message.messageClass === 'class-0') {
return;
}
// Here we can only have one sender, so deliveryInfo[0].deliveryStatus =>
// message status from sender. Ignore 'pending' messages that are received
// this means we are in automatic download mode.
if (message.delivery === 'not-downloaded' &&
message.deliveryInfo[0].deliveryStatus === 'pending') {
return;
}
Threads.registerMessage(message);
this.emit('message-received', { message: message });
},
/**
* Event handler executed when message is deleted.
* @param {MozSmsEvent|MozMmsEvent} e Instance of
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
*/
onDeleted: function(e) {
// The method verifies that some threads have been deleted.
if (e.deletedThreadIds && e.deletedThreadIds.length) {
this.emit('threads-deleted', {
ids: e.deletedThreadIds
});
}
},
/**
* Obtains all the treads and takes a set of callback functions as an
* argument.
* @param {Object} options A set of callback functions as described below:-
* @param {function) options.each: callback function invoked for each message.
* @param {function} options.end: callback function run when cursor is "done".
* @param {function} options.done: callback function run when we iterated
* through all the threads or consumer requested to do so.
*/
getThreads: function mm_getThreads(options) {
var each = options.each;
var end = options.end;
var done = options.done;
var cursor = null;
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
// is resolved.
try {
cursor = this._mozMobileMessage.getThreads();
} catch(e) {
console.error('Error occurred while retrieving threads:', e.name, e);
end && end();
done && done();
return;
}
cursor.onsuccess = function onsuccess() {
if (this.result) {
// Invoking callback on each thread.
each && each(this.result);
this.continue();
return;
}
// Invoking terminating callbacks.
end && end();
done && done();
};
cursor.onerror = function onerror() {
console.error('Reading the database. Error: ' + this.error.name);
done && done();
};
},
/**
* Method used to access message having a given id.
* @param {number} id The id of the message we wish to get.
* @return {DOMRequest<SmsMessage|MmsMessage>}
*/
getMessage: function mm_getMsg(id) {
return this._mozMobileMessage.getMessage(id);
},
/**
* Method to get the MMS object having a given id.
* @param {number} id The id of the MMS we wish to get.
* @return {DOMRequest<MmsMessage>}
*/
retrieveMMS: function mm_retrieveMMS(id) {
return this._mozMobileMessage.retrieveMMS(id);
},
/**
* Obtains all the messages using a given filter.
* @param {Object} options A set of callback functions as described below:-
* @param {function} options.each callback function invoked for each message.
* @param {function} options.end callback function run when cursor is "done".
* @param {function} options.endArgs specify arguments for the "end" callback.
* @param {function} options.done callback function invoked when we iterated
* through all the messages or consumer requested to do so.
* @param {function} options.filter a MobileMessageFilter or similar object.
* @param {boolean} invert option to invert the selection.
*/
getMessages: function mm_getMgs(options) {
var each = options.each;
var invert = options.invert;
var end = options.end;
var endArgs = options.endArgs;
var done = options.done;
var filter = options.filter;
var cursor = this._mozMobileMessage.getMessages(filter, !invert);
cursor.onsuccess = function onsuccess() {
if (!this.done) {
var shouldContinue = true;
if (each) {
shouldContinue = each(this.result);
}
// if each returns false the iteration stops.
if (shouldContinue !== false) { // if this is undefined this is fine.
this.continue();
} else {
done && done();
}
} else {
end && end(endArgs);
done && done();
}
};
cursor.onerror = function onerror() {
var msg = 'Reading the database. Error: ' + this.error.name;
console.log(msg);
done && done();
};
},
/**
* Sanitizes the "serviceId". It works as follows:-
* 0 is a valid value so we need to take care to not consider it as a falsy
* value. We want to return null for anything that's not a number or a string
* containing a number.
* @param {number|string} id The service ID.
* @private
* @return {?number}
*/
_sanitizeServiceId: function mm_sanitizeServiceId(serviceId) {
if (serviceId == null || // null or undefined
isNaN(+serviceId)) {
serviceId = null;
} else {
serviceId = +serviceId;
}
return serviceId;
},
/**
* Gets send options for the specified "serviceId".
* @param {number|string} id The service ID.
* @private
* @return {{ serviceId: number|string }}
*/
_getSendOptionsFromServiceId: function mm_gSOFSI(serviceId) {
var sendOpts;
if (serviceId != null && // not null, not undefined
Settings.hasSeveralSim()) {
sendOpts = { serviceId: serviceId };
}
return sendOpts;
},
/**
* Sends an SMS based on the specified parameters object
* @param {Object} opts Options object that contains :-
* - recipients (string or array of string): contains the list of recipients
* for this message.
* - content (string): the message's body.
* - serviceId (optional long or string): the SIM serviceId we use to send the
* message.
* - onsuccess (optional function): will be called when one SMS has been
* sent successfully, with the request's result as argument. Can be called
* several times.
* - onerror (optional function): will be called when one SMS transmission
* failed, with the error object as argument. Can be called several times.
* - oncomplete (optional function): will be called when all messages have
* been sent. It's argument will have the following properties:
* + hasError (boolean): whether we had at least one error
* + return (array): each item is an object with the following properties:
* . success (boolean): whether this is a success or an error.
* . result (request's result): the request's result object.
* . recipient (string): the recipient used for this transmission.
*/
sendSMS: function mm_send(opts) {
var recipients = opts.recipients || [],
content = opts.content,
serviceId = this._sanitizeServiceId(opts.serviceId),
onsuccess = opts.onsuccess,
onerror = opts.onerror,
oncomplete = opts.oncomplete;
if (!Array.isArray(recipients)) {
recipients = [recipients];
}
// The returned value is not a DOM request!
// Instead, It's an array of DOM requests.
var i = 0;
var requestResult = { hasError: false, return: [] };
// Getting serviceId field in sendOpts.
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
var requests = this._mozMobileMessage.send(recipients, content, sendOpts);
var numberOfRequests = requests.length;
requests.forEach(function(request, idx) {
request.onsuccess = function onSuccess(event) {
// Callback functions executed message has been successfully sent.
onsuccess && onsuccess(event.target.result);
requestResult.return.push({
success: true,
result: event.target.result,
recipient: recipients[idx]
});
// Last callback, oncomplete, executed at the end of the loop.
if (i === numberOfRequests - 1) {
oncomplete && oncomplete(requestResult);
}
i++;
};
request.onerror = function onError(event) {
console.error('Error Sending: ' + JSON.stringify(event.target.error));
onerror && onerror(event.target.error);
requestResult.hasError = true;
requestResult.return.push({
success: false,
code: event.target.error,
recipient: recipients[idx]
});
if (i === numberOfRequests - 1) {
oncomplete && oncomplete(requestResult);
}
i++;
};
});
},
/**
* Sends an MMS based on the specified parameters object
* @param {Object} opts is the options field with the following properties:
* - recipients (string or array of string): recipients for this message
* - subject (optional string): subject for this message
* - content (array of SMIL slides): this is the content for the message (see
* ConversationView for more information)
* - serviceId (optional long or string): the SIM that should be used for
* sending this message. If this is not the current default configuration
* for sending MMS, then we'll first switch the configuration to this
* serviceId, and only then send the message. That means that the "sending"
* event will come quite late in this case.
* - onsuccess (optional func): called only once, even for several recipients,
* when the message is successfully sent.
* - onerror (optional func): called only once if there is an error.
*/
sendMMS: function mm_sendMMS(opts) {
var request;
var recipients = opts.recipients,
subject = opts.subject,
content = opts.content,
serviceId = opts.serviceId = this._sanitizeServiceId(opts.serviceId),
onsuccess = opts.onsuccess,
onerror = opts.onerror;
if (!Array.isArray(recipients)) {
recipients = [recipients];
}
var message = SMIL.generate(content);
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
request = this._mozMobileMessage.sendMMS({
receivers: recipients,
subject: subject,
smil: message.smil,
attachments: message.attachments
}, sendOpts);
request.onsuccess = function onSuccess(event) {
onsuccess && onsuccess(event.target.result);
};
request.onerror = function onError(event) {
onerror && onerror(event.target.error);
};
},
/**
* Resends message with the given options.
* @param {Object} opts Options is the object with the following properties:-
* - message (object): the message object containing all the information
* depending on the type of the message which can be 'sms' or 'mms'.
* - onsuccess (optional function): will be called when the message has been
* sent successfully, with the request's result as argument. Can be called
* several times.
* - onerror (optional function): will be called when the message transmission
* failed, with the error object as argument. Can be called several times.
*/
resendMessage: function mm_resendMessage(opts) {
var message = opts.message;
if (!message) {
throw new Error('Message to resend is not defined.');
}
var serviceId = Settings.getServiceIdByIccId(message.iccId);
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
var onsuccess = opts.onsuccess;
var onerror = opts.onerror;
var request;
if (message.type === 'sms') {
request = this._mozMobileMessage.send(
message.receiver, message.body, sendOpts);
}
if (message.type === 'mms') {
request = this._mozMobileMessage.sendMMS({
receivers: message.receivers,
subject: message.subject,
smil: message.smil,
attachments: message.attachments
}, sendOpts);
}
request.onsuccess = function onSuccess(evt) {
MessageManager.deleteMessages(message.id);
onsuccess && onsuccess(evt.target.result);
};
request.onerror = function onError(evt) {
MessageManager.deleteMessages(message.id);
onerror && onerror(evt.target.error);
};
},
/**
* Deletes messages using the message id provided.
* @param {number} id The ID of the message to be deleted.
* @return {Promise}
*/
deleteMessages: function mm_deleteMessages(id) {
// As DOMRequest doesn't have "catch" method we should use "then" with
// "null" for the success callback to handle reject case only.
return this._mozMobileMessage.delete(id).then(null, (e) => {
// TODO: If the messages could not be deleted completely, conversation
// view will also update without notification currently. May need more
// information for user that the messages were not removed completely.
// See bug #1045666 for details.
console.error(`Deleting in the database. [${e.name}] ${e.message || ''}`);
return null;
});
},
/**
* Marks a thread as read or unread depending on value of isRead as true or
* false respectively.
* @param {number|string} threadId The ID of the thread to be marked as read.
* @param {boolean} isRead If "true" then message is to be marked as read
* otherwise message is to be marked as unread.
*/
markThreadRead: function mm_markThreadRead(threadId, isRead = true) {
// Create filter object to mark all unread messages of the specified thread.
var filter = {};
filter.threadId = +threadId;
filter.read = !isRead;
var messagesUnreadIDs = [];
// Building the options field in getMessages() with above filter.
var changeStatusOptions = {
each: function addUnreadMessage(message) {
// First build up array of Unread message ID.
messagesUnreadIDs.push(message.id);
return true;
},
filter: filter,
invert: true,
end: function handleUnread() {
// end with marking each of them as read.
MessageManager.markMessagesRead(messagesUnreadIDs, isRead);
}
};
MessageManager.getMessages(changeStatusOptions);
},
/**
* Marks messages with specified ids as read.
* @param {Array<Number>} list List of all the message IDs to be changed to
* read.
* @param {boolean} isRead Pass true if message is to be marked as read and
* false if message is to be marked as unread.
*/
markMessagesRead: function mm_markMessagesRead(list, isRead = true) {
if (!this._mozMobileMessage || !list.length) {
return;
}
// We chain the calls to the API in a way that we make no call to
// 'markMessageRead' until a previous call is completed. This way any
// other potential call to the API, like the one for getting a message
// list, could be done within the calls to mark the messages as read.
var id = list.pop();
// TODO: Third parameter of markMessageRead is return read request.
// Here we always return read request for now, but we can let user
// decide to return request or not in Bug 971658.
var req = this._mozMobileMessage.markMessageRead(id, isRead, true);
// isRead == false i.e mark as unread case, marking only one message
// as unread is sufficient.
req.onsuccess = (function onsuccess() {
if (!list.length || !isRead) {
return;
} else if (isRead) {
this.markMessagesRead(list, isRead);
}
}).bind(this);
req.onerror = function onerror() {
console.error(
'Error while marking message %d as read: %s', id, this.error.name
);
};
},
/**
* Method used to get the information necessary to create a multipart SMS for
* a given text.
* @param {string} text The text used to create the SMS.
* @return {Promise<MozSmsSegmentInfo>} Instance of
* {@link http://mdn.io/MozSmsSegmentInfo}
*/
getSegmentInfo: function mm_getSegmentInfo(text) {
if (!(this._mozMobileMessage &&
this._mozMobileMessage.getSegmentInfoForText)) {
return Promise.reject(new Error('mozMobileMessage is unavailable.'));
}
var defer = Utils.Promise.defer();
var request = this._mozMobileMessage.getSegmentInfoForText(text);
request.onsuccess = function onsuccess(e) {
defer.resolve(e.target.result);
};
request.onerror = function onerror(e) {
defer.reject(e.target.error);
};
return defer.promise;
},
/**
* This method checks whether message is received from a sender with specified
* number or sent to this particular number. (in case of MMS only 1-to-1,
* non-group MMS's are counted).
* It's used to differentiate from broadcast messages ---> group MMS.
* @param {number} number Phone number.
* @param {Object} message A composite object having details about message
* delivery, type, sender and receiver.
* @private
* @return {boolean}
*/
_isMessageBelongTo1to1Conversation:
function isMessageBelongTo1to1Conversation(number, message) {
var isIncoming = message.delivery === 'received' ||
message.delivery === 'not-downloaded';
// if it is a received message, it is a candidate
// we still need to test the sender in case the user filters with his own
// number, because we would get all the received messages in this case.
if (isIncoming) {
return Utils.probablyMatches(message.sender, number);
} else {
switch (message.type) {
case 'sms':
// in case of sent messages and sms, we test if the receiver match the
// filter, to filter out other sent messages in the case user is
// sending message to himself
return Utils.probablyMatches(message.receiver, number);
case 'mms':
return message.receivers.length === 1 &&
Utils.probablyMatches(message.receivers[0], number);
default:
console.error('Got an unknown message type: ' + message.type);
return false;
}
}
},
/**
* Find a SMS/MMS thread from a number.
* @param {number} number Phone number to find thread for.
* @return {Promise<number>} that is resolved to a threadId or rejected if
* not found
*/
findThreadFromNumber: function mm_findThread(number) {
function checkCandidate(message) {
var isMessageInThread = MessageManager.
_isMessageBelongTo1to1Conversation(number, message);
if (isMessageInThread) {
threadId = message.threadId;
return false; // found the message, stop iterating
}
}
var threadId = null;
var deferred = Utils.Promise.defer();
MessageManager.getMessages({
filter: { numbers: [number] },
each: checkCandidate,
done: function() {
if (threadId == null) {
deferred.reject(new Error('No thread found for number: ' + number));
return;
} else {
deferred.resolve(threadId);
return;
}
}
});
return deferred.promise;
},
/**
* This method ensures that threads have been registered in the in-memory
* cache. In the case it has, the promise returned resolves immediately. If
* not, it loads the last message from the thread and creates a new entry in
* cache based on it. It can happen when application loaded from non default
* page and Thread cache hasn't been populated.
* @param {number|string} threadId The ID of the thread we wish to register.
* @return {Promise}
*/
ensureThreadRegistered(threadId) {
var getOneMessageForThread = (threadId) => {
var defer = Utils.Promise.defer();
this.getMessages({
each: function(message) {
defer.resolve(message);
return false;
},
done: function() {
defer.reject(new Error('found no message'));
},
filter: { threadId: threadId }
});
return defer.promise;
};
// Checking whether Thread cache has this thread.
if (Threads.has(threadId)) {
return Promise.resolve();
}
return getOneMessageForThread(threadId).then(
(message) => Threads.registerMessage(message)
);
}
};
Object.defineProperty(exports, 'MessageManager', {
get: function () {
delete exports.MessageManager;
exports.MessageManager = EventDispatcher.mixin(MessageManager, [
'message-sending', 'message-failed-to-send', 'message-delivered',
'message-read', 'message-sent', 'message-received', 'threads-deleted'
]);
return exports.MessageManager;
},
configurable: true,
enumerable: true
});
})(window);

View file

@ -0,0 +1,125 @@
/* global bridge */
(function(exports) {
'use strict';
/**
* Name of the service that is responsible for messaging functionality.
* @type {string}
*/
const SERVICE_NAME = 'messaging-service';
/**
* Disable default client timeout from bridge by setting the timeout to false.
* @type {number|boolean}
*/
const TIMEOUT = false;
/**
* Reference to active bridge client instance.
* @type {Client}
*/
var client;
/**
* Unique identifier of app instance where client resides in.
* @type {string}
*/
var appInstanceId;
var MessagingClient = {
/**
* Initializes connection to MessagingService hosted in a SharedWorker.
* @param {string} applicationInstanceId Unique identifier of app instance
* where client resides in.
*/
init(applicationInstanceId) {
if (!applicationInstanceId) {
throw new Error('AppInstanceId is required!');
}
var serviceEndpoint = new SharedWorker(
'/services/js/messaging/messaging_service.js'
);
appInstanceId = applicationInstanceId;
client = bridge.client({
service: SERVICE_NAME,
endpoint: serviceEndpoint,
timeout: TIMEOUT
});
},
/**
* Sends an SMS based on the specified parameter object.
* @param {Object} options Options object that contains :-
* - recipients (string or array of string): contains the list of
* recipients for this message
* - content (string): the message's body
* - serviceId (optional long or string): the SIM serviceId we use to send
* the message
* - onsuccess (optional function): will be called when one SMS has been
* sent successfully, with the request's result as argument. Can be called
* several times.
* - onerror (optional function): will be called when one SMS transmission
* failed, with the error object as argument. Can be called several times.
* - oncomplete (optional function): will be called when all messages have
* been sent. Its argument will have the following properties:
* + hasError (boolean): whether we had at least one error
* + return (array): each item is an object with the following properties:
* . success (boolean): whether this is a success or an error
* . result (request's result): the request's result object
* . recipient (string): the recipient used for this transmission.
* @return {Promise}
*/
sendSMS(options) {
return client.method('sendSMS', options, appInstanceId);
},
/**
* Sends an MMS based on the specified parameter object.
* @param {Object} options Options object is the options field with the
* following properties:
* - recipients (string or array of string): recipients for this message.
* - subject (optional string): subject for this message.
* - content (array of SMIL slides): this is the content for the message
* (see ConversationView for more information).
* - serviceId (optional long or string): the SIM that should be used for
* sending this message. If this is not the current default configuration
* for sending MMS, then we'll first switch the configuration to this
* serviceId, and only then send the message. That means that the
* "sending" event will come quite late in this case.
* - onsuccess (optional func): called only once, even for several
* recipients, when the message is successfully sent.
* - onerror (optional func): called only once if there is an error.
* @return {Promise}
*/
sendMMS(options) {
return client.method('sendMMS', options, appInstanceId);
},
/**
* Resends message with the given opts.
* Takes a formatted message in case you happen to have one.
* @param {Object} message The message object containing all the information
* depending on the type of the message which can be 'sms' or 'mms'.
* @return {Promise}
*/
resendMessage(message) {
return client.method('resendMessage', message, appInstanceId);
},
/**
* Gets the MMS object having a given id.
* @param {number} id The id of the MMS we wish to get.
* @return {Promise<MmsMessage>} A Promise object that handles the success
* or error of the operation.
*/
retrieveMMS(id) {
return client.method('retrieveMMS', id, appInstanceId);
}
};
exports.MessagingClient = Object.freeze(MessagingClient);
})(window);

View file

@ -0,0 +1,197 @@
/* global bridge,
BridgeServiceMixin,
MozMobileMessageClient,
Settings,
SMIL
*/
'use strict';
if (!('BridgeServiceMixin' in self)) {
importScripts('/services/js/bridge_service_mixin.js');
}
if (!('MozMobileMessageClient' in self)) {
importScripts('/services/js/moz_mobile_message/moz_mobile_message_client.js');
}
(function(exports) {
const SERVICE_NAME = 'messaging-service';
const METHODS = Object.freeze([
'sendSMS', 'sendMMS', 'resendMessage', 'retrieveMMS'
]);
function ensureBridge() {
if (!('bridge' in self) || !('service' in bridge)) {
importScripts('/lib/bridge/service.js');
}
}
/**
* 0 is a valid value so we need to take care to not consider it as a falsy
* value. We want to return null for anything that's not a number or a
* string containing a number.
*/
function sanitizeServiceId(serviceId) {
if (serviceId == null || // null or undefined
isNaN(+serviceId)) {
serviceId = null;
} else {
serviceId = +serviceId;
}
return serviceId;
}
function getSendOptionsFromServiceId(serviceId) {
var sendOpts;
if (serviceId != null && // not null, not undefined
Settings.hasSeveralSim()) {
sendOpts = { serviceId: serviceId };
}
return sendOpts;
}
var MessagingService = {
/**
* Initializes our service using the Service mixin's initService and
* requests mozMobileMessage shim as client.
*/
init() {
ensureBridge();
this.initService();
},
/**
* Send sms by using send API shim.
* @param {String|String[]} recipients Contains the list of recipients for
* this message.
* @param {String} content The message's body.
* @param {Number|String} serviceId The SIM serviceId we use to send the
* message.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
* @returns {Promise} Promise with array of the request result.
*/
sendSMS({ recipients, content, serviceId }, appInstanceId) {
recipients = recipients || [];
serviceId = sanitizeServiceId(serviceId);
if (!Array.isArray(recipients)) {
recipients = [recipients];
}
var sendOpts = getSendOptionsFromServiceId(serviceId);
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
var results = recipients.map((recipient) => {
return mobileMessageClient.send(recipient, content, sendOpts).then(
(message) => ({ message: message }), (error) => ({ error: error })
);
});
return Promise.all(results).then((values) =>
values.map((result, idx) => ({
success: !result.error,
result: result.message,
code: result.error,
recipient: recipients[idx]
}))
);
},
/**
* Send mms by using sendMMS API shim.
* @param {String|String[]} recipients Recipients for this message.
* @param {String} subject Subject for this message.
* @param {Object[]} content This is the content for the message (
* see ConversationView for more information).
* @param {Number|String} serviceId The SIM that should be used for
* sending this message. If this is not the current default configuration
* for sending MMS, then we'll first switch the configuration to this
* serviceId, and only then send the message. That means that the
* "sending" event will come quite late in this case.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
* @returns {Promise} DOMRequest promise with success result or error.
*/
sendMMS({ recipients, content, subject, serviceId }, appInstanceId) {
serviceId = sanitizeServiceId(serviceId);
if (!Array.isArray(recipients)) {
recipients = [recipients];
}
var message = SMIL.generate(content);
var sendOpts = getSendOptionsFromServiceId(serviceId);
return MozMobileMessageClient.forApp(appInstanceId).sendMMS({
receivers: recipients,
subject: subject,
smil: message.smil,
attachments: message.attachments
}, sendOpts);
},
/**
* Resend a message by send API shim with existing message information.
* @param {Message} message Target message that need to be resent.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
* @returns {Promise} DOMRequest promise with success result or error.
*/
resendMessage(message, appInstanceId) {
if (!message) {
throw new Error('Message to resend is not defined.');
}
var serviceId = Settings.getServiceIdByIccId(message.iccId);
var sendOpts = getSendOptionsFromServiceId(serviceId);
var request;
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
if (message.type === 'sms') {
request = mobileMessageClient.send(
message.receiver, message.body, sendOpts
);
} else if (message.type === 'mms') {
request = mobileMessageClient.sendMMS({
receivers: message.receivers,
subject: message.subject,
smil: message.smil,
attachments: message.attachments
}, sendOpts);
}
return mobileMessageClient.delete(message.id).then(() => request);
},
/**
* Retrieve a MMS by retrieveMMS API shim by message ID.
* @param {Number} id mms message id for retrieval.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
* @returns {Promise} DOMRequest promise with success result or error.
*/
retrieveMMS(id, appInstanceId) {
return MozMobileMessageClient.forApp(appInstanceId).retrieveMMS(id);
}
};
exports.MessagingService = BridgeServiceMixin.mixin(
MessagingService,
SERVICE_NAME,
{ methods: METHODS }
);
if (!self.document) {
MessagingService.init();
}
})(self);

View file

@ -0,0 +1,61 @@
/* global bridge,
BroadcastChannel */
(function(exports) {
'use strict';
/**
* Name of the service that is responsible for messaging functionality.
* @type {string}
*/
const SERVICE_NAME = 'moz-mobile-connections-shim';
/**
* Disable default client timeout from bridge by setting the timeout to false.
* @type {number|boolean}
*/
const TIMEOUT = false;
/**
* Reference to active bridge client instance.
*
* @type {Client}
*/
var client;
var MozMobileConnectionsClient = {
/**
* Initializes connection to mozMobileConnectionsShim hosted in iframe.
* @param {string} appInstanceId Unique identifier of app instance
* where client resides in.
*/
init(appInstanceId) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
client = bridge.client({
service: SERVICE_NAME,
endpoint: new BroadcastChannel(broadcastChannelName),
timeout: TIMEOUT
});
},
/**
* Switch the default MMS and data to SIM that matches the given iccId.
* @param {string} iccId Id that could map to the target SIM.
* @returns {Promise} Promise of shim's method that returned when sim
* switching is ready or failed.
*/
switchMmsSimHandler(iccId) {
return client.method('switchMmsSimHandler', iccId);
}
};
exports.MozMobileConnectionsClient = Object.freeze(
MozMobileConnectionsClient
);
})(window);

View file

@ -0,0 +1,106 @@
/* global BridgeServiceMixin,
BroadcastChannel,
MozSettingsShim
*/
/* exported MozMobileConnectionsShim */
(function(exports) {
'use strict';
/**
* Name of the service for mozMobileConnections API shim.
* @type {string}
*/
const SERVICE_NAME = 'moz-mobile-connections-shim';
/**
* Array of method names that need to be exposed for API shim.
* @type {Array.<string>}
*/
const METHODS = Object.freeze(['getServiceIdByIccId', 'switchMmsSimHandler']);
var mozMobileConnections = null;
function setMmsSimServiceId(id) {
// DSDS: mms & data are both necessary for connection switch.
MozSettingsShim.set({
'ril.mms.defaultServiceId': id,
'ril.data.defaultServiceId': id
});
}
var MozMobileConnectionsShim = {
init(appInstanceId, conns) {
if (!conns) {
console.error('Invalid mozMobileConnections for shim initialization');
return;
}
mozMobileConnections = conns;
this.initService(
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
);
},
/* Methods */
/**
* Shim for returning the service id that matches the icc id.
* @param {string} iccId Id that could map to the target SIM.
* @returns {?number} matched service id or null if unmatched.
*/
getServiceIdByIccId(iccId) {
var index = Array.prototype.findIndex.call(mozMobileConnections, (conn) =>
iccId === conn.iccId
);
return index > -1 ? index : null;
},
/**
* Switching the default mms/data service id by icc id.
* @param {string} iccId Id that could map to the target SIM.
* @returns {Promise} Promise that returned when sim switching is ready or
* failed.
*/
switchMmsSimHandler(iccId) {
var serviceId = this.getServiceIdByIccId(iccId);
if (serviceId === null) {
return Promise.reject('NoSimCardError');
}
var conn = mozMobileConnections[serviceId];
if (conn.data.state === 'registered') {
// Call resolve directly if state is registered already
return Promise.resolve();
}
return new Promise(function(resolve) {
// Listen to MobileConnections datachange to make sure we can start
// to retrieve mms only when data.state is registered. But we can't
// guarantee datachange event will work in other device.
conn.addEventListener('datachange', function onDataChange() {
if (conn.data.state === 'registered') {
conn.removeEventListener('datachange', onDataChange);
resolve();
}
});
setMmsSimServiceId(serviceId);
});
}
};
exports.MozMobileConnectionsShim = Object.seal(
BridgeServiceMixin.mixin(
MozMobileConnectionsShim,
SERVICE_NAME,
{ methods: METHODS }
)
);
}(this));

View file

@ -0,0 +1,97 @@
/*global bridge,
BroadcastChannel,
streamClient
*/
'use strict';
(function(exports) {
const debug = 0 ?
(arg1, ...args) => console.log(`[MozMobileMessageClient] ${arg1}`, ...args):
() => {};
const priv = {
bridgeClient: Symbol('bridgeClient')
};
/**
* Disable default client timeout from bridge by setting the timeout to false.
* @type {number|boolean}
*/
const TIMEOUT = false;
function MobileMessageClient(endpoint) {
if (!('bridge' in self) || !('client' in bridge)) {
importScripts('/lib/bridge/client.js');
}
if (!('streamClient' in self)) {
importScripts('/lib/bridge/plugins/stream/client.js');
}
this[priv.bridgeClient] = bridge.client({
service: 'moz-mobile-message-shim',
endpoint: endpoint,
timeout: TIMEOUT
}).plugin(streamClient);
}
MobileMessageClient.prototype = {
retrieveMMS(id) {
return this[priv.bridgeClient].method('retrieveMMS', id);
},
getThreads() {
return this[priv.bridgeClient].stream('getThreads');
},
send(recipient, content, sendOpts) {
return this[priv.bridgeClient].method(
'send', recipient, content, sendOpts
);
},
sendMMS(dataOpts, sendOpts) {
return this[priv.bridgeClient].method('sendMMS', dataOpts, sendOpts);
},
delete(id) {
return this[priv.bridgeClient].method('delete', id);
}
};
/**
* List of MobileMessageClient mapped to specific application id.
* @type {Map.<string, MobileMessageClient>}
*/
const mobileMessageClients = new Map();
exports.MozMobileMessageClient = Object.freeze({
forApp(appInstanceId) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}
var mobileMessageClient = mobileMessageClients.get(appInstanceId);
if (!mobileMessageClient) {
mobileMessageClient = new MobileMessageClient(
new BroadcastChannel(
'moz-mobile-message-shim-channel-' + appInstanceId
)
);
mobileMessageClients.set(appInstanceId, mobileMessageClient);
debug(
'Create MobileMessageClient for app instance %s', appInstanceId
);
}
return mobileMessageClient;
},
cleanup() {
mobileMessageClients.clear();
}
});
})(self);

View file

@ -0,0 +1,276 @@
/* global BridgeServiceMixin,
BroadcastChannel
*/
/* exported MozMobileMessageShim */
(function(exports) {
'use strict';
const debug = 0 ?
(arg1, ...args) => console.log(`[MozMobileMessageShim] ${arg1}`, ...args):
() => {};
const mark = 0 ?
(...args) => exports.performance.mark(`[MozMobileMessageShim] ${args}`):
() => {};
/**
* Name of the service for mozMobileMessage API shim.
* @type {string}
*/
const SERVICE_NAME = 'moz-mobile-message-shim';
/**
* Event set that key is corresponding to mozMobileMessage events and value is
* the name that broadcasts to client side.
* @type {Object.<string, string>}
*/
const EVENTS = Object.freeze({
'received': 'message-received',
'sending': 'message-sending',
'sent': 'message-sent',
'failed': 'message-failed-to-send',
'deleted': 'threads-deleted',
'readsuccess': 'message-read',
'deliverysuccess': 'message-delivered'
});
/**
* Array of method names that need to be exposed for API shim.
* @type {Array.<string>}
*/
const METHODS = Object.freeze(['getMessage', 'retrieveMMS', 'send', 'sendMMS',
'delete', 'markMessageRead', 'getSegmentInfoForText']);
/**
* Array of stream names that need to return data (messages/threads) in chunk.
* @type {Array.<string>}
*/
const STREAMS = Object.freeze(['getThreads', 'getMessages']);
var mozMobileMessage = null;
/**
* Clones DOM thread object to plain JS object, so that it's possible to pass it
* via postMessage. Should be removed once bug 1172794 is landed.
* @param {DOMMozMobileMessageThread} thread DOM Message thread instance.
* @returns {Object}
*/
function cloneThread(thread) {
// See nsIDOMMozMobileMessageThread.idl file at:
// http://mxr.mozilla.org/mozilla-central/source/dom/mobilemessage/interfaces
return {
id: thread.id,
body: thread.body,
participants: thread.participants,
timestamp: thread.timestamp,
unreadCount: thread.unreadCount,
lastMessageType: thread.lastMessageType
};
}
var MozMobileMessageShim = {
init(appInstanceId, mobileMessage) {
if (!mobileMessage) {
return;
}
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
mozMobileMessage = mobileMessage;
this.initService(new BroadcastChannel(broadcastChannelName));
Object.keys(EVENTS).forEach((event) => {
mozMobileMessage.addEventListener(
event,
this['on' + capitalize(event)].bind(this)
);
});
debug(
'Listen incoming connections on "%s" broadcast channel',
broadcastChannelName
);
},
/* Events */
onSending(e) {
this.broadcast('message-sending', { message: e.message });
},
onFailed(e) {
this.broadcast('message-failed-to-send', { message: e.message });
},
onDeliverysuccess(e) {
this.broadcast('message-delivered', { message: e.message });
},
onReadsuccess(e) {
this.broadcast('message-read', { message: e.message });
},
onSent(e) {
this.broadcast('message-sent', { message: e.message });
},
onReceived(e) {
this.broadcast('message-received', { message: e.message });
},
onDeleted(e) {
if (e.deletedThreadIds && e.deletedThreadIds.length) {
this.broadcast('threads-deleted', {
ids: e.deletedThreadIds
});
}
},
/* Methods */
getMessage(id) {
return mozMobileMessage.getMessage(id);
},
/**
* Call platform retrieveMMS API to retrieve the MMS by ID.
* @param {Number} id MMS message id.
* @returns {Promise.<void|Error>} return void if MMS is retrieved
* successfully or error while failed.
*/
retrieveMMS(id) {
// Return void instead of message to avoid clone error issue. Remove once
// bug 1172794 is resolved.
return mozMobileMessage.retrieveMMS(id).then(() => {});
},
send(recipient, content, sendOpts) {
return mozMobileMessage.send(recipient, content, sendOpts);
},
sendMMS(dataOpts, sendOpts) {
return mozMobileMessage.sendMMS(dataOpts, sendOpts);
},
delete(id) {
return mozMobileMessage.delete(id);
},
markMessageRead(id, isRead, sendReadReport) {
return mozMobileMessage.markMessageRead(id, isRead, sendReadReport);
},
getSegmentInfoForText(text) {
return mozMobileMessage.getSegmentInfoForText(text);
},
/* Streams */
/**
* Stream wrapper for getThreads API for returning threads in chunk.
* @param {ServiceStream} stream Channel for returning thread.
*/
getThreads(stream) {
mark('start retrieving conversations');
var cursor = null;
var isStreamCancelled = false;
stream.cancel = (reason) => {
debug('getThreads stream is cancelled: %s', reason);
isStreamCancelled = true;
return Promise.resolve();
};
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
// is resolved
try {
cursor = mozMobileMessage.getThreads();
} catch(e) {
console.error('Error occurred while retrieving threads:', e);
// Pass error object once the following issue is fixed:
// https://github.com/gaia-components/threads/issues/74
stream.abort(`[${e.name}] ${e.message || ''}`);
return;
}
var index = 0;
cursor.onsuccess = function onsuccess() {
var result = this.result;
if (result && !isStreamCancelled) {
mark(++index, ' conversation retrieved');
stream.write(cloneThread(result));
this.continue();
return;
}
mark('all conversations retrieved');
stream.close();
};
cursor.onerror = function onerror() {
console.error('Error occurred while reading the database', this.error);
// Pass error object once the following issue is fixed:
// https://github.com/gaia-components/threads/issues/74
stream.abort(`[${this.error.name}] ${this.error.message || ''}`);
};
},
/**
* Stream wrapper for getMessages API for returning messages in chunk.
* @param {ServiceStream} stream Channel for returning message.
* @param {*} options Options for getMessages API.
*/
getMessages(stream, options) {
/*
options {
filter: a MobileMessageFilter or similar object
invert: option to invert the selection
}
*/
var invert = options.invert;
var filter = options.filter;
var cursor = mozMobileMessage.getMessages(filter, !invert);
cursor.onsuccess = function onsuccess() {
if (!this.done) {
stream.write(this.result);
this.continue();
} else {
stream.close();
}
};
cursor.onerror = function onerror() {
var msg = 'Reading the database. Error: ' + this.error.name;
console.error(msg);
stream.abort();
};
stream.cancel = function() {
stream.close();
};
}
};
exports.MozMobileMessageShim = Object.seal(
BridgeServiceMixin.mixin(
MozMobileMessageShim,
SERVICE_NAME, {
methods: METHODS,
streams: STREAMS,
events: Object.keys(EVENTS).map((key) => EVENTS[key])
}
)
);
}(this));

View file

@ -0,0 +1,59 @@
/* global BridgeServiceMixin */
/* exported MozSettingsShim */
(function(exports) {
'use strict';
/**
* Name of the service for mozSettings API shim.
* @type {string}
*/
const SERVICE_NAME = 'moz-settings-shim';
/**
* Array of method names that need to be exposed for API shim.
* @type {Array.<string>}
*/
const METHODS = Object.freeze(['get', 'set']);
var mozSettings = null;
var MozSettingsShim = {
init(settings) {
if (!settings) {
console.error('Invalid settings for shim initialization');
return;
}
mozSettings = settings;
this.initService();
},
/* Methods */
/**
* Shim for mozSettings.createLock().get API.
* @param {String} key Key name for Settings.
* @returns {Promise} Promise that return the settings value from settings DB.
*/
get(key) {
return mozSettings.createLock().get(key);
},
/**
* Shim for mozSettings.createLock().set API.
* @param {Object} settings Settings key and value for settings. It can have
* several key/value pairs inside.
* @returns {Promise} Promise that return saved result.
*/
set(settings) {
return mozSettings.createLock().set(settings);
}
};
exports.MozSettingsShim = Object.seal(
BridgeServiceMixin.mixin(MozSettingsShim, SERVICE_NAME, { methods: METHODS })
);
}(this));

View file

@ -0,0 +1,177 @@
/*global Drafts */
(function(exports) {
'use strict';
var threads = new Map();
var messageMap = new Map();
function Thread(thread) {
var length = Thread.FIELDS.length;
var key;
for (var i = 0; i < length; i++) {
key = Thread.FIELDS[i];
this[key] = thread[key];
}
this.messages = new Map();
}
Thread.prototype.getDraft = function() {
return this.isDraft ?
Drafts.byDraftId(this.id) : Drafts.byThreadId(this.id);
};
Thread.FIELDS = [
'body', 'id', 'lastMessageSubject', 'lastMessageType',
'participants', 'timestamp', 'unreadCount', 'isDraft'
];
Thread.fromMessage = function(record, options) {
var participants = [];
if (typeof record.delivery !== 'undefined') {
if (record.delivery === 'received' ||
record.delivery === 'not-downloaded') {
participants = [record.sender];
} else {
participants = record.receivers || [record.receiver];
}
}
return new Thread({
id: record.threadId,
participants: participants,
body: record.body,
timestamp: record.timestamp,
unreadCount: (options && options.unread) ? 1 : 0,
lastMessageType: record.type || 'sms',
isDraft: false
});
};
Thread.fromDraft = function(draft, options) {
var participants = draft.recipients && draft.recipients.length ?
draft.recipients : [''];
var body = draft.content && draft.content.length ?
draft.content.find(function(content) {
if (typeof content === 'string') {
return true;
}
}) : '';
return new Thread({
id: draft.threadId || draft.id,
participants: participants,
body: body,
timestamp: new Date(draft.timestamp),
unreadCount: (options && options.unread) ? 1 : 0,
lastMessageType: draft.type || 'sms',
isDraft: true
});
};
Thread.create = function(record, options) {
if (record instanceof Thread) {
return record;
}
return record.delivery ?
Thread.fromMessage(record, options) :
Thread.fromDraft(record, options);
};
var Threads = exports.Threads = {
registerMessage: function(message) {
var threadId = +message.threadId;
var messageId = +message.id;
if (!this.has(threadId)) {
this.set(threadId, Thread.create(message));
}
this.get(threadId).messages.set(messageId, message);
messageMap.set(messageId, threadId);
},
unregisterMessage: function(id) {
id = +id;
var threadId = messageMap.get(id);
if (this.has(threadId)) {
this.get(threadId).messages.delete(id);
}
messageMap.delete(id);
},
set: function(id, thread) {
var old, length, key;
id = +id;
if (threads.has(id)) {
// Updates the reference
old = threads.get(id);
length = Thread.FIELDS.length;
for (var i = 0; i < length; i++) {
key = Thread.FIELDS[i];
old[key] = thread[key];
}
return threads;
}
return threads.set(id, new Thread(thread));
},
get: function(id) {
return threads.get(+id);
},
has: function(id) {
return threads.has(+id);
},
delete: function(id) {
id = +id;
var thread = this.get(id);
if (!thread) {
return;
}
threads.delete(id);
var draft = thread.getDraft();
if (draft) {
Drafts.delete(draft).store();
}
},
clear: function() {
threads = new Map();
},
forEach: function(callback) {
threads.forEach(function(v, k) {
callback(v, k);
});
},
keys: function() {
return threads.keys();
},
get size() {
// support: gecko 18 - size might be a function
if (typeof threads.size === 'function') {
return +threads.size();
}
return +threads.size;
}
};
Threads.Messages = {
get(id) {
id = +id;
var threadId = messageMap.get(id);
if (Threads.has(threadId)) {
return Threads.get(threadId).messages.get(id);
}
return null;
}
};
exports.Thread = Thread;
}(this));

View file

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

View file

@ -0,0 +1,221 @@
/*global ActivityClient,
BroadcastChannel,
MocksHelper,
bridge
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
require('/shared/js/event_dispatcher.js');
require('/services/js/activity/activity_client.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel'
]).init();
suite('ActivityClient >', function() {
const APP_INSTANCE_ID = 'fake-app-id';
var clientStub;
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
clientStub = sinon.stub({
on: () => {},
off: () => {},
method: () => {}
});
this.sinon.stub(bridge, 'client').returns(clientStub);
});
teardown(function() {
ActivityClient.offAll();
});
suite('init()', function() {
test('throws if app instance id is not provided', function() {
assert.throws(() => ActivityClient.init());
});
test('bridge client is correctly initialized', function() {
ActivityClient.init(APP_INSTANCE_ID);
sinon.assert.calledOnce(bridge.client);
sinon.assert.calledWith(bridge.client, {
service: 'activity-service',
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
sinon.match.has(
'name',
'activity-service-channel-' + APP_INSTANCE_ID
)
),
timeout: false
});
assert.isFalse(ActivityClient.hasPendingRequest());
});
});
suite('throws in exceptional cases', function() {
setup(function() {
ActivityClient.init(APP_INSTANCE_ID);
});
test('postResult throws if there is no activity request', function(done) {
ActivityClient.postResult('Result').then(() => {
throw new Error('postResult should be rejected!');
}, (e) => {
assert.instanceOf(e, Error);
}).then(done, done);
});
test('postError throws if there is no activity request', function(done) {
ActivityClient.postError('Error').then(() => {
throw new Error('postError should be rejected!');
}, (e) => {
assert.instanceOf(e, Error);
}).then(done, done);
});
});
suite('handling incoming activity requests> ', function() {
setup(function() {
ActivityClient.init(APP_INSTANCE_ID);
});
teardown(function() {
ActivityClient.offAll();
});
test('post error on concurrent request attempt', function() {
var onShareActivityRequest = sinon.stub();
ActivityClient.on('share-activity-request', onShareActivityRequest);
clientStub.on.withArgs('activity-request').yield({
name: 'share',
data: { body: 'text' }
});
assert.isTrue(ActivityClient.hasPendingRequest());
sinon.assert.calledOnce(onShareActivityRequest);
clientStub.on.withArgs('activity-request').yield({
name: 'share',
data: { body: 'text' }
});
sinon.assert.calledOnce(onShareActivityRequest);
sinon.assert.calledWith(
clientStub.method, 'postError', sinon.match.string
);
assert.isFalse(ActivityClient.hasPendingRequest());
});
test('notifies about "share" activity request', function() {
var onShareActivityRequest = sinon.stub();
var onNewActivityRequest = sinon.stub();
ActivityClient.on('share-activity-request', onShareActivityRequest);
ActivityClient.on('new-activity-request', onNewActivityRequest);
clientStub.on.withArgs('activity-request').yield({
name: 'share',
data: { body: 'text' }
});
sinon.assert.notCalled(onNewActivityRequest);
sinon.assert.calledOnce(onShareActivityRequest);
sinon.assert.calledWith(onShareActivityRequest, { body: 'text' });
});
test('notifies about "new" activity request', function() {
var onShareActivityRequest = sinon.stub();
var onNewActivityRequest = sinon.stub();
ActivityClient.on('share-activity-request', onShareActivityRequest);
ActivityClient.on('new-activity-request', onNewActivityRequest);
clientStub.on.withArgs('activity-request').yield({
name: 'new',
data: { number: '+123' }
});
sinon.assert.notCalled(onShareActivityRequest);
sinon.assert.calledOnce(onNewActivityRequest);
sinon.assert.calledWith(onNewActivityRequest, { number: '+123' });
});
});
suite('responding to activity request >', function() {
setup(function() {
ActivityClient.init(APP_INSTANCE_ID);
clientStub.on.withArgs('activity-request').yield({
name: 'new',
data: { number: '+123' }
});
assert.isTrue(ActivityClient.hasPendingRequest());
});
test('postResult correctly handles successful method call', function(done) {
clientStub.method.returns(Promise.resolve());
ActivityClient.postResult('Result').then(() => {
sinon.assert.calledOnce(clientStub.method);
sinon.assert.calledWith(clientStub.method, 'postResult', 'Result');
assert.isFalse(ActivityClient.hasPendingRequest());
}).then(done, done);
});
test('postResult correctly handles failed method call', function(done) {
var rejectReason = new Error('Fake reason');
clientStub.method.returns(Promise.reject(rejectReason));
ActivityClient.postResult('Result').then(() => {
throw new Error('postResult should be rejected!');
}, (e) => {
sinon.assert.calledOnce(clientStub.method);
sinon.assert.calledWith(clientStub.method, 'postResult', 'Result');
assert.isFalse(ActivityClient.hasPendingRequest());
assert.equal(e, rejectReason);
}).then(done, done);
});
test('postError correctly handles successful method call', function() {
ActivityClient.postError('Error');
sinon.assert.calledOnce(clientStub.method);
sinon.assert.calledWith(clientStub.method, 'postError', 'Error');
assert.isFalse(ActivityClient.hasPendingRequest());
});
test('postError correctly handles failed method call', function(done) {
var rejectReason = new Error('Fake reason');
clientStub.method.returns(Promise.reject(rejectReason));
ActivityClient.postError('Error').then(() => {
throw new Error('postError should be rejected!');
}, (e) => {
sinon.assert.calledOnce(clientStub.method);
sinon.assert.calledWith(clientStub.method, 'postError', 'Error');
assert.isFalse(ActivityClient.hasPendingRequest());
assert.equal(e, rejectReason);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,146 @@
/*global ActivityShim,
bridge,
BroadcastChannel,
MocksHelper
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/shared/js/event_dispatcher.js');
require('/services/js/bridge_service_mixin.js');
require('/services/js/activity/activity_shim.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel'
]).init();
suite('ActivityShim >', function() {
const APP_INSTANCE_ID = 'fake-app-id';
var serviceStub, setMessageHandlerStub;
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
this.sinon.useFakeTimers();
serviceStub = sinon.stub({
method: () => {},
broadcast: () => {},
listen: () => {},
on: () => {}
});
setMessageHandlerStub = sinon.stub();
this.sinon.stub(bridge, 'service').returns(serviceStub);
});
suite('init()', function() {
test('throws if app instance id is not provided', function() {
assert.throws(() => ActivityShim.init());
});
test('bridge service is correctly initialized', function() {
ActivityShim.init(APP_INSTANCE_ID, setMessageHandlerStub);
sinon.assert.calledOnce(bridge.service);
sinon.assert.calledWith(bridge.service, 'activity-service');
sinon.assert.calledTwice(serviceStub.listen);
sinon.assert.calledWith(
serviceStub.listen,
sinon.match.instanceOf(BroadcastChannel).and(
sinon.match.has(
'name',
'activity-service-channel-' + APP_INSTANCE_ID
)
)
);
});
});
suite('activity system message >', function() {
setup(function() {
ActivityShim.init(APP_INSTANCE_ID, setMessageHandlerStub);
});
test('when client is not connected yet', function() {
sinon.assert.notCalled(setMessageHandlerStub);
});
suite('once client is connected', function() {
var activityRequest;
setup(function() {
activityRequest = sinon.stub({
postResult: () => {},
postError: () => {},
source: {
name: 'fake',
data: {
custom: 'custom'
}
}
});
serviceStub.on.withArgs('connected').yield();
setMessageHandlerStub.withArgs('activity').yield(activityRequest);
});
test('activity request is broadcasted', function() {
sinon.assert.calledWith(
serviceStub.broadcast, 'activity-request', activityRequest.source
);
});
test('postResult passes default arguments', function() {
serviceStub.method.withArgs('postResult').yield();
sinon.assert.calledOnce(activityRequest.postResult);
sinon.assert.calledWith(activityRequest.postResult, { success: true });
// Activity request is disposed after first postResult.
assert.throw(() => {
serviceStub.method.withArgs('postResult').yield();
});
sinon.assert.calledOnce(activityRequest.postResult);
});
test('postResult passes custom arguments', function() {
serviceStub.method.withArgs('postResult').yield({ custom: 'custom' });
sinon.assert.notCalled(activityRequest.postError);
sinon.assert.calledOnce(activityRequest.postResult);
sinon.assert.calledWith(
activityRequest.postResult, { custom: 'custom' }
);
// Activity request is disposed after first postResult.
assert.throw(() => {
serviceStub.method.withArgs('postResult').yield({ custom: 'custom' });
});
sinon.assert.calledOnce(activityRequest.postResult);
});
test('postError passes error reason', function() {
serviceStub.method.withArgs('postError').yield('error');
sinon.assert.notCalled(activityRequest.postResult);
sinon.assert.calledOnce(activityRequest.postError);
sinon.assert.calledWith(activityRequest.postError, 'error');
// Activity request is disposed after first postError.
assert.throw(() => {
serviceStub.method.withArgs('postError').yield('error');
});
sinon.assert.calledOnce(activityRequest.postError);
});
});
});
});

View file

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

View file

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

View file

@ -0,0 +1,107 @@
/*global
bridge,
BridgeServiceMixin,
MocksHelper,
streamService
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/shared/test/unit/mocks/mocks_helper.js');
require('/services/js/bridge_service_mixin.js');
var mocksHelperForServiceMixin = new MocksHelper([
'bridge',
'streamService'
]).init();
suite('BridgeServiceMixin >', function() {
mocksHelperForServiceMixin.attachTestHelpers();
var target, service;
setup(function() {
service = sinon.stub({
method() {},
stream() {},
broadcast() {},
listen() {},
on() {},
plugin() {}
});
target = sinon.stub({
usefulMethod() {},
usefulStream() {}
});
var methods = ['usefulMethod'];
var streams = ['usefulStream'];
var events = ['useful-event'];
var name = 'my-service';
this.sinon.stub(bridge, 'service').withArgs(name).returns(service);
BridgeServiceMixin.mixin(
target, name,
{ methods, streams, events }
);
target.initService();
});
test('Service is exposed', function() {
sinon.assert.calledWith(bridge.service, 'my-service');
sinon.assert.calledOnce(service.listen);
// Does not subscribe for any bridge event if target does not need it.
sinon.assert.notCalled(service.on);
});
test('Methods and streams are exposed', function() {
service.method.withArgs('usefulMethod').yield(1);
sinon.assert.calledWith(target.usefulMethod, 1);
sinon.assert.calledOn(target.usefulMethod, target);
sinon.assert.calledWith(service.plugin, streamService);
service.stream.withArgs('usefulStream').yield(2);
sinon.assert.calledWith(target.usefulStream, 2);
sinon.assert.calledOn(target.usefulStream, target);
});
test('Events are properly checked', function() {
var data = {};
target.broadcast('useful-event', data);
sinon.assert.calledWith(service.broadcast, 'useful-event', data);
assert.throws(() => target.broadcast('unknown-event'));
});
test('subscribes to "connected" event if target has corresponding handler',
function() {
var newTarget = sinon.stub({
usefulMethod() {},
onConnected() {}
});
BridgeServiceMixin.mixin(
newTarget, 'my-service', { methods: ['usefulMethod'] }
);
newTarget.initService();
sinon.assert.calledOnce(service.on);
sinon.assert.calledWith(service.on, 'connected');
sinon.assert.notCalled(newTarget.onConnected);
service.on.withArgs('connected').yield();
sinon.assert.calledOnce(newTarget.onConnected);
service.on.withArgs('connected').yield();
sinon.assert.calledTwice(newTarget.onConnected);
});
});

View file

@ -0,0 +1,143 @@
/*global bridge,
ConversationClient,
MocksHelper,
streamClient,
Thread
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/test/unit/mock_shared_worker.js');
require('/services/test/unit/mock_threads.js');
require('/services/js/conversation/conversation_client.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'SharedWorker',
'streamClient',
'Thread'
]).init();
suite('ConversationClient >', function() {
const APP_ID = 'fake-app-id';
var clientStub;
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
clientStub = sinon.stub({
stream: () => {},
plugin: () => {}
});
clientStub.plugin.returns(clientStub);
this.sinon.spy(window, 'SharedWorker');
this.sinon.stub(bridge, 'client').returns(clientStub);
});
test('throws if app instance id is not provided', function() {
assert.throws(() => ConversationClient.init());
});
test('bridge client is correctly initialized', function() {
ConversationClient.init(APP_ID);
sinon.assert.calledWith(
SharedWorker,
'/services/js/conversation/conversation_service.js'
);
sinon.assert.calledOnce(bridge.client);
sinon.assert.calledWith(bridge.client, {
service: 'conversation-service',
endpoint: SharedWorker.lastCall.returnValue,
timeout: false
});
sinon.assert.calledWith(clientStub.plugin, streamClient);
});
suite('getAllConversations', function() {
var streamStub, resolveStream, rejectStream;
setup(function() {
ConversationClient.init(APP_ID);
streamStub = sinon.stub({
listen: () => {},
closed: new Promise((resolve, reject) => {
resolveStream = resolve;
rejectStream = reject;
})
});
clientStub.stream.withArgs('getAllConversations', APP_ID).returns(
streamStub
);
});
test('calls callback on every retrieved item', function() {
var onConversationRetrievedStub = sinon.stub();
ConversationClient.getAllConversations(onConversationRetrievedStub);
sinon.assert.notCalled(onConversationRetrievedStub);
streamStub.listen.yield({ id: 1, body: 'body' });
sinon.assert.calledOnce(onConversationRetrievedStub);
sinon.assert.calledWith(
onConversationRetrievedStub, sinon.match({ id: 1, body: 'body'})
);
assert.instanceOf(onConversationRetrievedStub.lastCall.args[0], Thread);
streamStub.listen.yield({ id: 2, body: 'body2' });
sinon.assert.calledTwice(onConversationRetrievedStub);
sinon.assert.calledWith(
onConversationRetrievedStub, sinon.match({ id: 2, body: 'body2'})
);
assert.instanceOf(onConversationRetrievedStub.lastCall.args[0], Thread);
});
test('is resolved when stream.closed is resolved', function(done) {
var resolveStub = sinon.stub();
var streamPromise = ConversationClient.getAllConversations(() => {}).then(
resolveStub
);
Promise.resolve().then(() => {
sinon.assert.notCalled(resolveStub);
resolveStream();
return streamPromise;
}).then(() => {
sinon.assert.calledOnce(resolveStub);
}).then(done, done);
});
test('is rejected when stream.closed is rejected', function(done) {
var rejectStub = sinon.stub();
var error = new Error('Error');
var streamPromise = ConversationClient.getAllConversations(() => {}).then(
() => { throw new Error('getAllConversations should be rejected'); },
rejectStub
);
Promise.resolve().then(() => {
sinon.assert.notCalled(rejectStub);
rejectStream(error);
return streamPromise;
}).catch(() => {
sinon.assert.calledWith(rejectStub, error);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,253 @@
/*global bridge,
ConversationService,
Drafts,
MocksHelper,
MozMobileMessageClient
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/test/unit/mock_drafts.js');
require(
'/services/test/unit/moz_mobile_message/mock_moz_mobile_message_client.js'
);
require('/services/js/bridge_service_mixin.js');
require('/services/js/conversation/conversation_service.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'Drafts',
'Draft',
'MozMobileMessageClient',
'streamService'
]).init();
suite('ConversationService >', function() {
var serviceStub, mobileMessageClientStub;
const APP_INSTANCE_ID = 'fake-app-instance-id';
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
serviceStub = sinon.stub({
method: () => {},
stream: () => {},
broadcast: () => {},
listen: () => {},
plugin: () => {}
});
this.sinon.stub(bridge, 'service').withArgs('conversation-service').returns(
serviceStub
);
mobileMessageClientStub = sinon.stub({
getThreads: () => {}
});
this.sinon.stub(MozMobileMessageClient, 'forApp').withArgs(
APP_INSTANCE_ID
).returns(mobileMessageClientStub);
ConversationService.init();
});
suite('getAllConversations >', function() {
var serviceStreamStub, mobileMessageStreamStub, resolveMobileMessageStream,
rejectMobileMessageStream, drafts, threadDraft, threads;
function threadToConversationSummary(thread, threadDraft) {
return {
id: thread.id,
participants: thread.participants,
body: thread.body,
timestamp: thread.timestamp,
status: { hasUnread: thread.unreadCount > 0, hasNewError: false },
lastMessageType: thread.lastMessageType,
draft: threadDraft || null
};
}
function draftToConversationSummary(draft) {
return {
id: draft.id,
participants: draft.recipients,
body: draft.content[0],
timestamp: new Date(draft.timestamp),
status: { hasUnread: false, hasNewError: false },
lastMessageType: draft.type || 'sms',
draft: draft
};
}
setup(function() {
serviceStreamStub = sinon.stub({
write: () => {},
close: () => {},
abort: () => {}
});
mobileMessageStreamStub = sinon.stub({
listen: () => {},
cancel: () => {},
closed: new Promise((resolve, reject) => {
resolveMobileMessageStream = resolve;
rejectMobileMessageStream = reject;
})
});
mobileMessageClientStub.getThreads.returns(mobileMessageStreamStub);
drafts = [{
id: 1,
recipients: ['+1'],
content: ['body1'],
type: 'sms',
timestamp: 1
}, {
id: 3,
recipients: ['a@abc.xyz'],
content: ['body3'],
type: 'mms',
timestamp: 3
}];
threadDraft = {
id: 100,
recipients: ['+100'],
content: ['body100'],
type: 'sms',
threadId: 5,
timestamp: 100
};
threads = [{
id: 2,
participants: ['+2'],
body: 'body2',
timestamp: 2,
unreadCount: 0,
lastMessageType: 'sms'
}, {
id: 4,
participants: ['a@abc.xyz'],
body: 'body4',
timestamp: 4,
unreadCount: 1,
lastMessageType: 'mms'
}, {
id: 5,
participants: ['+5'],
body: 'body5',
timestamp: 5,
unreadCount: 0,
lastMessageType: 'sms'
}];
// Sort threads in the reverse order.
threads.sort((threadA, threadB) => threadB.timestamp - threadA.timestamp);
this.sinon.stub(Drafts, 'getAllThreadless').returns(drafts.slice());
this.sinon.stub(Drafts, 'byThreadId').returns(null);
Drafts.byThreadId.withArgs(5).returns(threadDraft);
});
test('mobileMessage stream is cancelled if main stream is cancelled',
function() {
ConversationService.getAllConversations(
serviceStreamStub, APP_INSTANCE_ID
);
serviceStreamStub.cancel('ReAsOn');
sinon.assert.calledOnce(mobileMessageStreamStub.cancel);
sinon.assert.calledWith(mobileMessageStreamStub.cancel, 'ReAsOn');
});
test('threads and drafts are returned in the correct order',
function(done) {
var getAllConversationsPromise = ConversationService.getAllConversations(
serviceStreamStub, APP_INSTANCE_ID
);
threads.forEach((thread) => {
mobileMessageStreamStub.listen.yield(thread);
});
resolveMobileMessageStream();
getAllConversationsPromise.then(() => {
sinon.assert.callCount(serviceStreamStub.write, 5);
sinon.assert.callOrder(
serviceStreamStub.write.withArgs(
sinon.match(threadToConversationSummary(threads[0], threadDraft))
),
serviceStreamStub.write.withArgs(
sinon.match(threadToConversationSummary(threads[1]))
),
serviceStreamStub.write.withArgs(
sinon.match(draftToConversationSummary(drafts[1]))
),
serviceStreamStub.write.withArgs(
sinon.match(threadToConversationSummary(threads[2]))
),
serviceStreamStub.write.withArgs(
sinon.match(draftToConversationSummary(drafts[0]))
)
);
}).then(done, done);
});
test('only drafts are returned if there is no threads', function(done) {
var getAllConversationsPromise = ConversationService.getAllConversations(
serviceStreamStub, APP_INSTANCE_ID
);
resolveMobileMessageStream();
getAllConversationsPromise.then(() => {
sinon.assert.callCount(serviceStreamStub.write, 2);
sinon.assert.callOrder(
serviceStreamStub.write.withArgs(
sinon.match(draftToConversationSummary(drafts[1]))
),
serviceStreamStub.write.withArgs(
sinon.match(draftToConversationSummary(drafts[0]))
)
);
}).then(done, done);
});
test('stream is closed when mobileMessageStream is successfully closed',
function(done) {
var getAllConversationsPromise = ConversationService.getAllConversations(
serviceStreamStub, APP_INSTANCE_ID
);
resolveMobileMessageStream();
getAllConversationsPromise.then(() => {
sinon.assert.called(serviceStreamStub.close);
}, () => {
throw new Error('getAllConversations should always be resolved');
}).then(done, done);
});
test('stream is aborted when mobileMessageStream is unexpectedly closed',
function(done) {
var getAllConversationsPromise = ConversationService.getAllConversations(
serviceStreamStub, APP_INSTANCE_ID
);
rejectMobileMessageStream(new Error('Exception'));
getAllConversationsPromise.then(() => {
sinon.assert.calledWith(serviceStreamStub.abort, '[Error] Exception');
}, () => {
throw new Error('getAllConversations should always be resolved');
}).then(done, done);
});
});
});

View file

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

View file

@ -0,0 +1,659 @@
/*global asyncStorage,
Drafts,
Draft,
InterInstanceEventDispatcher,
MocksHelper
*/
'use strict';
require('/shared/js/event_dispatcher.js');
require('/views/shared/js/utils.js');
require('/services/js/drafts.js');
require('/shared/test/unit/mocks/mock_async_storage.js');
require('/views/shared/test/unit/mock_inter_instance_event_dispatcher.js');
var MocksHelperForDraftsTest = new MocksHelper([
'asyncStorage',
'InterInstanceEventDispatcher'
]).init();
suite('Drafts', function() {
var mocksHelper = MocksHelperForDraftsTest;
var threadDraft1, threadDraft2, threadDraft3, threadDraft4, threadDraft5;
var draft1, draft2;
suiteSetup(function() {
mocksHelper.suiteSetup();
threadDraft1 = new Draft({
recipients: ['555', '666'],
content: ['This is a draft message'],
timestamp: 1,
threadId: 42,
subject: 'This is a subject',
type: 'sms',
id: 1
});
threadDraft2 = new Draft({
recipients: ['555'],
content: ['This is a draft message'],
timestamp: 2,
threadId: 44,
subject: 'This is a subject',
type: 'sms',
id: 2
});
threadDraft3 = new Draft({
recipients: ['555', '222'],
content: ['This is a draft message'],
timestamp: 3,
threadId: 1,
subject: 'This is a subject',
type: 'sms',
id: 3
});
threadDraft4 = new Draft({
recipients: ['555', '333'],
content: ['This is a draft message'],
timestamp: 4,
threadId: 2,
subject: 'This is a subject',
type: 'sms',
id: 4
});
draft1 = new Draft({
recipients: ['555', '444'],
content: ['This is a draft message'],
timestamp: 5,
threadId: null,
subject: 'This is a subject',
type: 'sms',
id: 5
});
threadDraft5 = new Draft({
recipients: ['123456'],
content: [
'This is a draft MMS...',
{
blob: {
type: 'audio/ogg',
size: 12345
},
name: 'audio.oga'
},
'...with a recipient and a thread'
],
timestamp: Date.now() - (3600000 * 2),
threadId: 8,
type: 'mms',
id: 6
});
draft2 = new Draft({
recipients: ['555', '444'],
content: ['This is a draft message'],
timestamp: 5,
threadId: null,
subject: 'This is a different subject',
type: 'sms',
id: 7
});
});
suiteTeardown(function() {
mocksHelper.suiteTeardown();
});
suite('init() >', function() {
var savedHandler;
var deletedHandler;
setup(function() {
this.sinon.spy(InterInstanceEventDispatcher, 'on');
this.sinon.stub(Drafts, 'request').returns(Promise.resolve());
this.sinon.stub(Drafts, 'emit');
Drafts.init();
savedHandler = InterInstanceEventDispatcher.on.firstCall.args[1];
deletedHandler = InterInstanceEventDispatcher.on.secondCall.args[1];
});
test('draft-saved will be dispatched as saved', function(done) {
sinon.assert.calledWith(InterInstanceEventDispatcher.on, 'draft-saved');
savedHandler(threadDraft1).then(() => {
sinon.assert.calledWith(Drafts.request, true);
sinon.assert.calledWith(Drafts.emit, 'saved', threadDraft1);
}).then(done, done);
});
test('draft-deleted will be dispatched as deleted', function(done) {
sinon.assert.calledWith(InterInstanceEventDispatcher.on, 'draft-deleted');
deletedHandler(threadDraft2).then(() => {
sinon.assert.calledWith(Drafts.request, true);
sinon.assert.calledWith(Drafts.emit, 'deleted', threadDraft2);
}).then(done, done);
});
});
suite('add() >', function() {
var onSavedStub;
setup(function() {
onSavedStub = sinon.stub();
Drafts.on('saved', onSavedStub);
this.sinon.spy(InterInstanceEventDispatcher, 'emit');
});
teardown(function() {
Drafts.clear().offAll('saved');
});
test('correctly adds thread bound drafts', function() {
Drafts.add(threadDraft1);
assert.deepEqual(Drafts.byThreadId(threadDraft1.threadId), threadDraft1);
assert.isNull(Drafts.byThreadId(threadDraft2.threadId));
sinon.assert.calledOnce(onSavedStub);
sinon.assert.calledWith(onSavedStub, threadDraft1);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
threadDraft1
);
Drafts.add(threadDraft2);
assert.deepEqual(Drafts.byThreadId(threadDraft1.threadId), threadDraft1);
assert.deepEqual(Drafts.byThreadId(threadDraft2.threadId), threadDraft2);
sinon.assert.calledTwice(onSavedStub);
sinon.assert.calledWith(onSavedStub, threadDraft2);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
threadDraft2
);
});
test('correctly adds thread less drafts', function() {
Drafts.add(draft1);
assert.deepEqual(Drafts.byDraftId(draft1.id), draft1);
assert.isNull(Drafts.byDraftId(draft2.id));
sinon.assert.calledOnce(onSavedStub);
sinon.assert.calledWith(onSavedStub, draft1);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
draft1
);
Drafts.add(draft2);
assert.deepEqual(Drafts.byDraftId(draft1.id), draft1);
assert.deepEqual(Drafts.byDraftId(draft2.id), draft2);
sinon.assert.calledTwice(onSavedStub);
sinon.assert.calledWith(onSavedStub, draft2);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
draft2
);
});
test('add draft of same threadId replaces previous', function() {
assert.equal(
Drafts.add(threadDraft2).byThreadId(threadDraft2.threadId).id,
threadDraft2.id
);
sinon.assert.calledOnce(onSavedStub);
sinon.assert.calledWith(onSavedStub, threadDraft2);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
threadDraft2
);
var updatedDraft = new Draft({
recipients: ['555'],
content: ['This is a new draft for thread 44'],
subject: 'This is a subject',
timestamp: 2,
threadId: 44,
type: 'sms'
});
Drafts.add(updatedDraft);
assert.notEqual(Drafts.byThreadId(44).id, threadDraft2.id);
sinon.assert.calledTwice(onSavedStub);
sinon.assert.calledWith(onSavedStub, updatedDraft);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
updatedDraft
);
});
test('add threadless draft of same draft.id replaces previous', function() {
assert.equal(
Drafts.add(draft1).byDraftId(draft1.id).content,
'This is a draft message'
);
sinon.assert.calledOnce(onSavedStub);
sinon.assert.calledWith(onSavedStub, draft1);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
draft1
);
var updatedDraft = new Draft({
recipients: ['555', '444'],
content: ['This is a new draft message'],
timestamp: 5,
threadId: null,
subject: 'This is a subject',
type: 'sms',
id: 5
});
Drafts.add(updatedDraft);
assert.equal(
Drafts.byDraftId(draft1.id).content,
'This is a new draft message'
);
sinon.assert.calledTwice(onSavedStub);
sinon.assert.calledWith(onSavedStub, updatedDraft);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-saved',
updatedDraft
);
});
});
suite('delete() >', function() {
var onDeletedStub;
setup(function() {
onDeletedStub = sinon.stub();
Drafts.on('deleted', onDeletedStub);
[threadDraft1, threadDraft2, threadDraft5, draft2].forEach(
Drafts.add, Drafts
);
this.sinon.spy(InterInstanceEventDispatcher, 'emit');
});
teardown(function() {
Drafts.clear().offAll('deleted');
});
test('Delete draft with reference', function() {
assert.isNotNull(Drafts.byThreadId(threadDraft1.threadId));
assert.isNull(
Drafts.delete(threadDraft1).byThreadId(threadDraft1.threadId)
);
sinon.assert.calledOnce(onDeletedStub);
sinon.assert.calledWith(onDeletedStub, threadDraft1);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-deleted',
threadDraft1
);
});
test('Deleting new message drafts', function() {
assert.isNotNull(Drafts.byThreadId(threadDraft5.threadId));
assert.isNotNull(Drafts.byDraftId(draft2.id));
// First draft removes only the draft from the drafts
assert.isNull(
Drafts.delete(threadDraft5).byThreadId(threadDraft5.threadId)
);
assert.isNotNull(Drafts.byDraftId(draft2.id));
sinon.assert.calledOnce(onDeletedStub);
sinon.assert.calledWith(onDeletedStub, threadDraft5);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-deleted',
threadDraft5
);
// The last draft in the thread removes the thread from the index
assert.isNull(Drafts.delete(draft2).byDraftId(draft2.id));
sinon.assert.calledTwice(onDeletedStub);
sinon.assert.calledWith(onDeletedStub, draft2);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-deleted',
draft2
);
});
test('delete by non-draft object', function() {
var draft = new Draft({
recipients: ['999999'],
content: ['foo'],
timestamp: 2,
threadId: 999,
subject: 'This is a subject',
type: 'sms'
});
Drafts.add(draft);
assert.deepEqual(Drafts.byThreadId(999), draft);
var draftToDelete = new Draft({
threadId: 999,
id: draft.id
});
Drafts.delete(draftToDelete);
assert.isNull(Drafts.byThreadId(999));
sinon.assert.calledOnce(onDeletedStub);
sinon.assert.calledWith(onDeletedStub, draftToDelete);
sinon.assert.calledWith(
InterInstanceEventDispatcher.emit,
'draft-deleted',
draftToDelete
);
});
test('does not fire "deleted" event if nothing was deleted', function() {
// Try to delete draft that doesn't exist.
Drafts.delete(draft1);
sinon.assert.notCalled(onDeletedStub);
sinon.assert.notCalled(InterInstanceEventDispatcher.emit);
});
});
suite('Select drafts', function() {
suiteSetup(function() {
[threadDraft1, threadDraft2, threadDraft3, threadDraft4, draft1].forEach(
Drafts.add, Drafts
);
});
suiteTeardown(function() {
Drafts.clear();
});
suite('byThreadId', function() {
test('get drafts for id 1', function() {
assert.deepEqual(
Drafts.byThreadId(threadDraft3.threadId), threadDraft3
);
});
test('throws if threadId is not a number', function() {
assert.throws(() => Drafts.byThreadId(null));
});
test('returns null if there is no draft for the thread id', function() {
assert.isNull(Drafts.byThreadId(10));
});
});
});
suite('get(id) > ', function() {
var id = 101;
suiteSetup(function() {
Drafts.add(new Draft({
id: id,
recipients: [],
content: ['A new message draft with no recipients'],
type: 'sms'
}));
});
suiteTeardown(function() {
Drafts.clear();
});
test('get draft for id 101', function() {
var draft = Drafts.byDraftId(id);
assert.equal(draft.id, id);
assert.equal(draft.threadId, null);
});
test('get no draft for non-existant id', function() {
assert.isNull(Drafts.byDraftId(9999999));
});
});
suite('getAll()>', function() {
setup(function() {
[
threadDraft1,
threadDraft2,
threadDraft3,
threadDraft4,
threadDraft5,
draft1,
draft2
].forEach(Drafts.add, Drafts);
});
teardown(function() {
Drafts.clear();
});
test('getAll returns all drafts', function() {
var orderedDrafts = [
threadDraft1,
threadDraft2,
threadDraft3,
threadDraft4,
threadDraft5,
draft1,
draft2
];
for (var draft of Drafts.getAll()) {
assert.deepEqual(draft, orderedDrafts.shift());
}
// All drafts were examined
assert.equal(orderedDrafts.length, 0);
});
});
suite('getAllThreadless()>', function() {
setup(function() {
[
threadDraft1,
draft1,
draft2
].forEach(Drafts.add, Drafts);
});
teardown(function() {
Drafts.clear();
});
test('getAllThreadless returns only thread less drafts', function() {
assert.deepEqual(Drafts.getAllThreadless(), [draft1, draft2]);
});
});
suite('clear() >', function() {
suiteSetup(function() {
[threadDraft1, threadDraft2, threadDraft3, threadDraft4].forEach(
Drafts.add, Drafts
);
});
test('clear the entire draft index', function() {
Drafts.clear();
assert.isNull(Drafts.byThreadId(42));
assert.isNull(Drafts.byThreadId(1));
assert.isNull(Drafts.byThreadId(2));
});
});
suite('Draft', function() {
var draft;
test('Draft', function() {
draft = new Draft();
assert.ok(Draft);
});
test('Draft from empty object', function() {
draft = new Draft([]);
assert.deepEqual(draft.recipients, []);
assert.deepEqual(draft.content, []);
assert.equal(draft.threadId, null);
assert.isFalse(draft.isEdited);
});
test('Draft from Draft object', function() {
draft = new Draft(threadDraft1);
assert.deepEqual(draft.recipients, ['555', '666']);
assert.deepEqual(draft.content, ['This is a draft message']);
assert.equal(draft.timestamp, 1);
assert.equal(draft.threadId, 42);
assert.equal(draft.type, 'sms');
assert.isFalse(draft.isEdited);
});
test('Draft with explicit valid id', function() {
draft = new Draft({
id: 101,
recipients: [],
content: ['An explicit id'],
type: 'sms'
});
assert.equal(draft.id, 101);
});
test('Draft with explicit invalid id', function() {
try {
draft = new Draft({
id: '101',
recipients: [],
content: ['An explicit id'],
timestamp: Date.now(),
type: 'sms'
});
assert.ok(false);
} catch (e) {
assert.ok(true);
}
});
});
suite('Storage and Retrieval', function() {
setup(function() {
Drafts.clear();
this.sinon.useFakeTimers();
});
test('Load drafts, has stored data', function(done) {
this.sinon.stub(asyncStorage, 'getItem').yields([
[42, [threadDraft1]],
[44, [threadDraft2]],
[null, [draft1, draft2]]
]);
Drafts.request().then(function() {
assert.deepEqual(Drafts.byThreadId(42), threadDraft1);
assert.deepEqual(Drafts.byThreadId(44), threadDraft2);
var threadLessDrafts = Array.from(Drafts.getAll()).filter(
(draft) => !draft.threadId
);
assert.equal(threadLessDrafts.length, 2);
assert.isNull(Drafts.byThreadId(5));
}).then(done, done);
});
test('Load, clear, restore drafts', function(done) {
this.sinon.stub(asyncStorage, 'getItem').yields([
[42, [threadDraft1]],
[44, [threadDraft2]],
[null, [draft1, draft2]]
]);
// Load
Drafts.request().then(function() {
assert.equal(Drafts.size, 3);
assert.isTrue(asyncStorage.getItem.calledOnce);
// Clear (This will set isCached = false)
Drafts.clear();
assert.equal(Drafts.size, 0);
// Restore
return Drafts.request().then(function() {
assert.equal(Drafts.size, 3);
assert.isTrue(asyncStorage.getItem.calledTwice);
});
}).then(done, done);
});
test('Load drafts, has no stored data', function(done) {
this.sinon.stub(asyncStorage, 'getItem').yields([]);
Drafts.request().then(function() {
assert.equal(Drafts.size, 0);
}).then(done, done);
});
suite('drafts index cache >', function() {
setup(function() {
this.sinon.stub(asyncStorage, 'getItem').
withArgs('draft index').
yields(null);
});
test('is properly populated', function(done) {
Drafts.request().then(() => {
sinon.assert.calledOnce(asyncStorage.getItem);
return Drafts.request();
}).then(() => {
sinon.assert.calledOnce(asyncStorage.getItem);
}).then(done, done);
});
test('is properly refreshed with force parameter', function(done) {
Drafts.request().then(() => {
sinon.assert.calledOnce(asyncStorage.getItem);
// "Force" parameter should re-request data from asyncStorage even if
// it's already cached
return Drafts.request(true);
}).then(() => {
sinon.assert.calledTwice(asyncStorage.getItem);
}).then(done, done);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
/*global bridge,
MessagingClient,
MocksHelper
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/js/messaging/messaging_client.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
]).init();
suite('Messaging Client >', function() {
MocksHelperForAttachment.attachTestHelpers();
const APP_INSTANCE_ID = 'fake-app-instance-id';
var clientStub;
var successResult = {};
var errorResult = new Error();
const METHODS = {
'sendSMS': [{
recipients: '123',
content: 'hola'
}],
'sendMMS': [{
receivers: ['123'],
subject: null,
smil: 'fake smil',
attachments: 'fake attachments'
}],
'resendMessage': [{
id: 'fake message'
}],
'retrieveMMS': [1]
};
setup(function() {
clientStub = sinon.stub({
method: () => {}
});
this.sinon.stub(bridge, 'client').returns(clientStub);
});
suite('init', function() {
test('throws if app instance id is not provided', function() {
assert.throws(() => MessagingClient.forApp());
});
test('client is initialized', function() {
MessagingClient.init(APP_INSTANCE_ID);
sinon.assert.calledWith(bridge.client, {
service: 'messaging-service',
endpoint: sinon.match.instanceOf(SharedWorker),
timeout: false
});
});
});
Object.keys(METHODS).forEach((method) => {
setup(function() {
MessagingClient.init(APP_INSTANCE_ID);
});
suite(method, function() {
var params = METHODS[method];
test('onsuccess', function(done) {
clientStub.method.withArgs(method, ...params, APP_INSTANCE_ID).returns(
Promise.resolve(successResult)
);
MessagingClient[method](...params).then((result) => {
sinon.assert.calledWithExactly(
clientStub.method, method, ...params, APP_INSTANCE_ID
);
assert.equal(successResult, result);
}).then(done, done);
});
test('onerror', function(done) {
clientStub.method.withArgs(method, ...params, APP_INSTANCE_ID).returns(
Promise.reject(errorResult)
);
MessagingClient[method](...params).then(() => {
throw new Error('Promise should not be resolved!');
}, (error) => {
sinon.assert.calledWithExactly(
clientStub.method, method, ...params, APP_INSTANCE_ID
);
assert.deepEqual(errorResult, error);
}).then(done, done);
});
});
});
});

View file

@ -0,0 +1,560 @@
/*global bridge,
MessagingService,
MozMobileMessageClient,
MocksHelper,
MockMessages,
Settings,
SMIL
*/
'use strict';
require('/services/js/bridge_service_mixin.js');
require('/services/test/unit/mock_bridge.js');
require(
'/services/test/unit/moz_mobile_message/mock_moz_mobile_message_client.js'
);
require('/views/shared/test/unit/mock_messages.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/test/unit/mock_smil.js');
require('/services/js/messaging/messaging_service.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'MozMobileMessageClient',
'Settings',
'SMIL'
]).init();
suite('Messaging service >', function() {
var serviceStub, clientStub, successResult, errorResult;
const APP_INSTANCE_ID = 'fake-app-instance-id';
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
successResult = {};
errorResult = new Error();
serviceStub = sinon.stub({
method: () => {},
stream: () => {},
broadcast: () => {},
listen: () => {},
plugin: () => {}
});
clientStub = sinon.stub({
send: () => {},
sendMMS: () => {},
resendMessage: () => {},
retrieveMMS: () => {},
delete: () => {}
});
this.sinon.stub(MozMobileMessageClient, 'forApp').withArgs(
APP_INSTANCE_ID
).returns(clientStub);
this.sinon.stub(bridge, 'service').returns(serviceStub);
MessagingService.init();
});
suite('init', function() {
test('service mixing and client are initialized', function() {
sinon.assert.calledWith(bridge.service, 'messaging-service');
});
});
suite('sendSMS', function() {
var smsOpts;
test('send to one recipient successfully', function(done) {
smsOpts = {
recipients: '123',
content: 'hola'
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
);
assert.deepEqual(results, [{
success: true,
result: successResult,
code: undefined,
recipient: '123'
}]);
}).then(done, done);
});
test('send to two recipients successfully', function(done) {
smsOpts = {
recipients: ['123', '456'],
content: 'hola'
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
sinon.assert.calledTwice(clientStub.send);
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients[0], smsOpts.content, undefined
);
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients[1], smsOpts.content, undefined
);
results.forEach((result, idx) => {
assert.deepEqual(result, {
success: true,
result: successResult,
code: undefined,
recipient: smsOpts.recipients[idx]
});
});
}).then(done, done);
});
test('send to one recipient unsuccessfully', function(done) {
smsOpts = {
recipients: '123',
content: 'hola'
};
clientStub.send.returns(Promise.reject(errorResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
);
assert.deepEqual(results, [{
success: false,
result: undefined,
code: errorResult,
recipient: '123'
}]);
}).then(done, done);
});
test('send to two recipients unsuccessfully', function(done) {
smsOpts = {
recipients: ['123', '456'],
content: 'hola'
};
clientStub.send.returns(Promise.reject(errorResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
sinon.assert.calledTwice(clientStub.send);
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients[0], smsOpts.content, undefined
);
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients[1], smsOpts.content, undefined
);
results.forEach((result, idx) => {
assert.deepEqual(result, {
success: false,
result: undefined,
code: errorResult,
recipient: smsOpts.recipients[idx]
});
});
}).then(done, done);
});
test('send with a serviceId in dual SIM setup', function(done) {
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
smsOpts = {
recipients: '123',
content: 'hola',
serviceId: 0 // we use 0 because it's falsy, to test it still works
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, { serviceId: 0 }
);
}).then(done, done);
});
test('send with a serviceId in non-dual SIM setup', function(done) {
this.sinon.stub(Settings, 'hasSeveralSim').returns(false);
smsOpts = {
recipients: '123',
content: 'hola',
serviceId: 1
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
);
}).then(done, done);
});
test('serviceId is a string containing a number', function(done) {
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
smsOpts = {
recipients: '123',
content: 'hola',
serviceId: '0'
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, { serviceId: 0 }
);
}).then(done, done);
});
test('serviceId is a bad string', function(done) {
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
smsOpts = {
recipients: '123',
content: 'hola',
serviceId: 'oirutoirutoitr'
};
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
);
}).then(done, done);
});
});
suite('sendMMS', function() {
var smil;
setup(function() {
smil = {
smil: {},
attachments: {}
};
this.sinon.stub(SMIL, 'generate').returns(smil);
});
test('send to one recipient successfully', function(done) {
var mmsOpts = {
recipients: '123',
subject: null,
content: 'hola'
};
clientStub.sendMMS.returns(Promise.resolve(successResult));
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then((result) => {
sinon.assert.calledWithExactly(
clientStub.sendMMS,
{
receivers: ['123'],
subject: null,
smil: smil.smil,
attachments: smil.attachments
}, /* send options */ undefined
);
assert.equal(result, successResult);
}).then(done, done);
});
test('send to two recipients successfully', function(done) {
var mmsOpts = {
recipients: ['123', '456'],
subject: null,
content: 'hola'
};
clientStub.sendMMS.returns(Promise.resolve(successResult));
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then((result) => {
sinon.assert.calledWithExactly(
clientStub.sendMMS,
{
receivers: ['123', '456'],
subject: null,
smil: smil.smil,
attachments: smil.attachments
}, /* send options */ undefined
);
assert.equal(result, successResult);
}).then(done, done);
});
test('send to one recipient unsuccessfully', function(done) {
var mmsOpts = {
recipients: '123',
subject: null,
content: 'hola'
};
clientStub.sendMMS.returns(Promise.reject(errorResult));
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).catch((error) => {
assert.equal(error, errorResult);
}).then(done, done);
});
test('send to two recipients unsuccessfully', function(done) {
var mmsOpts = {
recipients: ['123', '456'],
subject: null,
content: 'hola'
};
clientStub.sendMMS.returns(Promise.reject(errorResult));
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).catch((error) => {
assert.equal(error, errorResult);
}).then(done, done);
});
suite('send with a serviceId', function() {
var mmsOpts;
setup(function() {
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
mmsOpts = {
recipients: '123',
subject: null,
content: 'hola',
// we use 0 to check that the code behaves correctly with falsy values
serviceId: 0
};
clientStub.sendMMS.returns(Promise.resolve(successResult));
});
test('while the current serviceId is the same', function(done) {
Settings.mmsServiceId = 0;
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.sendMMS,
{
receivers: ['123'],
subject: null,
smil: smil.smil,
attachments: smil.attachments
}, {
serviceId: mmsOpts.serviceId
}
);
}).then(done, done);
});
test('while the current serviceId is different', function(done) {
Settings.mmsServiceId = 1;
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.sendMMS,
{
receivers: ['123'],
subject: null,
smil: smil.smil,
attachments: smil.attachments
}, {
serviceId: mmsOpts.serviceId
}
);
}).then(done, done);
});
test('on a non-dual sim setup with different serviceId', function(done) {
Settings.hasSeveralSim.returns(false);
Settings.mmsServiceId = 1;
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.sendMMS,
{
receivers: ['123'],
subject: null,
smil: smil.smil,
attachments: smil.attachments
}, undefined);
}).then(done, done);
});
test('serviceId is a string containing a number', function(done) {
mmsOpts.serviceId = '0';
Settings.mmsServiceId = 0;
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWith(
clientStub.sendMMS, sinon.match.any, { serviceId: 0 }
);
}).then(done, done);
});
test('serviceId is a bad string', function(done) {
mmsOpts.serviceId = 'hjuoriut';
Settings.mmsServiceId = 0;
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWithExactly(
clientStub.sendMMS, sinon.match.any, undefined
);
}).then(done, done);
});
});
});
suite('resendMessage', function() {
setup(function() {
clientStub.delete.returns(Promise.resolve());
});
test('fails if message is not given', function() {
assert.throws(function() {
MessagingService.resendMessage();
});
});
suite('SMS message', function() {
var message;
setup(function() {
message = MockMessages.sms({
iccId: 100
});
});
test('uses message iccId to retrieve service Id in case of multiple SIMs',
function(done) {
var serviceId = 3;
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
this.sinon.stub(Settings, 'getServiceIdByIccId').returns(serviceId);
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWith(
Settings.getServiceIdByIccId,
message.iccId
);
sinon.assert.calledWith(
clientStub.send,
sinon.match.any, sinon.match.any, {
serviceId: serviceId
}
);
}).then(done, done);
});
test('deletes old message on service promise resolve', function(done) {
clientStub.send.returns(Promise.resolve(successResult));
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWith(
clientStub.delete, message.id
);
}).then(done, done);
});
test('deletes old message on service promise reject', function(done) {
clientStub.send.returns(Promise.reject(errorResult));
MessagingService.resendMessage(message, APP_INSTANCE_ID).catch(
(error) => {
sinon.assert.calledWith(clientStub.delete, message.id);
assert.equal(error, errorResult);
}).then(done, done);
});
});
suite('MMS message', function() {
var message;
setup(function() {
message = MockMessages.mms({
iccId: 100
});
});
test('uses message iccId to retrieve service Id in case of multiple SIMs',
function(done) {
var serviceId = 3;
clientStub.sendMMS.returns(Promise.resolve(successResult));
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
this.sinon.stub(Settings, 'getServiceIdByIccId').returns(serviceId);
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(
(result) => {
sinon.assert.calledWith(
Settings.getServiceIdByIccId,
message.iccId
);
sinon.assert.calledWith(
clientStub.sendMMS,{
receivers: message.receivers,
subject: message.subject,
smil: message.smil,
attachments: message.attachments
}, {
serviceId: serviceId
}
);
assert.equal(result, successResult);
}).then(done, done);
});
test('deletes old message on success and calls callback', function(done) {
clientStub.sendMMS.returns(Promise.resolve(successResult));
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
sinon.assert.calledWith(clientStub.delete, message.id);
}).then(done, done);
});
test('deletes old message on error and calls callback', function(done) {
clientStub.sendMMS.returns(Promise.reject(errorResult));
MessagingService.resendMessage(message, APP_INSTANCE_ID).catch(
(error) => {
sinon.assert.calledWith(clientStub.delete, message.id);
assert.equal(error, errorResult);
}).then(done, done);
});
});
});
suite('retrieveMMS', function() {
var id;
setup(function() {
id = 1;
});
test('retrieveMMS success', function(done) {
clientStub.retrieveMMS.returns(Promise.resolve(successResult));
MessagingService.retrieveMMS(id, APP_INSTANCE_ID).then((result) => {
sinon.assert.calledWith(clientStub.retrieveMMS, id);
assert.equal(result, successResult);
}).then(done, done);
});
test('retrieveMMS error', function(done) {
clientStub.retrieveMMS.returns(Promise.reject(errorResult));
MessagingService.retrieveMMS(id, APP_INSTANCE_ID).catch((error) => {
sinon.assert.calledWith(clientStub.retrieveMMS, id);
assert.equal(error, errorResult);
}).then(done, done);
});
});
});

View file

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

View file

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

View file

@ -0,0 +1,23 @@
/*exported MockDrafts,
MockDraft
*/
'use strict';
var MockDrafts = {
init: () => {},
add: function() { return this; },
delete: function() { return this; },
byThreadId: () => null,
byDraftId: function() {},
clear: function() {},
store: () => Promise.resolve(),
request: () => Promise.resolve(),
getAll: () => [],
getAllThreadless: () => [],
on: () => {}
};
function MockDraft(draft) {
Object.assign(this, { id: 'draftId' }, draft);
}

View file

@ -0,0 +1,30 @@
/* global Promise */
/* exported MockMessageManager */
'use strict';
var MockMessageManager = {
_message: null,
init: () => {},
getThreads: function() {},
getMessages: function() {},
getMessage: () => {},
deleteMessages: () => Promise.resolve(),
sendSMS: function() {
return {};
},
sendMMS: function() {
return {};
},
resendMessage: function() {},
retrieveMMS: function() {},
markMessagesRead: function() {},
markThreadRead: function() {},
findThreadFromNumber: function() {},
getSegmentInfo: function() {
return Promise.reject(new Error('not implemented '));
},
ensureThreadRegistered() { return Promise.resolve(); },
on: function() {},
off: function() {}
};

View file

@ -0,0 +1,24 @@
/*exported MockMozMobileConnections */
// The real navigator.mozMobileConnections is not a real array
'use strict';
var MockMozMobileConnections = {
0: {
iccId: 'SIM 1',
addEventListener: () => {},
removeEventListener: () => {},
data: {
state: 'searching'
}
},
1: {
iccId: 'SIM 2',
addEventListener: () => {},
removeEventListener: () => {},
data: {
state: 'searching'
}
},
length: 2
};

View file

@ -0,0 +1,225 @@
/*exported MockNavigatormozMobileMessage */
'use strict';
var MockNavigatormozMobileMessage = {
_mSmsRequest: null,
_mMmsRequest: null,
_mSegmentInfoRequests: [],
_mMarkReadRequest: null,
_mMessagesRequest: null,
_mMessageRequest: null,
send: function(recipients) {
this._mSmsRequest = Array.isArray(recipients) ?
recipients.map(() => { return {}; }) : {};
return this._mSmsRequest;
},
sendMMS: function() {
this._mMmsRequest = {};
return this._mMmsRequest;
},
markMessageRead: function(messageId, value) {
this._mMarkReadRequest = {};
return this._mMarkReadRequest;
},
/**
* mTriggerMarkReadSuccess returns true if there was a read request to act
* upon, false otherwise
*/
mTriggerMarkReadSuccess: function() {
var evt = { target: { result: null } };
if (this._mMarkReadRequest && this._mMarkReadRequest.onsuccess) {
var current = this._mMarkReadRequest;
this._mMarkReadRequest = null;
current.onsuccess.call(evt.target, evt);
return true;
}
return false;
},
/**
* mTriggerMarkReadError returns true if there was a read request to act
* upon, false otherwise
*/
mTriggerMarkReadError: function(errorName) {
var evt = { target: { error: { name: errorName || null } } };
if (this._mMarkReadRequest && this._mMarkReadRequest.onerror) {
var current = this._mMarkReadRequest;
this._mMarkReadRequest = null;
current.onerror.call(evt.target, evt);
return true;
}
return false;
},
getMessages: function(filter, invert) {
this._mMessagesRequest = {};
return this._mMessagesRequest;
},
getMessage: function(aMessageID) {
this._mMessageRequest = {};
return this._mMessageRequest;
},
mTriggerMessagesRequest: function(messages) {
var mock = this;
if (this._mMessagesRequest && this._mMessagesRequest.onsuccess) {
var cursor = {
done: false,
continue: function() {
var next = messages.shift();
if (next === undefined) {
this.result = null;
this.done = true;
} else {
this.result = next;
}
var evt = { target: cursor };
mock._mMessagesRequest.onsuccess.call(this, evt);
}
};
cursor.continue();
}
},
mTriggerSuccessMessageRequest: function() {
if (this._mMessageRequest && this._mMessageRequest.onsuccess) {
this._mMessageRequest.onsuccess.call(this, null);
}
},
getSegmentInfoForText: function() {
var lastRequest = {};
this._mSegmentInfoRequests.push(lastRequest);
return lastRequest;
},
mTriggerSegmentInfoSuccess: function(segmentInfo, index) {
segmentInfo = segmentInfo || this._mDefaultSegmentInfo;
var evt = { target: { result: segmentInfo } };
if (this._mSegmentInfoRequests && this._mSegmentInfoRequests.length) {
if (index === undefined) {
// trigger the last one
index = this._mSegmentInfoRequests.length - 1;
}
var request = this._mSegmentInfoRequests.splice(index, 1)[0];
if (request.onsuccess) {
request.onsuccess.call(evt.target, evt);
}
}
},
mTriggerSegmentInfoError: function(index) {
var evt = { target: { error: { name: 'SegmentInfoError' } } };
if (this._mSegmentInfoRequests && this._mSegmentInfoRequests.length) {
if (index === undefined) {
// trigger the last one
index = this._mSegmentInfoRequests.length - 1;
}
var request = this._mSegmentInfoRequests.splice(index, 1)[0];
if (request.onerror) {
request.onerror.call(evt.target, evt);
}
}
},
getThreads: function() {
var cursor = {
result: null
};
var evt = { target: {} };
setTimeout(function() {
cursor.onsuccess.call(evt.target, evt);
});
return cursor;
},
retrieveMMS: function() {},
_mDefaultSegmentInfo: {
segments: 1,
charsAvailableInLastSegment: 150
},
mTriggerSmsOnSuccess: function() {
var evt = { target: { result: null } };
if (this._mSmsRequest) {
if (Array.isArray(this._mSmsRequest)) {
this._mSmsRequest.forEach(function(request) {
if (request.onsuccess) {
request.onsuccess.call(evt.target, evt);
}
});
} else {
this._mSmsRequest.onsuccess &&
this._mSmsRequest.onsuccess.call(evt.target, evt);
}
this._mSmsRequest = null;
}
},
mTriggerSmsOnError: function() {
var evt = { target: { error: { name: null } } };
if (this._mSmsRequest) {
if (Array.isArray(this._mSmsRequest)) {
this._mSmsRequest.forEach(function(request) {
if (request.onerror) {
request.onerror.call(evt.target, evt);
}
});
} else {
this._mSmsRequest.onerror &&
this._mSmsRequest.onerror.call(evt.target, evt);
}
this._mSmsRequest = null;
}
},
mTriggerMmsOnSuccess: function() {
var evt = { target: { result: null } };
if (this._mMmsRequest && this._mMmsRequest.onsuccess) {
this._mMmsRequest.onsuccess.call(evt.target, evt);
this._mMmsRequest = null;
}
},
mTriggerMmsOnError: function() {
var evt = { target: { error: { name: null } } };
if (this._mMmsRequest && this._mMmsRequest.onerror) {
this._mMmsRequest.onerror.call(evt.target, evt);
this._mMmsRequest = null;
}
},
mTeardown: function() {
this._mSegmentInfoRequests = [];
this._mSmsRequest = null;
this._mMmsRequest = null;
this._mMarkReadRequest = null;
this._mMessagesRequest = null;
this._mMessageRequest = null;
},
addEventListener: function() {}
};

View file

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

View file

@ -0,0 +1,25 @@
/*exported MockThreads, MockThread */
'use strict';
var MockThreads = {
registerMessage: () => {},
unregisterMessage: () => {},
has: () => false,
set: () => {},
get: () => new MockThread(),
delete: () => {},
clear: () => {},
keys: () => []
};
MockThreads.Messages = {
get: () => {}
};
function MockThread(thread) {
Object.assign(this, { messages: new Map() }, thread);
}
MockThread.prototype.getDraft = () => null;
MockThread.create = () => {};

View file

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

View file

@ -0,0 +1,76 @@
/*global bridge,
BroadcastChannel,
MozMobileConnectionsClient,
MocksHelper
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
require('/services/js/moz_mobile_connections/moz_mobile_connections_client.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel'
]).init();
suite('MozMobileConnectionsClient >', function() {
var clientStub;
var APP_INSTANCE_ID = 'fake-id';
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
clientStub = sinon.stub({
method: () => {}
});
this.sinon.stub(bridge, 'client').returns(clientStub);
});
suite('init', function() {
test('throws if app instance id is not provided', function() {
assert.throws(() => MozMobileConnectionsClient.init());
});
test('client is initialized', function() {
MozMobileConnectionsClient.init(APP_INSTANCE_ID);
sinon.assert.calledWith(bridge.client, {
service: 'moz-mobile-connections-shim',
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
sinon.match.has(
'name',
'moz-mobile-connections-shim-channel-' + APP_INSTANCE_ID
)
),
timeout: false
});
});
});
suite('switchMmsSimHandler method', function() {
setup(function() {
MozMobileConnectionsClient.init(APP_INSTANCE_ID);
});
test('client method is called with correct parameter', function() {
var iccId = 123;
var fakePromise = 'fakePromise';
clientStub.method.withArgs('switchMmsSimHandler', iccId).returns(
fakePromise
);
assert.equal(
fakePromise,
MozMobileConnectionsClient.switchMmsSimHandler(iccId)
);
sinon.assert.calledWithMatch(
clientStub.method,
'switchMmsSimHandler',
iccId
);
});
});
});

View file

@ -0,0 +1,111 @@
/*global bridge,
BroadcastChannel,
MockMozMobileConnections,
MocksHelper,
MozMobileConnectionsShim,
MozSettingsShim
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/test/unit/mock_moz_mobile_connections.js');
require('/services/test/unit/moz_settings/mock_moz_settings_shim.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
require('/services/js/bridge_service_mixin.js');
require('/services/js/moz_mobile_connections/moz_mobile_connections_shim.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel',
'MozMobileConnections',
'MozSettingsShim'
]).init();
suite('MozMobileConnectionsShim >', function() {
var serviceStub;
const APP_ID = 'fake-app-id';
MocksHelperForAttachment.attachTestHelpers();
suiteSetup(function() {
serviceStub = sinon.stub({
method: () => {},
listen: () => {}
});
sinon.spy(window, 'BroadcastChannel');
sinon.stub(bridge, 'service').returns(serviceStub);
});
setup(function() {
MozMobileConnectionsShim.init(APP_ID, MockMozMobileConnections);
});
test('bridge service is correctly initialized', function() {
sinon.assert.calledOnce(bridge.service);
sinon.assert.calledWith(bridge.service, 'moz-mobile-connections-shim');
sinon.assert.calledTwice(serviceStub.listen);
sinon.assert.calledWith(
serviceStub.listen,
sinon.match.instanceOf(BroadcastChannel)
);
sinon.assert.calledWith(
BroadcastChannel,
'moz-mobile-connections-shim-channel-' + APP_ID
);
});
suite('methods for wrapping up API', function() {
suite('getServiceIdByIccId', function() {
test('returns the correct id', function() {
assert.equal(MozMobileConnectionsShim.getServiceIdByIccId('SIM 1'), 0);
assert.equal(MozMobileConnectionsShim.getServiceIdByIccId('SIM 2'), 1);
assert.isNull(MozMobileConnectionsShim.getServiceIdByIccId('SIM 3'));
});
});
suite('switchMmsSimHandler', function() {
setup(function() {
this.sinon.stub(MozSettingsShim, 'set');
});
test('serviceId is null', function(done) {
MozMobileConnectionsShim.switchMmsSimHandler('SIM 3').then(() => {
throw new Error('Resolve should not be called');
}, (err) => {
assert.equal(err, 'NoSimCardError');
}).then(done, done);
});
test('mozMobileConnection is already registered', function(done) {
MockMozMobileConnections[0].data.state = 'registered';
MozMobileConnectionsShim.switchMmsSimHandler('SIM 1').then(() => {
// No need to set the default serviceId
sinon.assert.notCalled(MozSettingsShim.set);
}).then(done, done);
});
test('switch sim and return when state is ready', function(done) {
this.sinon.stub(MockMozMobileConnections[0], 'addEventListener');
this.sinon.stub(MockMozMobileConnections[0], 'removeEventListener');
MockMozMobileConnections[0].data.state = 'searching';
MozMobileConnectionsShim.switchMmsSimHandler('SIM 1').then(() => {
sinon.assert.calledWith(MozSettingsShim.set, {
'ril.mms.defaultServiceId': 0,
'ril.data.defaultServiceId': 0
});
sinon.assert.called(MockMozMobileConnections[0].removeEventListener);
}).then(done, done);
// Change target connection state to registered and invoke the event
MockMozMobileConnections[0].data.state = 'registered';
MockMozMobileConnections[0].addEventListener.yield();
});
});
});
});

View file

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

View file

@ -0,0 +1,159 @@
/*global bridge,
BroadcastChannel,
MozMobileMessageClient,
MocksHelper,
streamClient
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
require('/services/js/moz_mobile_message/moz_mobile_message_client.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel',
'streamClient'
]).init();
suite('MozMobileMessageClient >', function() {
var clientStub;
function matchMobileMessageShim(channelName) {
return {
service: 'moz-mobile-message-shim',
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
sinon.match.has('name', channelName)
),
timeout: false
};
}
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
clientStub = sinon.stub({
method: () => {},
stream: () => {},
plugin: () => {}
});
clientStub.plugin.returns(clientStub);
this.sinon.spy(self, 'BroadcastChannel');
this.sinon.stub(bridge, 'client').withArgs(
matchMobileMessageShim('moz-mobile-message-shim-channel-1')
).returns(clientStub);
});
teardown(function() {
MozMobileMessageClient.cleanup();
});
test('throws if app instance id is not provided', function() {
assert.throws(() => MozMobileMessageClient.forApp());
});
test('spawns bridge client for every app instance', function() {
// Prepare stub for the second app instance.
var clientStub2 = sinon.stub({
method: () => {},
stream: () => {},
plugin: () => {}
});
clientStub2.plugin.returns(clientStub2);
bridge.client.withArgs(
matchMobileMessageShim('moz-mobile-message-shim-channel-2')
).returns(clientStub2);
var mobileMessageClient1 = MozMobileMessageClient.forApp(1);
sinon.assert.calledOnce(bridge.client);
sinon.assert.calledWith(
bridge.client,
matchMobileMessageShim('moz-mobile-message-shim-channel-1')
);
sinon.assert.calledWith(clientStub.plugin, streamClient);
var mobileMessageClient2 = MozMobileMessageClient.forApp(2);
sinon.assert.calledTwice(bridge.client);
sinon.assert.calledWith(
bridge.client,
matchMobileMessageShim('moz-mobile-message-shim-channel-2')
);
sinon.assert.calledWith(clientStub2.plugin, streamClient);
// Once we created client for the app instance we should reuse for
// consequent requests.
bridge.client.reset();
MozMobileMessageClient.forApp(1);
MozMobileMessageClient.forApp(2);
sinon.assert.notCalled(bridge.client);
// Check that method calls go to the correct client.
mobileMessageClient1.send();
sinon.assert.calledOnce(clientStub.method);
sinon.assert.notCalled(clientStub2.method);
mobileMessageClient2.send();
sinon.assert.calledOnce(clientStub.method);
sinon.assert.calledOnce(clientStub2.method);
});
suite('method calls', function() {
var mobileMessageClient, methodResult, streamResult;
setup(function() {
methodResult = sinon.stub();
streamResult = sinon.stub();
clientStub.method.returns(methodResult);
clientStub.stream.returns(streamResult);
mobileMessageClient = MozMobileMessageClient.forApp(1);
});
test('send is correctly forwarded to bridge client', function() {
mobileMessageClient.send('recipient', 'content', { serviceId: 100 });
sinon.assert.calledWith(
clientStub.method, 'send', 'recipient', 'content', { serviceId: 100 }
);
});
test('sendMMS is correctly forwarded to bridge client', function() {
var mmsOptions = {
receivers: ['+1'],
subject: 'subject',
smil: 'smil',
attachments: [{}]
};
mobileMessageClient.sendMMS(mmsOptions, { serviceId: 100 });
sinon.assert.calledWith(
clientStub.method, 'sendMMS', mmsOptions, { serviceId: 100 }
);
});
test('retrieveMMS is correctly forwarded to bridge client', function() {
mobileMessageClient.retrieveMMS(100);
sinon.assert.calledWith(clientStub.method, 'retrieveMMS', 100);
});
test('delete is correctly forwarded to bridge client', function() {
mobileMessageClient.delete(100);
sinon.assert.calledWith(clientStub.method, 'delete', 100);
});
test('getThreads is correctly forwarded to bridge client', function() {
mobileMessageClient.getThreads();
sinon.assert.calledWith(clientStub.stream, 'getThreads');
});
});
});

View file

@ -0,0 +1,391 @@
/*global bridge,
BroadcastChannel,
DOMError,
MockNavigatormozMobileMessage,
MocksHelper,
MozMobileMessageShim,
streamService
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/test/unit/mock_navigatormoz_sms.js');
require('/views/shared/test/unit/mock_broadcast_channel.js');
require('/views/shared/js/utils.js');
require('/services/js/bridge_service_mixin.js');
require('/services/js/moz_mobile_message/moz_mobile_message_shim.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge',
'BroadcastChannel',
'streamService'
]).init();
suite('MozMobileMessageShim >', function() {
var serviceStub;
const APP_ID = 'fake-app-id';
MocksHelperForAttachment.attachTestHelpers();
suiteSetup(function() {
serviceStub = sinon.stub({
method: () => {},
stream: () => {},
broadcast: () => {},
plugin: () => {},
listen: () => {}
});
sinon.spy(window, 'BroadcastChannel');
sinon.stub(bridge, 'service').returns(serviceStub);
sinon.stub(MockNavigatormozMobileMessage, 'addEventListener');
});
setup(function() {
MozMobileMessageShim.init(APP_ID, MockNavigatormozMobileMessage);
});
test('bridge service is correctly initialized', function() {
sinon.assert.calledOnce(bridge.service);
sinon.assert.calledWith(bridge.service, 'moz-mobile-message-shim');
sinon.assert.calledWith(serviceStub.plugin, streamService);
sinon.assert.calledTwice(serviceStub.listen);
sinon.assert.calledWith(
serviceStub.listen,
sinon.match.instanceOf(BroadcastChannel)
);
sinon.assert.calledWith(
BroadcastChannel,
'moz-mobile-message-shim-channel-' + APP_ID
);
});
suite('event broadcast', function() {
var messageEvent, deleteEvent, eventMap;
setup(function() {
messageEvent = { message: {} };
deleteEvent = { deletedThreadIds: [1, 2, 3] };
eventMap = new Map([
['sending', {
apiEvent: messageEvent,
broadcastEventName: 'message-sending',
broadcastParameters: { message: messageEvent.message }
}],
['failed', {
apiEvent: messageEvent,
broadcastEventName: 'message-failed-to-send',
broadcastParameters: { message: messageEvent.message }
}],
['deliverysuccess', {
apiEvent: messageEvent,
broadcastEventName: 'message-delivered',
broadcastParameters: { message: messageEvent.message }
}],
['readsuccess', {
apiEvent: messageEvent,
broadcastEventName: 'message-read',
broadcastParameters: { message: messageEvent.message }
}],
['sent', {
apiEvent: messageEvent,
broadcastEventName: 'message-sent',
broadcastParameters: { message: messageEvent.message }
}],
['deleted', {
apiEvent: deleteEvent,
broadcastEventName: 'threads-deleted',
broadcastParameters: { ids: deleteEvent.deletedThreadIds }
}]
]);
});
test('event broadcasted by service with correct parameter', function() {
eventMap.forEach((eventDescription, apiEventName) => {
MockNavigatormozMobileMessage.addEventListener
.withArgs(apiEventName).yield(eventDescription.apiEvent);
sinon.assert.calledWith(
serviceStub.broadcast,
eventDescription.broadcastEventName,
eventDescription.broadcastParameters
);
});
});
});
suite('methods for wrapping up API', function() {
setup(function() {
this.sinon.spy(MockNavigatormozMobileMessage, 'getMessage');
this.sinon.stub(MockNavigatormozMobileMessage, 'retrieveMMS');
this.sinon.spy(MockNavigatormozMobileMessage, 'send');
this.sinon.spy(MockNavigatormozMobileMessage, 'sendMMS');
this.sinon.spy(MockNavigatormozMobileMessage, 'markMessageRead');
this.sinon.spy(MockNavigatormozMobileMessage, 'getSegmentInfoForText');
MockNavigatormozMobileMessage.delete = sinon.stub();
});
test('getMessage', function() {
var args = [1];
assert.equal(
MozMobileMessageShim.getMessage(...args),
MockNavigatormozMobileMessage.getMessage.firstCall.returnValue
);
sinon.assert.calledWith(
MockNavigatormozMobileMessage.getMessage,
...args
);
});
suite('retrieveMMS', function() {
var args;
setup(function() {
args = [1];
});
test('success', function(done) {
MockNavigatormozMobileMessage.retrieveMMS.returns(Promise.resolve({
id: 'fake message'
}));
MozMobileMessageShim.retrieveMMS(...args).then(() => {
sinon.assert.calledWith(
MockNavigatormozMobileMessage.retrieveMMS,
...args
);
}).then(done, done);
});
test('DOMError', function(done) {
var error = new DOMError('fakeError');
MockNavigatormozMobileMessage.retrieveMMS.returns(
Promise.reject(error)
);
MozMobileMessageShim.retrieveMMS(...args).catch((err) => {
assert.equal(err, error);
sinon.assert.calledWith(
MockNavigatormozMobileMessage.retrieveMMS,
...args
);
}).then(done, done);
});
test('other error', function(done) {
var error = 'fakeError';
MockNavigatormozMobileMessage.retrieveMMS.returns(
Promise.reject(error)
);
MozMobileMessageShim.retrieveMMS(...args).catch((err) => {
assert.equal(err, error);
sinon.assert.calledWith(
MockNavigatormozMobileMessage.retrieveMMS,
...args
);
}).then(done, done);
});
});
test('send', function() {
var args = [['123'], 'body', {}];
assert.equal(
MozMobileMessageShim.send(...args),
MockNavigatormozMobileMessage.send.firstCall.returnValue
);
sinon.assert.calledWith(MockNavigatormozMobileMessage.send, ...args);
});
test('sendMMS', function() {
var args = [{}, {}];
assert.equal(
MozMobileMessageShim.sendMMS(...args),
MockNavigatormozMobileMessage.sendMMS.firstCall.returnValue
);
sinon.assert.calledWith(MockNavigatormozMobileMessage.sendMMS, ...args);
});
test('delete', function() {
var args = [1];
assert.equal(
MozMobileMessageShim.delete(...args),
MockNavigatormozMobileMessage.delete.firstCall.returnValue
);
sinon.assert.calledWith(MockNavigatormozMobileMessage.delete, ...args);
});
test('markMessageRead', function() {
var args = [1, true, {}];
assert.equal(
MozMobileMessageShim.markMessageRead(...args),
MockNavigatormozMobileMessage.markMessageRead.firstCall.returnValue
);
sinon.assert.calledWith(
MockNavigatormozMobileMessage.markMessageRead,
...args
);
});
test('getSegmentInfoForText', function() {
var args = ['body'];
var mockAPI = MockNavigatormozMobileMessage.getSegmentInfoForText;
assert.equal(
MozMobileMessageShim.getSegmentInfoForText(...args),
mockAPI.firstCall.returnValue
);
sinon.assert.calledWith(mockAPI, ...args);
});
});
suite('streams for wrapping up API', function() {
var streamStub;
setup(function() {
streamStub = sinon.stub({
write: () => {},
close: () => {},
abort: () => {}
});
});
suite('getThreads', function() {
var cursor;
setup(function() {
cursor = {
result: {
id: 1,
body: 'body',
participants: ['+1234'],
timestamp: 0,
unreadCount: 0,
lastMessageType: 'sms'
},
continue: sinon.stub()
};
this.sinon.stub(MockNavigatormozMobileMessage, 'getThreads');
});
test('continue', function() {
MockNavigatormozMobileMessage.getThreads.returns(cursor);
MozMobileMessageShim.getThreads(streamStub);
cursor.onsuccess();
sinon.assert.calledWith(streamStub.write, cursor.result);
sinon.assert.called(cursor.continue);
});
test('stream cancelled', function() {
MockNavigatormozMobileMessage.getThreads.returns(cursor);
MozMobileMessageShim.getThreads(streamStub);
streamStub.cancel();
cursor.onsuccess();
sinon.assert.notCalled(streamStub.write);
sinon.assert.notCalled(cursor.continue);
sinon.assert.called(streamStub.close);
});
test('done', function() {
MockNavigatormozMobileMessage.getThreads.returns(cursor);
MozMobileMessageShim.getThreads(streamStub);
cursor.result = null;
cursor.onsuccess();
sinon.assert.called(streamStub.close);
});
test('error while retrieving threads', function() {
var error = new Error('retrieving error');
this.sinon.spy(console, 'error');
MockNavigatormozMobileMessage.getThreads.throws(error);
MozMobileMessageShim.getThreads(streamStub);
sinon.assert.calledWith(
console.error, 'Error occurred while retrieving threads:', error
);
sinon.assert.calledWith(streamStub.abort, '[Error] retrieving error');
});
test('error while reading the database', function() {
MockNavigatormozMobileMessage.getThreads.returns(cursor);
MozMobileMessageShim.getThreads(streamStub);
cursor.error = new Error('fake error');
this.sinon.spy(console, 'error');
cursor.onerror();
sinon.assert.calledWith(
console.error,
'Error occurred while reading the database',
cursor.error
);
sinon.assert.calledWith(streamStub.abort, '[Error] fake error');
});
});
suite('getMessages', function() {
var opts, cursor;
setup(function() {
opts = {
invert: true,
filter: {}
};
cursor = {};
this.sinon.stub(MockNavigatormozMobileMessage, 'getMessages')
.withArgs(opts.filter, !opts.invert).returns(cursor);
MozMobileMessageShim.getMessages(streamStub, opts);
});
test('continue', function() {
cursor.done = false;
cursor.result = {};
cursor.continue = sinon.stub();
cursor.onsuccess();
sinon.assert.calledWith(streamStub.write, cursor.result);
sinon.assert.called(cursor.continue);
});
test('done', function() {
cursor.done = true;
cursor.onsuccess();
sinon.assert.called(streamStub.close);
});
test('error', function() {
cursor.error = { name: 'fake error' };
this.sinon.spy(console, 'error');
cursor.onerror();
sinon.assert.calledWith(
console.error,
'Reading the database. Error: fake error'
);
sinon.assert.called(streamStub.abort);
});
test('cancel stream', function() {
streamStub.cancel();
sinon.assert.called(streamStub.close);
});
});
});
});

View file

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

View file

@ -0,0 +1,106 @@
/*global bridge,
MockNavigatorSettings,
MocksHelper,
MozSettingsShim
*/
'use strict';
require('/services/test/unit/mock_bridge.js');
require('/services/js/bridge_service_mixin.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/services/js/moz_settings/moz_settings_shim.js');
var MocksHelperForAttachment = new MocksHelper([
'bridge'
]).init();
suite('MozSettingsShim >', function() {
var serviceStub;
MocksHelperForAttachment.attachTestHelpers();
setup(function() {
serviceStub = sinon.stub({
method: () => {},
listen: () => {}
});
this.sinon.stub(bridge, 'service').returns(serviceStub);
});
test('service is correctly initialized with settings provided', function() {
MozSettingsShim.init(MockNavigatorSettings);
sinon.assert.calledOnce(bridge.service);
sinon.assert.calledWith(bridge.service, 'moz-settings-shim');
});
test('service isn\'t initialized without settings', function() {
this.sinon.stub(console, 'error');
MozSettingsShim.init();
sinon.assert.notCalled(bridge.service);
sinon.assert.called(console.error);
});
suite('Settings methods >', function() {
var lockStub;
setup(function() {
lockStub = sinon.stub({
get: () => {},
set: () => {}
});
this.sinon.stub(MockNavigatorSettings, 'createLock').returns(lockStub);
MozSettingsShim.init(MockNavigatorSettings);
});
suite('get >', function() {
test('returns value successfully', function(done) {
var key = 'key';
var value = 'result';
lockStub.get.withArgs(key).returns(Promise.resolve(value));
MozSettingsShim.get(key).then((result) => {
sinon.assert.calledWith(lockStub.get, key);
assert.equal(result, value);
}).then(done, done);
});
test('returns reject while error', function(done) {
var error = new Error('moz settings get error');
lockStub.get.returns(Promise.reject(error));
MozSettingsShim.get().then(() => {
throw new Error('Should not be resolved');
}, (result) => {
sinon.assert.called(lockStub.get);
assert.equal(result, error);
}).then(done, done);
});
});
suite('set >', function() {
test('returns revolve successfully', function(done) {
var settings = { 'key': 'value' };
lockStub.set.withArgs(settings).returns(Promise.resolve());
MozSettingsShim.set(settings).then(() => {
sinon.assert.calledWith(lockStub.set, settings);
}).then(done, done);
});
test('returns reject while error', function(done) {
var error = new Error('moz settings set error');
lockStub.set.returns(Promise.reject(error));
MozSettingsShim.set().then(() => {
throw new Error('Should not be resolved');
}, (result) => {
sinon.assert.called(lockStub.set);
assert.equal(result, error);
}).then(done, done);
});
});
});
});

View file

@ -0,0 +1,315 @@
/*global Thread, Threads, MockMessages, Drafts, MocksHelper */
'use strict';
require('/services/js/threads.js');
require('/services/test/unit/mock_drafts.js');
require('/views/shared/test/unit/mock_messages.js');
var mocksHelperForThreadsTest = new MocksHelper([
'Drafts'
]).init();
function assertDeepEqual(test, expected) {
for (var key in expected) {
assert.deepEqual(test[key], expected[key]);
}
}
suite('Threads', function() {
var message;
mocksHelperForThreadsTest.attachTestHelpers();
setup(function() {
// Create a message with read status 'true'
message = MockMessages.sms();
});
teardown(function() {
Threads.clear();
});
suite('create', function() {
test('Thread.fromMessage, Thread.fromDraft', function() {
assert.ok(Thread.fromMessage);
assert.ok(Thread.fromDraft);
});
test(' > create with unread status in options', function() {
var options = { unread: true };
var thread = Thread.create(message, options);
assert.equal(thread.unreadCount, 1);
});
test(' > create without options', function() {
var thread = Thread.create(message);
assert.equal(thread.unreadCount, 0);
});
test(' > create with read status in options', function() {
var options = { read: true };
var thread = Thread.create(message, options);
assert.equal(thread.unreadCount, 0);
});
test(' > create from SMS sended', function() {
var options = { read: true };
message.delivery = 'sent';
var thread = Thread.create(message, options);
assert.equal(thread.participants.length, 1);
assert.equal(thread.participants[0], message.receiver);
});
test(' > create from SMS received', function() {
var options = { unread: true };
message.delivery = 'received';
var thread = Thread.create(message, options);
assert.equal(thread.participants.length, 1);
assert.equal(thread.participants[0], message.sender);
});
test(' > create from MMS sending', function() {
var options = { read: true };
var mms = MockMessages.mms();
mms.delivery = 'sent';
var thread = Thread.create(mms, options);
assert.equal(thread.participants, mms.receivers);
});
test(' > create from MMS received', function() {
var options = { unread: true };
var mms = MockMessages.mms();
mms.delivery = 'received';
var thread = Thread.create(mms, options);
assert.equal(thread.participants.length, 1);
assert.equal(thread.participants[0], message.sender);
});
test(' > create from Draft with null threadId', function() {
var now = Date.now();
var thread = Thread.create({
id: 1,
recipients: ['555'],
content: ['This is a new draft for thread 44'],
subject: 'This is a subject',
timestamp: now,
threadId: null,
type: 'sms'
});
assertDeepEqual(thread, {
// id was used
id: 1,
participants: ['555'],
body: 'This is a new draft for thread 44',
timestamp: new Date(now),
unreadCount: 0,
lastMessageSubject: undefined,
lastMessageType: 'sms',
messages: new Map()
});
});
test(' > create from Draft with a threadId', function() {
var now = Date.now();
var thread = Thread.create({
id: 1,
recipients: ['555'],
content: ['This is a new draft for thread 44'],
subject: 'This is a subject',
timestamp: now,
threadId: 44,
type: 'sms'
});
assertDeepEqual(thread, {
// threadId was used
id: 44,
participants: ['555'],
body: 'This is a new draft for thread 44',
timestamp: new Date(now),
unreadCount: 0,
lastMessageSubject: undefined,
lastMessageType: 'sms',
messages: new Map()
});
});
});
suite('Collection', function() {
test('is like a Map', function() {
assert.ok(Threads);
assert.isFunction(Threads.set);
assert.isFunction(Threads.get);
assert.isFunction(Threads.has);
assert.isFunction(Threads.delete);
assert.isFunction(Threads.clear);
assert.isNumber(Threads.size);
});
test('Threads.set(key, val)', function() {
Threads.set(1, {});
assertDeepEqual(Threads.get(1), {
body: undefined,
id: undefined,
lastMessageSubject: undefined,
lastMessageType: undefined,
participants: undefined,
timestamp: undefined,
unreadCount: undefined,
messages: new Map()
});
assert.equal(Threads.size, 1);
});
test('Threads.get(key)', function() {
Threads.set(1, {});
var value = Threads.get(1);
assert.ok(value.messages instanceof Map);
assert.equal(Threads.size, 1);
});
test('Threads.has(key)', function() {
assert.equal(Threads.has(1), false);
Threads.set(1, {});
assert.equal(Threads.has(1), true);
});
test('Threads.delete()', function() {
Threads.set(1, {});
assert.equal(Threads.has(1), true);
assert.equal(Threads.size, 1);
Threads.delete(1);
assert.equal(Threads.has(1), false);
assert.equal(Threads.size, 0);
});
test('Threads.delete() calls Drafts.delete()', function() {
this.sinon.spy(Drafts, 'delete');
this.sinon.stub(Drafts, 'store');
this.sinon.stub(Threads, 'get').returns({
id: 1,
getDraft: () => { return { threadId: 1 }; }
});
Threads.delete(1);
sinon.assert.calledWith(Drafts.delete, { threadId: 1 });
sinon.assert.callOrder(Drafts.delete, Drafts.store);
});
suite('Threads.registerMessage/unregisterMessage', function () {
setup(function() {
this.sinon.spy(Threads, 'get');
});
test('thread existed', function() {
Threads.set(1, {});
Threads.registerMessage(message);
assert.equal(Threads.get(1).messages.get(message.id), message);
assert.equal(Threads.Messages.get(message.id), message);
// unregister the message
Threads.unregisterMessage(message.id);
assert.isUndefined(Threads.get(1).messages.get(message.id));
});
test('thread does not exist', function() {
Threads.registerMessage(message);
assert.isTrue(Threads.has(1));
assert.equal(Threads.get(1).messages.get(message.id), message);
assert.equal(Threads.Messages.get(message.id), message);
});
test('remove the message that does not exist in Threads', function() {
Threads.unregisterMessage(message);
sinon.assert.notCalled(Threads.get);
assert.isNull(Threads.Messages.get(message.id));
});
});
});
});
suite('Thread', function() {
var date = new Date();
var fixture = {
id: 1,
participants: ['555'],
lastMessageType: 'sms',
body: 'Hello 555',
timestamp: date,
unreadCount: 0
};
mocksHelperForThreadsTest.attachTestHelpers();
teardown(function() {
Threads.clear();
});
setup(function() {
Threads.set(1, fixture);
});
test('Thread object', function() {
var thread = new Thread(fixture);
assertDeepEqual(thread, {
id: 1,
participants: ['555'],
lastMessageSubject: undefined,
lastMessageType: 'sms',
body: 'Hello 555',
timestamp: date,
unreadCount: 0,
messages: new Map()
});
});
test('thread has draft', function() {
var draft = {
id: 101,
recipients: ['555'],
content: ['This is a new draft for thread 1'],
subject: 'This is a subject',
timestamp: 2,
threadId: 1,
type: 'sms'
};
this.sinon.stub(Drafts, 'byThreadId').withArgs(1).returns(draft);
Threads.set(1, {
id: 1,
participants: ['555'],
lastMessageType: 'sms',
body: 'Hello 555',
timestamp: date,
unreadCount: 0,
messages: new Map()
});
assert.deepEqual(Threads.get(1).getDraft(), draft);
});
test('thread does not have draft', function() {
this.sinon.stub(Drafts, 'byThreadId').withArgs(1).returns(null);
Threads.set(1, {
id: 1,
participants: ['555'],
lastMessageType: 'sms',
body: 'Hello 555',
timestamp: date,
unreadCount: 0,
messages: new Map()
});
assert.isNull(Threads.get(1).getDraft());
});
});

Binary file not shown.

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('SMS tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=sms make', function(done) {
helper.exec('APP=sms make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/sms.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,20 @@
BEGIN:VCARD
VERSION:4.0
N:Gump;Forrest;;;
FN:Forrest Gump
ORG:Bubba Gump Shrimp Co.
TITLE:Shrimp Man
PHOTO:data:image/bmp;base64,Qk02BAAAAAAAADYAAAAoAAAAEAAAAPD///8BACAAAAAAAAAAAABiBwAAYgcAAAAAAAAAAAAA///////////////////////////////////////////////////+///////////////////////////////////////////////////////////////////////V0s3/b21l/1pbVf9ydHL/u8HB//D09f/29vb///////////////////////////////////////Lx8P9ycWv/EhEO/wAAAP8AAgD/Gh0Z/15mav+tt7z/6+zr/////////////v////////////////////////+IiYf/AgYF/wAAAP8AAAD/AAAA/x8iI/88Ojv/e3l6///+/v/////////////////////////////////p5+f/am5z/xoeI/8iKDH/HSIr/ztATP+wtL//sbfC/zI4P//Ky8r///////////////////////7+/f/s7O3/vcDH/2Fkbf8+Qk7/foSW/5Wcsv+5vtP/7ezz//////97g5P/j5Oh//f4+P//////+/v7//Py8v/z8vP/9/j5/8jL2/8zNkD/MDM4/3Nydf/X0+H/0cvZ/7Srrv/Hvrz/h4uX/2Vsf/+5vcP/5OXj//Tz8//5+fn///////7+/v+eoa3/NjxE/2Vnaf9vbGr/pqSt/+ro8v+xqrP/6ePi/4SLnP85QE//e4SQ/5CWmv////////////z8+//X1tf/kZWc/09UXP9hY2f/TEhL/62twP/o6vP/n5ij/9LM0f+WnbX/PkRS/4CFkv99gor//f39//j39//19PP/+Pj3//////+ysrj/cnqP/21viv/c2On/9PL3//Hv+v/l5/T/x8jd/7Cvv//Y0NX/5NzD//X19f/7+/v///////Py9v/W3uf/z9Xa/4WFm/9UVGn/tK3D/+/o8v/Y0uD/9fL8/+3q9f/w7fn/+/n+///////////////////////08/b/2eLo/8XT2P9zdYb/XFxy/8S80//68vz/urTE/9XU5//j4+//9vX6////////////7e/u/+Dk5v/a4OT/8vf5///////g5+n/kZKe/25of/9sa3D/ZGNn/2pndf/ExdP/0tTf/+nm4//+/Pr//////87V2v+KmKf/cYOV/3mMn/+NnK3/TVll/1dYZP9kYHn/ODBG/yAcMP9fXW//ubzK/9PW3/9JREX/OTIu/7OxsP//////y9HZ/4WSov9xfo//TVhx/x8nNP8ZGiH/Y2F//3Bphv9vZYj/mZm3/7C1yv/Ey9n/DREX/wYIDf9ZaYb//v7+/8vS2v+Hk6T/h4+c/2hxg/8iJi//AwQE/0pHYP9nX4f/kIuv/4WIo/+GjaT/doOb/wECBv8dIS3/VWWL/w==
TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212
TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212
ADR;TYPE=work;LABEL="100 Waters Edge
Baytown,LA 30314
United States of America"
:;;100 Waters Edge;Baytown;LA;30314;United States of America
ADR;TYPE=home;LABEL="42 Plantation St.
Baytown,LA 30314
United States of America"
:;;42 Plantation St.;Baytown;LA;30314;United States of America
EMAIL:forrestgump@example.com
REV:20080424T195243Z
END:VCARD

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Messages Activity Caller</title>
<script defer src="/js/activity_caller.js"></script>
</head>
<body>
<div>I am Messages activity caller</div>
<button id="share-image">Share Image</button>
<button id="pick-image">Pick Image</button>
<button id="pick-contact">Pick Contact</button>
<button id="send-message">Send message</button>
</body>
</html>

View file

@ -0,0 +1,113 @@
/* global MozActivity */
(function(window) {
'use strict';
function generateCanvas(width, height) {
var canvas = document.createElement('canvas');
var 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;
}
function generateImageBlob(width, height, type, quality) {
var canvas = generateCanvas(width, height);
return new Promise(function(resolve) {
canvas.toBlob(function(blob) {
canvas.width = canvas.height = 0;
canvas = null;
resolve(blob);
}, type, quality);
});
}
function generateVcardBlob() {
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/data/vcard_4.vcf');
xhr.overrideMimeType('text/vcard');
xhr.responseType = 'blob';
xhr.onload = function() {
resolve(xhr.response);
};
xhr.send();
});
}
window.addEventListener('load', function() {
var shareImageButton = document.getElementById('share-image');
shareImageButton.addEventListener('click', function() {
generateImageBlob(100, 100, 'image/jpeg').then(function(blob) {
return new MozActivity({
name: 'share',
data: {
type: 'image/*',
number: 1,
blobs: [blob],
filenames: 'image.jpg',
filepaths: 'image.jpg'
}
});
}).catch(function(e) {
console.warn('share activity error:', e.message || e.name);
});
});
var sendMessageButton = document.getElementById('send-message');
sendMessageButton.addEventListener('click', function() {
var activity = new MozActivity({
name: 'new',
data: {
type: 'websms/sms',
number: this.dataset.number,
body: this.dataset.body
}
});
activity.onerror = function() {
console.warn('new activity error:', activity.error.name);
};
});
});
window.navigator.mozSetMessageHandler('activity', function(activity) {
var pick = document.getElementById('pick-image');
pick.addEventListener('click', function() {
generateImageBlob(100, 100, 'image/jpeg').then(function(blob) {
activity.postResult({
type: blob.type,
blob: blob
});
}).catch(function(e) {
activity.postError(e);
});
});
var pickContactButton = document.getElementById('pick-contact');
pickContactButton.addEventListener('click', function() {
generateVcardBlob().then(function(blob) {
activity.postResult({
name: 'test_file.vcf',
blob: blob
});
});
});
});
})(window);

View file

@ -0,0 +1,20 @@
{
"name": "Messages Activity Caller",
"description": "Messages activity caller app",
"type": "certified",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"activities": {
"pick": {
"filters": {
"type": ["image/*", "image/jpeg", "image/png", "text/vcard"]
},
"disposition": "inline",
"returnValue": true,
"href": "/index.html#pick"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,68 @@
/* global require, marionette, setup, suite, test, __dirname */
'use strict';
var assert = require('chai').assert;
var Messages = require('./lib/messages.js');
var MessagesActivityCaller = require('./lib/messages_activity_caller.js');
marionette('Attachment picking and sending tests', function() {
var apps = {};
apps[MessagesActivityCaller.ORIGIN] = __dirname + '/apps/activitycaller';
var client = marionette.client({
profile: {
prefs: {
'focusmanager.testmode': true
},
apps: apps
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var messagesApp, activityCallerApp;
setup(function() {
messagesApp = Messages.create(client);
activityCallerApp = MessagesActivityCaller.create(client);
client.loader.getMockManager('sms').inject([
'navigator_moz_icc_manager',
'navigator_moz_mobile_message'
]);
});
suite('Test Suite', function() {
setup(function() {
messagesApp.launch();
messagesApp.Inbox.navigateToComposer();
});
test('A contact can be enclosed as attachment', function() {
messagesApp.addRecipient('+346666666');
var composer = messagesApp.Composer;
client.waitFor(function() {
return composer.attachButton.enabled();
});
composer.attachButton.tap();
messagesApp.Menu.selectSystemMenuOption('Messages Activity Caller');
activityCallerApp.switchTo();
activityCallerApp.pickContact();
messagesApp.switchTo();
messagesApp.send();
client.helper.waitForElement(messagesApp.Selectors.Message.
vcardAttachment);
var fileName = client.helper.waitForElement(messagesApp.Selectors.
Message.fileName);
assert.equal(fileName.text(), 'test_file.vcf');
});
});
});

View file

@ -0,0 +1,354 @@
/* global require, marionette, setup, suite, test, __dirname */
'use strict';
var assert = require('chai').assert;
var Messages = require('./lib/messages.js');
var InboxView = require('./lib/views/inbox/view');
var MessagesActivityCaller = require('./lib/messages_activity_caller.js');
var Storage = require('./lib/storage.js');
var Tools = require('./lib/views/shared/tools.js');
marionette('Messages Composer', function() {
var apps = {};
apps[MessagesActivityCaller.ORIGIN] = __dirname + '/apps/activitycaller';
var client = marionette.client({
profile: {
prefs: {
'focusmanager.testmode': true
},
apps: apps
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var messagesApp, activityCallerApp;
var storage;
function assertIsDisplayed(element) {
assert.isTrue(element.displayed(), 'Element should be displayed');
}
function assertIsNotDisplayed(element) {
assert.isFalse(element.displayed(), 'Element should not be displayed');
}
setup(function() {
messagesApp = Messages.create(client);
storage = Storage.create(client);
activityCallerApp = MessagesActivityCaller.create(client);
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_icc_manager',
'navigator_moz_mobile_message',
'navigator_moz_contacts'
]);
});
suite('Preserve message input while navigating', function() {
var inboxView, conversationView;
var message = 'test message';
setup(function() {
messagesApp.launch();
inboxView = new InboxView(client);
var newMessageView = inboxView.createNewMessage();
newMessageView.addNewRecipient('a@b.c');
newMessageView.addNewRecipient('s@p.c');
newMessageView.typeMessage('MMS thread.');
conversationView = newMessageView.send();
conversationView.typeMessage(message);
});
test('Message input is preserved when navigating to and from group-view',
function() {
var participantsView = conversationView.openParticipants();
participantsView.back();
assert.equal(conversationView.messageText, message);
});
test('Message input is preserved when navigating to and from ' +
'message-report', function() {
var reportView = conversationView.openReport(
conversationView.messages()[0].id
);
reportView.back();
assert.equal(conversationView.messageText, message);
});
});
suite('Sending a message', function() {
setup(function() {
messagesApp.launch();
messagesApp.Inbox.navigateToComposer();
// Create new thread from the scratch
messagesApp.addRecipient('+1');
messagesApp.Composer.messageInput.sendKeys('message');
messagesApp.send();
});
test('Header should be updated accordingly', function() {
client.helper.waitForElement(messagesApp.Conversation.message);
assert.equal(
messagesApp.Conversation.headerTitle.text(),
'+1',
'Header title should display contact phone number'
);
});
});
suite('Messages Composer Test Suite', function() {
var message = 'long long long long long message long long message long ' +
'message long long message message long message long message long ' +
'for message long message long message long message long message long';
setup(function() {
messagesApp.launch();
messagesApp.Inbox.navigateToComposer();
});
test('Message char counter and MMS label', function() {
var composer = messagesApp.Composer;
// Case #1: When we open composer initially we should not see neither char
// counter nor MMS label.
assertIsNotDisplayed(composer.charCounter);
assertIsNotDisplayed(composer.mmsLabel);
// Case #2: Focus on message editor and enter some text. If we have more
// than 20 chars left then we don't show available char counter yet
composer.messageInput.tap();
composer.messageInput.sendKeys(message.slice(0, 70));
assertIsNotDisplayed(composer.charCounter);
assertIsNotDisplayed(composer.mmsLabel);
// Case #3: Enter some more text to see available char counter
composer.messageInput.sendKeys(message.slice(0, 70));
client.helper.waitForElement(composer.charCounter);
assert.equal(composer.charCounter.text(), '20/1');
assertIsNotDisplayed(composer.mmsLabel);
// Case #4: Remove one character and char counter should disappear
composer.messageInput.sendKeys(Messages.Chars.BACKSPACE);
client.helper.waitForElementToDisappear(composer.charCounter);
assertIsNotDisplayed(composer.mmsLabel);
// Case #5: Add empty subject, if subject is empty then we still have SMS
// and char counter can be displayed
messagesApp.showSubject();
assert.equal(composer.subjectInput.text(), '');
assertIsNotDisplayed(composer.mmsLabel);
assertIsNotDisplayed(composer.charCounter);
// Case #6: Enter some text to message input to show available char
// counter while subject is empty
composer.messageInput.sendKeys('ab');
client.helper.waitForElement(composer.charCounter);
assert.equal(composer.charCounter.text(), '19/1');
assertIsNotDisplayed(composer.mmsLabel);
// Case #7: Enter some text to subject input. Message should be converted
// to MMS and appropriate label should appear near subject field,
// available char counter should disappear
composer.subjectInput.sendKeys('subject');
client.helper.waitForElement(composer.mmsLabel);
client.helper.waitForElementToDisappear(composer.charCounter);
// Case #8: Enter some text to message input while subject is not empty
// and visible. In this case we still update available char counter, but
// doesn't display it to the user as currently message is MMS
composer.messageInput.sendKeys('cd');
assertIsDisplayed(composer.mmsLabel);
assertIsNotDisplayed(composer.charCounter);
// Case #9: Remove subject when there is enough text to show available
// char counter (with actual value), message converted to SMS
messagesApp.hideSubject();
client.helper.waitForElementToDisappear(composer.mmsLabel);
client.helper.waitForElement(composer.charCounter);
assert.equal(composer.charCounter.text(), '17/1');
// Case #10: Add attachment, message is converted to MMS and appropriate
// label appears in the subject line.
client.waitFor(function() {
return composer.attachButton.enabled();
}.bind(this));
composer.attachButton.tap();
messagesApp.Menu.selectSystemMenuOption('Messages Activity Caller');
activityCallerApp.switchTo();
activityCallerApp.pickImage();
messagesApp.switchTo();
client.helper.waitForElement(composer.mmsLabel);
client.helper.waitForElementToDisappear(composer.charCounter);
// Since we have an attachment we don't track available characters and old
// available char value should be removed to not confusing user
assert.equal(composer.charCounter.text(), '');
// Case #11: show subject when we have an attachment, message is still
// MMS.
messagesApp.showSubject();
assertIsDisplayed(composer.mmsLabel);
assertIsNotDisplayed(composer.charCounter);
// Case #12: remove subject when we have an attachment, message is still
// MMS
messagesApp.hideSubject();
assertIsDisplayed(composer.mmsLabel);
assertIsNotDisplayed(composer.charCounter);
// Case #13: edit some text and remove attachment, in this case message is
// converted to SMS and we should show actual available char counter
composer.messageInput.sendKeys('ef');
composer.attachments[0].scriptWith(function(el) {
el.scrollIntoView(false);
});
// Remove this workaround once Marionette bug is resolved:
// "Bug 1046706 - "tap" does not find the element after scrolling in APZC"
client.helper.wait(600);
composer.attachments[0].tap();
messagesApp.Menu.selectAppMenuOption('Remove image');
client.helper.waitForElementToDisappear(composer.mmsLabel);
client.helper.waitForElement(composer.charCounter);
assert.equal(composer.charCounter.text(), '15/1');
// Case #14: add an email recipient, the message is converted to MMS.
messagesApp.addRecipient('a@b.com');
client.helper.waitForElement(composer.mmsLabel);
// Case #15: remove the email recipient, the message is converted to SMS.
messagesApp.clearRecipient();
client.helper.waitForElementToDisappear(composer.mmsLabel);
});
test('Subject focus management', function() {
var composer = messagesApp.Composer;
// Case #1: Add subject input, once added it should be focused
messagesApp.showSubject();
Tools.assertElementFocused(composer.subjectInput,
'Subject input should be focused');
// Case #2: Hide subject field, focus should be moved to message field
messagesApp.hideSubject();
Tools.assertElementFocused(composer.messageInput,
'Message input should be focused');
// Case #3: Focus should be moved to message input when subject is removed
// by user with backspace key as well
messagesApp.showSubject();
Tools.assertElementFocused(composer.subjectInput,
'Subject input should be focused');
composer.subjectInput.sendKeys(Messages.Chars.BACKSPACE);
Tools.assertElementFocused(composer.messageInput,
'Message input should be focused');
});
});
suite('Recipients', function() {
var newMessage;
var contact = {
name: ['Existing Contact'],
givenName: ['Existing'],
familyName: ['Contact'],
tel: [{
value: '5551234567',
type: 'Mobile'
}]
};
setup(function() {
storage.setContactsStorage([contact]);
messagesApp.launch();
var inbox = new InboxView(client);
newMessage = inbox.createNewMessage();
});
test('should match an existing contact', function() {
newMessage.addNewRecipient('Existing Contact');
assert.deepEqual(contact.name, newMessage.recipients);
assert.equal(contact.tel[0].value, newMessage.recipientsPhoneNumbers);
});
suite('Invalid recipients', function() {
suite('Recipients list', function() {
test('should display that a non existing contact is invalid',
function() {
newMessage.addNewRecipient('non_exisiting_contact');
assert.isTrue(newMessage.containsInvalidRecipients());
});
test('should allow to correct an invalid contact', function() {
newMessage.addNewRecipient('non_exisiting_contact');
newMessage.clearRecipients();
newMessage.addNewRecipient(123);
assert.lengthOf(newMessage.recipients, 1);
assert.equal(newMessage.recipients[0], '123');
assert.isFalse(newMessage.containsInvalidRecipients());
});
});
suite('Content composer', function() {
test('should not enable send button if the contact is invalid',
function() {
newMessage.addNewRecipient('invalidContact');
newMessage.typeMessage('Test message');
assert.isFalse(newMessage.isSendButtonEnabled());
});
});
});
suite('Semicolon as separator', function() {
var separator = ';';
test('should complete the entered recipients', function() {
newMessage.addNewRecipient(123, separator);
assert.lengthOf(newMessage.recipients, 1);
assert.notInclude(newMessage.recipients[0], separator);
});
test('should leave the input ready to accept another recipient',
function() {
newMessage.addNewRecipient(123, separator);
newMessage.addNewRecipient(456, separator);
assert.lengthOf(newMessage.recipients, 2);
});
});
});
});

View file

@ -0,0 +1,683 @@
/* global require, marionette, setup, suite, test, __dirname */
'use strict';
var assert = require('chai').assert;
var _ = require('SMS/node_modules/lodash');
var ThreadGenerator = require('./generators/thread');
var Messages = require('./lib/messages.js');
var InboxView = require('./lib/views/inbox/view');
var Storage = require('./lib/storage.js');
var Tools = require('./lib/views/shared/tools.js');
var MessagesActivityCaller = require('./lib/messages_activity_caller.js');
var Keyboard = require('../../../keyboard/test/marionette/lib/keyboard.js');
marionette('Conversation Panel Tests', function() {
var apps = {};
apps[MessagesActivityCaller.ORIGIN] = __dirname + '/apps/activitycaller';
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false },
profile: {
prefs: {
'focusmanager.testmode': true
},
apps: apps
}
});
var messagesApp, storage, activityCallerApp, keyboardApp;
function assertIsDisplayed(element) {
assert.isTrue(element.displayed(), 'Element should be displayed');
}
setup(function() {
messagesApp = Messages.create(client);
keyboardApp = new Keyboard(client);
storage = Storage.create(client);
activityCallerApp = MessagesActivityCaller.create(client);
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_icc_manager',
'navigator_moz_mobile_message',
'navigator_moz_contacts'
]);
});
suite('General use cases', function() {
suite('Long SMS thread', function() {
var thread;
setup(function() {
thread = ThreadGenerator.generate({
numberOfMessages: 150
});
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
});
test('User can see all messages when scrolls up', function() {
messagesApp.Inbox.firstConversation.tap();
// Composer panel should always be visible
assertIsDisplayed(messagesApp.Composer.sendButton);
assertIsDisplayed(messagesApp.Composer.messageInput);
thread.messages.forEach(function(message) {
var messageNode = messagesApp.Conversation.findMessage(message.id);
// If node is not visible, let's just scroll up and wait until it
// becomes visible.
if (!messageNode.displayed()) {
client.waitFor(function() {
messagesApp.Conversation.scrollUp();
return messageNode.displayed();
});
}
});
assertIsDisplayed(messagesApp.Composer.sendButton);
assertIsDisplayed(messagesApp.Composer.messageInput);
});
});
suite('Long MMS thread', function() {
var thread;
var inboxView;
var attachmentScreenshot;
suiteSetup(function() {
attachmentScreenshot = Tools.loadAsset('mms-attachment.png');
});
suiteTeardown(function() {
attachmentScreenshot = null;
});
setup(function() {
thread = ThreadGenerator.generate({
numberOfMessages: 150,
baseTimestamp: Date.UTC(2015, 8, 1, 13, 40),
messageType: 'mms',
attachments: [
{ type: 'image/png', width: 10, height: 10 }
]
});
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
inboxView = new InboxView(client);
});
test('MMS are properly displayed after going to a subview', function() {
var conversationView = inboxView.goToConversation(thread.id);
conversationView.waitForFullRendering(thread);
var recentId = thread.messages[0].id;
var reportView = conversationView.openReport(recentId);
reportView.back();
var message = conversationView.findMessage(recentId);
var screenshot = message.attachments[0].screenshot();
assert.equal(screenshot, attachmentScreenshot);
conversationView.fakeScrollUpTo(0);
// Find the first displayed message. Displayed does not especially mean
// that we see it in the viewport, it merely means it's not "hidden"
// using CSS (eg: "display: none" here).
// I want to find the topmost already displayed message, and then scroll
// to it so that Gecko tries to load the attachment's background image.
var displayedMessage = _.find(
conversationView.messages(),
function(message) { return message.isDisplayed; }
);
conversationView.fakeScrollUpTo(0);
screenshot = displayedMessage.attachments[0].screenshot();
assert.equal(screenshot, attachmentScreenshot);
});
});
});
suite('Forward message', function() {
var smsBody = 'this is a sms body';
var mmsBody = 'this is a mms body';
var smsThread, mmsThread;
setup(function() {
smsThread = ThreadGenerator.generate({
body: smsBody
});
mmsThread = ThreadGenerator.generate({
numberOfMessages: 1,
messageType: 'mms',
participants: ['a@b.c'],
attachments: [
{ type: 'image/png', width: 10, height: 10 },
{ type: 'text/plain', content: mmsBody }
]
});
storage.setMessagesStorage(
[smsThread, mmsThread],
ThreadGenerator.uniqueMessageId
);
messagesApp.launch();
});
test('Forward a SMS', function() {
messagesApp.Inbox.findConversation(smsThread.id).tap();
// Forward message
messagesApp.contextMenu(messagesApp.Conversation.message);
messagesApp.Menu.selectAppMenuOption('Forward');
// Wait for message to be forwarded to fill out composer fields
client.waitFor(function() {
return messagesApp.Composer.messageInput.text() !== '';
});
assert.equal(
messagesApp.Composer.messageInput.text(),
smsThread.messages[0].body,
'Forwarded body is the initial body'
);
assert.equal(
messagesApp.Conversation.headerTitle.text(),
'New message',
'Header title should indicate that we are composing new message'
);
Tools.assertElementFocused(
messagesApp.NewMessage.recipientsInput,
'Recipients input should be focused'
);
});
test('Forwarding a MMS', function() {
messagesApp.Inbox.findConversation(mmsThread.id).tap();
// Forward message
messagesApp.contextMenu(messagesApp.Conversation.message);
messagesApp.Menu.selectAppMenuOption('Forward');
// Wait for message to be forwarded to fill out composer fields
client.waitFor(function() {
return messagesApp.Composer.messageInput.text() !== '';
});
assert.equal(
messagesApp.Composer.messageInput.text(),
mmsBody,
'Forwarded body is the initial body'
);
var composerAttachments = messagesApp.Composer.attachments;
assert.equal(composerAttachments.length, 1);
assert.equal(
composerAttachments[0].getAttribute('data-attachment-type'), 'img'
);
assert.equal(
messagesApp.Conversation.headerTitle.text(),
'New message',
'Header title should indicate that we are composing new message'
);
Tools.assertElementFocused(
messagesApp.NewMessage.recipientsInput,
'Recipients input should be focused'
);
});
});
suite('Action links in messages', function() {
var threads;
setup(function() {
threads = [
ThreadGenerator.generate({
participants: ['+400000'],
body: 'Call +400000 or send message to mozilla@mozilla.org'
}),
ThreadGenerator.generate({
participants: ['+100000'],
body: 'Use these numbers: +200000, +300000 or +400000'
})
];
storage.setMessagesStorage(threads, ThreadGenerator.uniqueMessageId);
storage.setContactsStorage([{
name: ['Alan Turing'],
tel: [{
value: '+300000',
type: 'Mobile'
}]
}]);
messagesApp.launch();
messagesApp.Inbox.findConversation(threads[1].id).tap();
});
test('Send message to unknown number', function() {
// Try to send message to the unknown number
var unknownNumberLink = messagesApp.Conversation.message.findElement(
'[data-dial="+200000"]'
);
unknownNumberLink.tap();
messagesApp.Menu.selectContactPromptMenuOption('Send message');
// Wait for the recipient input to be filled
var recipients = messagesApp.NewMessage.recipients;
client.waitFor(function() {
return recipients.length === 1;
});
assert.equal(recipients[0].text(), '+200000');
assert.equal(recipients[0].getAttribute('data-source'), 'manual');
Tools.assertElementFocused(
messagesApp.Composer.messageInput, 'Message input should be focused'
);
});
test('Send message to contact number', function() {
// Try to send message to number that has contact associated with it
var unknownNumberLink = messagesApp.Conversation.message.findElement(
'[data-dial="+300000"]'
);
unknownNumberLink.tap();
messagesApp.Menu.selectContactPromptMenuOption('Send message');
// Wait for the recipient input to be filled
var recipients = messagesApp.NewMessage.recipients;
client.waitFor(function() {
return recipients.length === 1;
});
assert.equal(recipients[0].text(), 'Alan Turing');
assert.equal(recipients[0].getAttribute('data-source'), 'contacts');
Tools.assertElementFocused(
messagesApp.Composer.messageInput, 'Message input should be focused'
);
});
test('Send message to thread number', function() {
// Try to send message to number that has thread associated with it
var threadNumberLink = messagesApp.Conversation.message.findElement(
'[data-dial="+400000"]'
);
threadNumberLink.tap();
messagesApp.Menu.selectContactPromptMenuOption('Send message');
// Verify that we entered the right thread
var message = messagesApp.Conversation.message;
assert.equal(
messagesApp.Conversation.getMessageContent(message).text(),
threads[0].messages[0].body
);
Tools.assertElementFocused(
messagesApp.Composer.messageInput, 'Message input should be focused'
);
});
});
suite('Retrieve MMS', function() {
var conversationView;
setup(function() {
var thread = ThreadGenerator.generate({
messageType: 'mms',
delivery: 'not-downloaded',
attachments: [
{ type: 'image/png', width: 10, height: 10 },
{ type: 'text/plain', content: 'Attachment' }
],
expiryDate: Date.now() + 100000
});
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
var inboxView = new InboxView(client);
conversationView = inboxView.goToConversation(thread.id);
});
test('MMS should be retrieved successfully', function() {
var notDownloadedMessage = conversationView.messages()[0];
assert.isFalse(notDownloadedMessage.isDownloaded);
assert.isFalse(notDownloadedMessage.isPending);
assert.equal(notDownloadedMessage.attachments.length, 0);
conversationView.downloadMessage(notDownloadedMessage.id);
// Now old message should be removed and new message is received.
var downloadedMessage;
client.waitFor(function() {
downloadedMessage = conversationView.findMessage(
notDownloadedMessage.id
);
return downloadedMessage && downloadedMessage.isDownloaded;
});
assert.equal(downloadedMessage.attachments.length, 1);
assert.equal(downloadedMessage.attachments[0].type, 'img');
assert.equal(downloadedMessage.content.trim(), 'Attachment');
});
});
suite('Edit Mode', function() {
var thread, conversationView;
setup(function() {
thread = ThreadGenerator.generate({
numberOfMessages: 4
});
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
var inboxView = new InboxView(client);
conversationView = inboxView.goToConversation(thread.id);
});
test('User can enter and exit edit mode', function () {
conversationView.enterEditMode();
conversationView.messages().forEach(function(message) {
assert.isTrue(message.isInEditMode);
});
conversationView.exitEditMode();
conversationView.messages().forEach(function(message) {
assert.isFalse(message.isInEditMode);
});
});
suite('Toggle selection button behavior', function() {
setup(function() {
conversationView.enterEditMode();
});
test('User can select/deselect all messages at once', function () {
// Selecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isTrue(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Deselect all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'4 selected',
'Edit mode header should show correct number of messages'
);
// Deselecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isFalse(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Select all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'Delete messages',
'Edit mode header should not indicate that any message is selected'
);
});
test('User selects a few and then selects/deselects all', function() {
// Selecting the 1st and 3rd message
var messageIndicesToSelect = [2, 0];
var messages = conversationView.messages();
messageIndicesToSelect.forEach(function(messageIndex) {
conversationView.tapOnMessage(messages[messageIndex].id);
});
conversationView.messages().forEach(function(message, index) {
if (messageIndicesToSelect.indexOf(index) >= 0) {
assert.isTrue(message.isSelected);
} else {
assert.isFalse(message.isSelected);
}
});
assert.equal(
conversationView.editHeaderTitle,
'2 selected',
'Edit mode header should show correct number of messages'
);
// Selecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isTrue(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Deselect all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'4 selected',
'Edit mode header should show correct number of messages'
);
// Deselecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isFalse(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Select all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'Delete messages',
'Edit mode header should not indicate that any message is selected'
);
});
test('User selects all, deselects some, selects all', function() {
// Selecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isTrue(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Deselect all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'4 selected',
'Edit mode header should show correct number of messages'
);
// Deselecting 1st and 3rd message
var messageIndicesToDeselect = [2, 0];
var messages = conversationView.messages();
messageIndicesToDeselect.forEach(function(messageIndex) {
conversationView.tapOnMessage(messages[messageIndex].id);
});
conversationView.messages().forEach(function(message, index) {
if (messageIndicesToDeselect.indexOf(index) >= 0) {
assert.isFalse(message.isSelected);
} else {
assert.isTrue(message.isSelected);
}
});
assert.equal(
conversationView.editHeaderTitle,
'2 selected',
'Edit mode header should show correct number of messages'
);
// Selecting all messages
conversationView.toggleMessagesSelection();
conversationView.messages().forEach(function(message) {
assert.isTrue(message.isSelected);
});
assert.equal(
conversationView.toggleSelectionButtonTitle,
'Deselect all',
'Select / Deselect all button should display correct text'
);
assert.equal(
conversationView.editHeaderTitle,
'4 selected',
'Edit mode header should show correct number of messages'
);
});
});
});
suite('Add recipients from thread', function() {
var conversationView;
setup(function() {
var thread = ThreadGenerator.generate({
numberOfMessages: 1
});
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
var inboxView = new InboxView(client);
conversationView = inboxView.goToConversation(thread.id);
});
test('With empty composer: recipients should be kept',
function() {
var newMessageView = conversationView.includeSomeoneElse();
// this also ensure we are on Composer view
assert.equal(newMessageView.recipients, '+123',
'Recipients should be kept');
assert.equal(newMessageView.messageText, '', 'SMS text should be empty');
// add a recipient, a subject and send a mms to create a thread with
// several recipients
newMessageView.typeMessage('message2');
newMessageView.addNewRecipient('+234');
newMessageView.showSubject();
newMessageView.typeSubject('subject1');
conversationView = newMessageView.send();
newMessageView = conversationView.includeSomeoneElse();
var recipients = newMessageView.recipients;
assert.lengthOf(recipients, 2, 'There should be 2 recipients');
assert.include(recipients, '+123', '+123 should be in the recipients');
assert.include(recipients, '+234', '+234 should be in the recipients');
assert.equal(newMessageView.messageText, '', 'MMS text should be empty');
});
test('Sms being written: message and recipients should be kept',
function() {
conversationView.typeMessage('message2');
var newMessageView = conversationView.includeSomeoneElse();
assert.equal(newMessageView.recipients, '+123',
'Recipient should be +123');
assert.equal(newMessageView.messageText, 'message2',
'message text should be kept');
});
test('Mms being written: all info must be kept', function() {
conversationView.typeMessage('message2');
// add a subject
conversationView.showSubject();
conversationView.typeSubject('subject1');
var newMessageView = conversationView.includeSomeoneElse();
// assert that we kept all the infos
assert.equal(newMessageView.recipients, '+123',
'Recipient should be +123');
assert.equal(newMessageView.messageText, 'message2',
'Message text should be kept');
assert.equal(newMessageView.subject, 'subject1',
'Subject should be kept');
// let's add a recipients and send the message.
// we will have a thread with several recipients
newMessageView.addNewRecipient('+234');
conversationView = newMessageView.send();
// write another MMS
conversationView.typeMessage('message3');
conversationView.showSubject();
conversationView.typeSubject('subject2');
// add a blob
conversationView.addAttachment().choose('Messages Activity Caller')
.pickImage();
messagesApp.switchTo();
// take screenshot of current attachment to compare it later
var beforeScreenshot = conversationView
.takeComposerAttachmentScreenshot(0);
// redirect to new message to add recipients
newMessageView = conversationView.includeSomeoneElse();
// assert the content
var recipients = newMessageView.recipients;
assert.lengthOf(recipients, 2, 'There should be 2 recipients');
assert.include(recipients, '+123', '+123 should be in the recipients');
assert.include(recipients, '+234', '+234 should be in the recipients');
assert.equal(newMessageView.messageText, 'message3',
'Message text should be kept');
assert.equal(newMessageView.subject, 'subject2',
'Subject should be kept');
// hide the keyboard: when it is displayed, the attachment is hidden,
// which prevents the screenshot to be taken
client.switchToFrame();
var systemInputMgmt = client.loader.getAppClass(
'system', 'input_management'
);
systemInputMgmt.waitForKeyboardFrameDisplayed();
keyboardApp.switchTo();
keyboardApp.longPressSpaceBar(0.7);
client.switchToFrame();
// wait for the keyboard to be hidden
systemInputMgmt.waitForKeyboardFrameHidden();
messagesApp.switchTo();
assert.equal(
beforeScreenshot,
newMessageView.takeComposerAttachmentScreenshot(0)
);
});
});
});

View file

@ -0,0 +1,162 @@
/* global require, marionette, setup, suite, test */
'use strict';
var assert = require('chai').assert;
var Messages = require('./lib/messages.js');
var Storage = require('./lib/storage.js');
var ThreadGenerator = require('./generators/thread');
marionette('Message Type Conversion Banner', function() {
var apps = {};
var client = marionette.client({
profile: {
prefs: {
'focusmanager.testmode': true
},
apps: apps
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var messagesApp, storage, composer, inbox, newMessage;
function assertIsDisplayed(element) {
assert.isTrue(element.displayed(), 'Element should be displayed');
}
function assertIsNotDisplayed(element) {
assert.isFalse(element.displayed(), 'Element should not be displayed');
}
function waitForBannerToDisappear() {
client.helper.waitForElementToDisappear(newMessage.conversionBanner);
}
function exitConversation() {
messagesApp.performHeaderAction();
// Wait for the thread list to appear
inbox.waitToAppear();
// Wait a little bit, much less than the time needed for the banner to
// expire in order to give the render loop time to clear the banners.
client.helper.wait(300);
}
setup(function() {
messagesApp = Messages.create(client);
storage = Storage.create(client);
composer = messagesApp.Composer;
inbox = messagesApp.Inbox;
newMessage = messagesApp.NewMessage;
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_icc_manager',
'navigator_moz_mobile_message'
]);
});
suite('Message Type Conversion Banner for new threads', function() {
setup(function() {
messagesApp.launch();
inbox.navigateToComposer();
});
test('Old conversion banner is cleared before entering a thread',
function() {
// Force it to be MMS
messagesApp.addRecipient('a@b.c');
// Without waiting for the banner to disappear, return to thread list
exitConversation();
// Create another new message
inbox.navigateToComposer();
// The banner should not be displayed
assertIsNotDisplayed(newMessage.conversionBanner);
});
test('MMS to email conversion and reminders', function() {
// Case #1: When we open composer initially we should see not banner while
// we are in SMS mode.
assertIsNotDisplayed(newMessage.conversionBanner);
// Case #2: Add an email recipient and the message becomes MMS and the
// banner should appear.
messagesApp.addRecipient('a@b.c');
assertIsDisplayed(newMessage.conversionBanner);
// Case #3: Wait for the banner to disappear, then send a message to
// be redirected to the thread view. We are still in MMS mode but no
// banner should be displayed.
waitForBannerToDisappear();
composer.messageInput.tap();
composer.messageInput.sendKeys('test');
messagesApp.send();
assertIsNotDisplayed(newMessage.conversionBanner);
// Case #4: Return to threads view and re-enter the thread to be reminded
// about being in a MMS thread.
exitConversation();
inbox.firstConversation.tap();
client.helper.waitForElement(newMessage.conversionBanner);
});
});
suite('Message Type Conversion Banner for pre-existent threads', function() {
function navigateToSMSConversation() {
inbox.smsConversation.tap();
messagesApp.Conversation.waitToAppear();
}
function navigateToMMSConversation() {
inbox.mmsConversation.tap();
messagesApp.Conversation.waitToAppear();
}
setup(function() {
var smsThread = ThreadGenerator.generate();
var mmsThread = ThreadGenerator.generate({
numberOfMessages: 1,
messageType: 'mms',
participants: ['a@b.c'],
attachments: [
{ type: 'image/png', width: 10, height: 10 },
{ type: 'text/plain', content: 'Email attachment' }
]
});
storage.setMessagesStorage(
[smsThread, mmsThread],
ThreadGenerator.uniqueMessageId
);
messagesApp.launch();
});
test('The banner is not shown after sending another message',
function() {
navigateToMMSConversation();
composer.messageInput.sendKeys('Another message');
waitForBannerToDisappear();
messagesApp.send();
assertIsNotDisplayed(newMessage.conversionBanner);
});
test('The banner for SMS is not shown when entering a SMS thread after ' +
'visiting a MMS thread', function() {
navigateToMMSConversation();
exitConversation();
navigateToSMSConversation();
assertIsNotDisplayed(newMessage.conversionBanner);
});
});
});

View file

@ -0,0 +1,446 @@
/* global require, marionette, setup, suite, test, __dirname */
'use strict';
var assert = require('chai').assert;
var MessagesActivityCaller = require('./lib/messages_activity_caller.js');
var ThreadGenerator = require('./generators/thread');
var Messages = require('./lib/messages.js');
var Storage = require('./lib/storage.js');
var InboxView = require('./lib/views/inbox/view');
marionette('Messages Drafts', function() {
var apps = {};
apps[MessagesActivityCaller.ORIGIN] = __dirname + '/apps/activitycaller';
var client = marionette.client({
profile: {
prefs: {
'focusmanager.testmode': true
},
apps: apps
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var messagesApp, storage;
function createAndSaveDraft(view, draft) {
// Add text content.
if (draft.text) {
view.typeMessage(draft.text);
}
// Add subject.
if (draft.subject) {
view.showSubject();
view.typeSubject(draft.subject);
}
// Add recipient.
if (draft.recipients) {
draft.recipients.forEach(function(recipient) {
view.addNewRecipient(recipient);
});
}
// Add attachment.
if (draft.shouldHaveAttachment) {
view.addAttachment().choose('Messages Activity Caller').pickImage();
messagesApp.switchTo();
}
var inboxView = view.backToInbox();
// It's important to wait for the updated thread node (when old node is
// removed, and new one is inserted), otherwise we can get old node
// reference and when we try to tap on it, we'll get stale element
// reference exception if node has been removed in between.
var conversationWithDraft = null;
client.waitFor(function() {
var conversations = inboxView.conversations;
if (!conversations.length) {
return false;
}
conversationWithDraft = conversations[0];
if (!conversationWithDraft.isDraft && !conversationWithDraft.hasDraft) {
return false;
}
var expectedBodyText = (draft.subject || draft.shouldHaveAttachment) ?
'' : draft.text;
return conversationWithDraft.bodyText === expectedBodyText;
});
return conversationWithDraft;
}
function discardDraft(view, draft) {
if (draft.text || draft.shouldHaveAttachment) {
view.clearMessage();
}
if (draft.subject) {
view.hideSubject();
}
if (draft.recipients) {
view.clearRecipients();
}
view.backToInbox();
}
function assertDraft(view, draft) {
// Check that recipient is loaded from draft.
if (draft.recipients) {
var recipients = view.recipients;
assert.equal(recipients.length, draft.recipients.length);
draft.recipients.forEach(function(recipient, index) {
assert.equal(recipients[index], recipient);
});
}
// Check that text content is loaded.
if (draft.text) {
assert.equal(view.messageText, draft.text);
}
// Check that subject is loaded.
if (draft.subject) {
assert.isTrue(view.isSubjectVisible());
assert.equal(view.subject, draft.subject);
} else {
assert.isFalse(view.isSubjectVisible());
}
// Check that attachment is loaded.
if (draft.shouldHaveAttachment) {
assert.equal(view.attachments.length, 1);
}
view.assertMessageInputFocused('Message input should be focused');
}
setup(function() {
messagesApp = Messages.create(client);
storage = Storage.create(client);
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_mobile_message'
]);
});
suite('Messages Drafts Test Suite', function() {
var conversation, draft, draftWithoutRecipient, inboxView;
function assertDraftIsDiscarded() {
// Check that draft is discarded and removed from inbox view.
var originalConversation = null;
client.waitFor(function() {
var conversations = inboxView.conversations;
// In discard draft case we expect exactly one conversation in inbox.
if (conversations.length !== 1) {
return false;
}
originalConversation = conversations[0];
return !originalConversation.isDraft && !originalConversation.hasDraft;
});
assert.deepEqual(originalConversation, {
id: conversation.id,
title: conversation.participants[0],
bodyText: conversation.body,
lastMessageType: 'sms',
isDraft: false,
hasDraft: false
});
}
function relaunchApp() {
messagesApp.close();
messagesApp.launch();
}
setup(function() {
conversation = ThreadGenerator.generate();
draft = {
text: 'some message',
subject: 'some subject',
recipients: ['+123'],
shouldHaveAttachment: true
};
draftWithoutRecipient = {
text: 'some message',
subject: 'some subject',
shouldHaveAttachment: true
};
storage.setMessagesStorage(
[conversation], ThreadGenerator.uniqueMessageId
);
messagesApp.launch();
inboxView = new InboxView(client);
});
suite('Conversation-less drafts', function() {
var newMessageView, originalDraftConversation;
setup(function() {
newMessageView = inboxView.createNewMessage();
originalDraftConversation = createAndSaveDraft(newMessageView, draft);
assert.deepEqual(originalDraftConversation, {
// We don't know draft id in advance, but we need every field for the
// assert.deepEqual to work.
id: originalDraftConversation.id,
isDraft: true,
hasDraft: true,
lastMessageType: 'mms',
title: draft.recipients[0],
bodyText: ''
});
inboxView.goToConversation(originalDraftConversation.id);
assertDraft(newMessageView, draft);
});
test('Draft is correctly saved', function() {
// Relaunch application and verify draft is persisted.
relaunchApp();
assert.deepEqual(
inboxView.conversations[0], originalDraftConversation
);
inboxView.goToConversation(originalDraftConversation.id);
assertDraft(newMessageView, draft);
});
test('Draft is correctly modified', function() {
var newDraft = {
text: 'content_changed',
recipients: ['+234']
};
newMessageView.clearRecipients();
newMessageView.clearMessage();
newMessageView.hideSubject();
var updatedDraftConversation = createAndSaveDraft(
newMessageView, newDraft
);
assert.deepEqual(updatedDraftConversation, {
id: originalDraftConversation.id,
isDraft: true,
hasDraft: true,
lastMessageType: 'sms',
title: newDraft.recipients[0],
bodyText: newDraft.text
});
inboxView.goToConversation(updatedDraftConversation.id);
assertDraft(newMessageView, newDraft);
// Relaunch application and verify draft is persisted.
relaunchApp();
assert.deepEqual(
inboxView.conversations[0], updatedDraftConversation
);
inboxView.goToConversation(updatedDraftConversation.id);
assertDraft(newMessageView, newDraft);
});
test('Draft is correctly discarded', function() {
discardDraft(newMessageView, draft);
assertDraftIsDiscarded();
// Relaunch application and verify draft is persistently discarded.
relaunchApp();
assertDraftIsDiscarded();
});
});
suite('Conversation-less recipient-less drafts', function() {
var newMessageView, originalDraftConversation;
test('Recipient field is focused when draft has no recipients',
function() {
newMessageView = inboxView.createNewMessage();
originalDraftConversation = createAndSaveDraft(newMessageView,
draftWithoutRecipient);
assert.deepEqual(originalDraftConversation, {
// We don't know draft id in advance, but we need every field for the
// assert.deepEqual to work.
id: originalDraftConversation.id,
isDraft: true,
hasDraft: true,
lastMessageType: 'mms',
title: '(No recipient)',
bodyText: ''
});
inboxView.goToConversation(originalDraftConversation.id);
newMessageView.assertRecipientsInputFocused(
'Message input should be focused'
);
});
});
suite('Conversation drafts', function() {
var conversationView, originalConversationWithDraft;
setup(function() {
// Conversation draft can't contain any recipients.
delete draft.recipients;
conversationView = inboxView.goToConversation(conversation.id);
originalConversationWithDraft = createAndSaveDraft(
conversationView, draft
);
assert.deepEqual(originalConversationWithDraft, {
id: conversation.id,
lastMessageType: 'mms',
title: conversation.participants[0],
bodyText: '',
isDraft: false,
hasDraft: true
});
inboxView.goToConversation(originalConversationWithDraft.id);
assertDraft(conversationView, draft);
});
test('Conversation draft is correctly saved', function() {
// Relaunch application and verify draft is persisted.
relaunchApp();
assert.deepEqual(
inboxView.conversations[0], originalConversationWithDraft
);
inboxView.goToConversation(originalConversationWithDraft.id);
assertDraft(conversationView, draft);
});
test('Conversation Draft is correctly modified', function() {
var newDraft = {
text: 'content_changed'
};
conversationView.clearMessage();
conversationView.hideSubject();
var updatedConversationWithDraft = createAndSaveDraft(
conversationView, newDraft
);
assert.deepEqual(updatedConversationWithDraft, {
id: originalConversationWithDraft.id,
lastMessageType: 'sms',
title: originalConversationWithDraft.title,
bodyText: newDraft.text,
isDraft: false,
hasDraft: true
});
inboxView.goToConversation(updatedConversationWithDraft.id);
assertDraft(conversationView, newDraft);
// Relaunch application and verify draft is persisted.
relaunchApp();
assert.deepEqual(
inboxView.conversations[0], updatedConversationWithDraft
);
inboxView.goToConversation(updatedConversationWithDraft.id);
assertDraft(conversationView, newDraft);
});
test('Conversation Draft is correctly discarded', function() {
discardDraft(conversationView, draft);
assertDraftIsDiscarded();
// Relaunch application and verify draft is persisted.
relaunchApp();
assertDraftIsDiscarded();
});
test('Conversation Draft is correctly converted to conversation-less one',
function() {
conversationView.deleteMessage(conversation.messages[0].id);
var newDraftConversation = null;
client.waitFor(function() {
var conversations = inboxView.conversations;
if (conversations.length !== 1) {
return false;
}
newDraftConversation = conversations[0];
return newDraftConversation.isDraft;
});
assert.deepEqual(newDraftConversation, {
// We don't know draft id in advance, but we need every field for the
// assert.deepEqual to work.
id: newDraftConversation.id,
lastMessageType: 'mms',
title: originalConversationWithDraft.title,
bodyText: originalConversationWithDraft.bodyText,
isDraft: true,
hasDraft: true
});
// New draft should contain recipients from the removed conversation.
var newDraft = {
text: draft.text,
subject: draft.subject,
recipients: conversation.participants,
shouldHaveAttachment: draft.shouldHaveAttachment
};
var newMessageView = inboxView.goToConversation(
newDraftConversation.id
);
assertDraft(newMessageView, newDraft);
// Relaunch application and verify draft is persisted.
relaunchApp();
assert.deepEqual(inboxView.conversations[0], newDraftConversation);
inboxView.goToConversation(newDraftConversation.id);
assertDraft(newMessageView, newDraft);
});
});
});
});

View file

@ -0,0 +1,124 @@
'use strict';
/* global module */
// TODO(gaye): This is no bueno and we need to fix.
var _ = require('SMS/node_modules/lodash');
var DEFAULT_THREAD_PARAMETERS = {
numberOfMessages: 1,
messageType: 'sms',
participants: ['+123'],
body: 'Thread message content',
baseTimestamp: Date.now()
};
var DEFAULT_MESSAGE = {
iccId: null,
sender: null,
delivery: 'sent',
timestamp: 0,
sentTimestamp: 0,
read: false
};
var DEFAULT_SMS = _.assign({
type: 'sms',
body: 'Thread message content',
receiver: null,
messageClass: 'class-1',
deliveryStatus: 'not-applicable',
deliveryTimestamp: 0
}, DEFAULT_MESSAGE);
var DEFAULT_MMS = _.assign({
type: 'mms',
receivers: [],
deliveryInfo: [{ deliveryStatus: 'not-applicable' }],
subject: 'Message subject',
smil: null,
attachments: [],
expiryDate: 0,
readReportRequested: false
}, DEFAULT_MESSAGE);
var ThreadGenerator = {
uniqueThreadId: 0,
uniqueMessageId: 0,
generate: function(parameters) {
parameters = _.assign({}, DEFAULT_THREAD_PARAMETERS, parameters);
var thread = {
id: ++ThreadGenerator.uniqueThreadId,
body: parameters.body,
lastMessageType: parameters.messageType,
timestamp: parameters.baseTimestamp,
messages: [],
participants: parameters.participants
};
for (var i = 0; i < parameters.numberOfMessages; i++) {
var message = null;
var messageBody = thread.body + ' (threadId: ' + thread.id + ')';
if (i) {
messageBody +=
' (messageId: ' + (ThreadGenerator.uniqueMessageId + 1) + ')';
}
if (parameters.messageType === 'sms') {
message = this.generateSMS({
threadId: thread.id,
receiver: parameters.participants[0],
delivery: parameters.delivery,
body: messageBody,
timestamp: parameters.baseTimestamp - i * 10000
});
} else {
message = this.generateMMS({
threadId: thread.id,
receivers: parameters.participants,
delivery: parameters.delivery,
expiryDate: parameters.expiryDate,
timestamp: parameters.baseTimestamp - i * 10000,
subject: parameters.subject,
attachments: parameters.attachments || [{
type: 'text/plain', content: messageBody
}]
});
}
thread.messages.push(message);
}
thread.timestamp = thread.messages[0].timestamp;
return thread;
},
generateSMS: function(smsParameters) {
if (!smsParameters.threadId) {
throw new Error('ThreadId should be specified!');
}
return _.assign(
{ id: smsParameters.id || ++ThreadGenerator.uniqueMessageId },
DEFAULT_SMS,
smsParameters
);
},
generateMMS: function(mmsParameters) {
if (!mmsParameters.threadId) {
throw new Error('ThreadId should be specified!');
}
return _.assign(
{ id: mmsParameters.id || ++ThreadGenerator.uniqueMessageId },
DEFAULT_MMS,
mmsParameters
);
}
};
module.exports = ThreadGenerator;

View file

@ -0,0 +1,141 @@
/* global require, marionette, setup, suite, test, __dirname */
'use strict';
var assert = require('chai').assert;
var ThreadGenerator = require('./generators/thread');
var Messages = require('./lib/messages.js');
var InboxView = require('./lib/views/inbox/view');
var Storage = require('./lib/storage.js');
marionette('Inbox View tests', function() {
var client = marionette.client({
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var messagesApp, storage;
setup(function() {
messagesApp = Messages.create(client);
storage = Storage.create(client);
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_icc_manager',
'navigator_moz_mobile_message',
'navigator_moz_contacts'
]);
});
suite('Long list of conversations', function() {
setup(function() {
var phoneNumbers = [];
for (var i = 0; i < 450; i++) {
phoneNumbers.push('+1234' + i);
}
// Only few first conversations will have photos.
var contacts = phoneNumbers.slice(0, 10).map(function(phoneNumber) {
return {
name: ['Contact ' + phoneNumber],
tel: [{ value: phoneNumber, type: 'Mobile' }],
photo: [{ width: 50, height: 50, type: 'image/jpg' }]
};
});
var conversations = phoneNumbers.map(function(phoneNumber) {
return ThreadGenerator.generate({ participants: [phoneNumber] });
});
storage.setContactsStorage(contacts);
storage.setMessagesStorage(
conversations, ThreadGenerator.uniqueMessageId
);
messagesApp.launch(true /* doNotWaitForAppToBecomeReady */);
});
test('User could navigate without waiting for the app to be fully loaded',
function() {
var inboxView = new InboxView(client);
// Make sure we've entered new message view.
var newMessageView = inboxView.createNewMessage();
newMessageView.assertRecipientsInputFocused();
// We should enter new message view even if not all messages are rendered.
assert.isFalse(inboxView.hasConversation(400));
// And rendering should still continue.
client.waitFor(function() {
return inboxView.hasConversation(400);
});
});
});
suite('Split Inbox view tests', function() {
const NUMBER_OF_CONVERSATIONS = 10;
const NUMBER_OF_MESSAGES_IN_CONVERSATION = 5;
setup(function() {
client.contentScript.inject(__dirname + '/mocks/mock_split_view_mode.js');
ThreadGenerator.uniqueThreadId = 0;
var conversations = [];
for (var i = 0; i < NUMBER_OF_CONVERSATIONS; i++) {
conversations.push(
ThreadGenerator.generate({
participants: ['+1234' + i],
numberOfMessages: NUMBER_OF_MESSAGES_IN_CONVERSATION
})
);
}
storage.setMessagesStorage(
conversations, ThreadGenerator.uniqueMessageId
);
messagesApp.launch();
});
test('All conversations are loaded', function() {
var inboxView = new InboxView(client);
client.waitFor(function() {
return inboxView.conversations.length === NUMBER_OF_CONVERSATIONS;
});
});
test('User can enter New Message view from Inbox and go back', function() {
var inboxView = new InboxView(client);
// Make sure we've entered new message view.
var newMessageView = inboxView.createNewMessage();
newMessageView.assertRecipientsInputFocused();
newMessageView.backToInbox();
client.waitFor(function() {
return inboxView.conversations.length === NUMBER_OF_CONVERSATIONS;
});
});
test('User can enter Conversation view from Inbox and go back', function() {
var inboxView = new InboxView(client);
// Make sure we've entered conversation view.
var conversationView = inboxView.goToConversation(1);
assert.equal(
conversationView.messages().length, NUMBER_OF_MESSAGES_IN_CONVERSATION
);
conversationView.backToInbox();
client.waitFor(function() {
return inboxView.conversations.length === NUMBER_OF_CONVERSATIONS;
});
});
});
});

View file

@ -0,0 +1,198 @@
'use strict';
/* global module */
var InboxAccessor = require('./views/inbox/accessors');
var NewMessageAccessor = require('./views/new-message/accessors');
var NewMessageView = require('./views/new-message/view');
var ConversationAccessor = require('./views/conversation/accessors');
var MenuAccessor = require('./views/shared/menu_accessors');
var ComposerAccessor = require('./views/shared/composer_accessors');
var ReportAccessor = require('./views/report/accessors');
var ParticipantsAccessor = require('./views/participants/accessors');
(function(module) {
var ORIGIN_URL = 'app://sms.gaiamobile.org';
var MANIFEST_URL = ORIGIN_URL + '/manifest.webapp';
// TODO Move these constants to marionette, see bug 1207516
var Chars = {
ENTER: '\ue007',
BACKSPACE: '\ue003'
};
var SELECTORS = Object.freeze({
main: '#main-wrapper',
appReady: 'body.js-app-ready',
Message: {
content: '.message-content > p:first-child',
vcardAttachment: '[data-attachment-type="vcard"]',
fileName: '.file-name'
}
});
module.exports = {
create: function(client) {
var actions = client.loader.getActions();
var newMessageView = new NewMessageView(client);
return {
Selectors: SELECTORS,
manifestURL: MANIFEST_URL,
Composer: new ComposerAccessor(client),
NewMessage: new NewMessageAccessor(client),
Conversation: new ConversationAccessor(client),
Inbox: new InboxAccessor(client),
Menu: new MenuAccessor(client),
Report: new ReportAccessor(client),
Participants: new ParticipantsAccessor(client),
launch(doNotWaitForAppToBecomeReady) {
client.switchToFrame();
client.apps.launch(ORIGIN_URL);
client.apps.switchToApp(ORIGIN_URL);
// In 99.99% cases we'd like to wait for the app to become fully ready
// before we do anything in the tests, but sometimes we'd like to
// verify that app is actionable before it's marked as ready, in this
// case we just wait for the main app container to appear.
if (!doNotWaitForAppToBecomeReady) {
client.helper.waitForElement(SELECTORS.appReady);
} else {
client.helper.waitForElement(SELECTORS.main);
}
},
close: function() {
client.apps.close(ORIGIN_URL);
},
loadMocks: function() {
client.loader.getMockManager('sms').inject([
'test_storages',
'test_blobs',
'navigator_moz_icc_manager',
'navigator_moz_mobile_message',
'navigator_moz_contacts'
]);
},
/**
* Sends system message to the Messages app using SystemMessageInternal
* class available in chrome context. Should be replaced by marionette
* apps built-in method or shared lib (see bug 1162165).
* @param {string} name Name of the system message to send.
* @param {Object} parameters Parameters object to pass with system
* message.
*/
sendSystemMessage: function(name, parameters) {
var chromeClient = client.scope({ context: 'chrome' });
chromeClient.executeScript(function(manifestURL, name, parameters) {
/* global Components, Services */
var managerClass = Components.classes[
'@mozilla.org/system-message-internal;1'
];
var systemMessageManager = managerClass.getService(
Components.interfaces.nsISystemMessagesInternal
);
systemMessageManager.sendMessage(
name,
parameters,
null, /* pageURI */
Services.io.newURI(manifestURL, null, null)
);
}, [MANIFEST_URL, name, parameters]);
},
switchTo: function() {
client.switchToFrame();
client.apps.switchToApp(ORIGIN_URL);
client.helper.waitForElement(SELECTORS.appReady);
},
waitForAppToAppear: function() {
client.switchToFrame();
var frame = client.scope({ searchTimeout: 50 }).helper.waitForElement(
'iframe[src*="' + ORIGIN_URL + '"]'
);
return frame;
},
waitForAppToDisappear: function() {
client.switchToFrame();
client.scope({ searchTimeout: 50 }).helper.waitForElementToDisappear(
'iframe[src*="' + ORIGIN_URL + '"]'
);
},
addRecipient: function(recipient) {
newMessageView.addNewRecipient(recipient);
},
getActivePanelName: function() {
var cls = client.findElement('.panel-active').getAttribute('class');
var matchResult = /\bpanel-([A-Z]\w+)\b/g.exec(cls);
return matchResult && matchResult[1];
},
getRecipient: function(number) {
return client.helper.waitForElement(
'#messages-recipients-list .recipient[data-number="' + number + '"]'
);
},
clearRecipient: function() {
newMessageView.clearRecipients();
},
send: function() {
newMessageView.send();
},
showSubject: function() {
newMessageView.showSubject();
},
hideSubject: function() {
newMessageView.hideSubject();
},
contextMenu: function(element) {
actions.longPress(element, 1).perform();
},
performHeaderAction: function() {
client.switchToShadowRoot(this.NewMessage.header);
this.NewMessage.headerActionButton.tap();
client.switchToShadowRoot();
},
performReportHeaderAction: function() {
client.switchToShadowRoot(this.Report.header);
this.Report.headerActionButton.tap();
client.switchToShadowRoot();
},
performGroupHeaderAction: function() {
client.switchToShadowRoot(this.Participants.header);
this.Participants.headerActionButton.tap();
client.switchToShadowRoot();
}
};
},
Chars: Chars
};
})(module);

View file

@ -0,0 +1,87 @@
/* global module */
(function(module) {
'use strict';
var SELECTORS = Object.freeze({
shareMenu: 'form[data-z-index-level="action-menu"]',
shareImageButton: '#share-image',
pickImageButton: '#pick-image',
pickContactButton: '#pick-contact',
sendMessageButton: '#send-message'
});
var ORIGIN = 'messages.activity.gaiamobile.org';
module.exports = {
ORIGIN: ORIGIN,
create: function(client) {
var originURL = 'app://' + ORIGIN;
return {
get shareMenu() {
// Switch to the system app first.
client.switchToFrame();
return client.helper.waitForElement(SELECTORS.shareMenu);
},
get shareWithMessagesButton() {
var shareWithOptions = this.shareMenu.findElements('button');
for (var i = 0; i < shareWithOptions.length; i++) {
var shareWithOption = shareWithOptions[i];
if (shareWithOption.text() === 'Messages') {
return shareWithOption;
}
}
},
launch: function() {
client.switchToFrame();
client.apps.launch(originURL);
client.apps.switchToApp(originURL);
},
switchTo: function() {
client.switchToFrame();
client.apps.switchToApp(originURL);
},
shareImage: function() {
client.helper.waitForElement(SELECTORS.shareImageButton).tap();
this.shareWithMessagesButton.tap();
},
pickImage: function() {
client.helper.waitForElement(SELECTORS.pickImageButton).tap();
},
pickContact: function() {
client.helper.waitForElement(SELECTORS.pickContactButton).tap();
},
/**
* Forces app to issue "websms/sms" new activity request with phone
* number and body.
* @param {{number: Number, body: string }} parameters Object that
* contains parameters to pass to "websms/sms" new activity.
*/
sendMessage: function(parameters) {
var button = client.helper.waitForElement(
SELECTORS.sendMessageButton
);
// We set button dataset attributes, that will be used by test
// activity app as "websms/sms" activity parameters.
button.scriptWith(function(element, parameters) {
Object.keys(parameters).forEach(function(parameterKey) {
element.dataset[parameterKey] = parameters[parameterKey];
});
}, [parameters]);
button.tap();
}
};
}
};
})(module);

View file

@ -0,0 +1,56 @@
/* global Components, module, Services */
(function(module) {
'use strict';
module.exports = {
create: function(client) {
return {
/**
* Sets pre-populated thread/message storage.
* @param {Array.<Thread>?} threads List of the thread to pre-populate
* storage with.
* @param {number?} uniqueMessageIdCounter Start value for the unique
* message id counter, it's used to avoid message "id" collision between
* message ids from predefined store and message ids that can be
* generated during test (e.g. send or receive new message).
*/
setMessagesStorage: function(threads, uniqueMessageIdCounter) {
this.setStorage('messagesDB', {
threads: threads || [],
uniqueMessageIdCounter: uniqueMessageIdCounter || 0
});
},
/**
* Sets pre-populated contacts storage.
* @param {Array.<MozContact>?} contacts List of the contacts to
* pre-populate storage with.
*/
setContactsStorage: function(contacts) {
this.setStorage('contactsDB', {
contacts: contacts || []
});
},
/**
* Sets pre-populated custom storage.
* @param {string} storageName Name of the custom storage.
* @param {*} storage Custom storage data.
* @private
*/
setStorage: function(storageName, storage) {
client.scope({ context: 'chrome' }).executeScript(
function(storage, storageName) {
Services.obs.addObserver(function(window) {
window.wrappedJSObject.TestStorages.set(
storageName, Components.utils.cloneInto(storage, window)
);
}, 'test-storage-ready', false);
},
[storage, storageName]
);
}
};
}
};
})(module);

View file

@ -0,0 +1,32 @@
'use strict';
/* global module */
var MenuAccessor = require('../shared/menu_accessors');
function ActivityChooserView(client) {
this.client = client;
this.menuAccessors = new MenuAccessor(client);
}
ActivityChooserView.prototype = {
choose: function(appName) {
this.menuAccessors.selectSystemMenuOption(appName);
var activityApp = this.createAppViewByName(appName);
activityApp.switchTo();
return activityApp;
},
createAppViewByName: function(appName) {
switch(appName) {
case 'Messages Activity Caller':
return require('../../messages_activity_caller.js').create(this.client);
default:
throw new Error('Unsupported app name: ' + appName);
}
}
};
module.exports = ActivityChooserView;

View file

@ -0,0 +1,132 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
main: '.panel-ConversationView',
message: '.message',
messageBubble: '.message .bubble',
header: '#messages-header',
headerTitle: '#messages-header-text',
headerActionButton: '.action-button',
container: '#messages-container',
callButton: '#messages-call-number-button',
// TODO: Remove the next selector once Message has its own accessors
messageContent: '.message-content > p:first-child',
createNewContactMenuOption: '.contact-prompt ' +
'[data-l10n-id="createNewContact"]',
addToExistingContactOption: '.contact-prompt ' +
'[data-l10n-id="addToExistingContact"]',
carrierHeaderPhoneNumber: '#contact-carrier .phone-number',
optionsButton: '#messages-options-button'
});
var EDIT_MODE_SELECTORS = Object.freeze({
toggleSelectionButton: '#messages-check-uncheck-all-button',
editHeaderTitle: '#messages-edit-mode'
});
function ConversationAccessor(client) {
this.client = client;
this.actions = client.loader.getActions();
}
ConversationAccessor.prototype = {
get messages() {
return this.client.findElements(SELECTORS.message);
},
get message() {
return this.client.helper.waitForElement(SELECTORS.messageBubble);
},
get header() {
return this.client.helper.waitForElement(SELECTORS.header);
},
get headerTitle() {
return this.client.helper.waitForElement(SELECTORS.headerTitle);
},
get editHeaderTitle() {
return this.client.helper.waitForElement(
EDIT_MODE_SELECTORS.editHeaderTitle
);
},
get headerActionButton() {
return this.client.helper.waitForElement(SELECTORS.headerActionButton);
},
get carrierHeaderPhoneNumber() {
return this.client.helper.waitForElement(
SELECTORS.carrierHeaderPhoneNumber
);
},
get callButton() {
return this.client.helper.waitForElement(SELECTORS.callButton);
},
get optionsButton() {
return this.client.helper.waitForElement(SELECTORS.optionsButton);
},
get toggleSelectionButton() {
return this.client.helper.waitForElement(
EDIT_MODE_SELECTORS.toggleSelectionButton
);
},
get createNewContactOption() {
return this.client.helper.waitForElement(
SELECTORS.createNewContactMenuOption
);
},
get addToExistingContactOption() {
return this.client.helper.waitForElement(
SELECTORS.addToExistingContactOption
);
},
getMessageContent: function(message) {
return this.client.helper.waitForElement(
message.findElement(SELECTORS.messageContent)
);
},
findMessage: function(id) {
return this.client.findElement(
'.message[data-message-id="' + id + '"] .bubble'
);
},
scrollUp: function() {
var conversationContainer = this.client.findElement(
SELECTORS.container
);
this.actions.flick(conversationContainer, 50, 50, 50, 350).perform();
},
fakeScrollUpTo: function(scrollTop) {
var conversationContainer = this.client.findElement(
SELECTORS.container
);
conversationContainer.scriptWith(function(container, scrollTop) {
container.scrollTop = scrollTop;
}, [scrollTop]);
},
waitToAppear: function() {
var conversationPanel = this.client.helper.waitForElement(SELECTORS.main);
this.client.waitFor(function() {
return conversationPanel.location().x === 0;
});
return conversationPanel;
}
};
module.exports = ConversationAccessor;

View file

@ -0,0 +1,72 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
main: '.message[data-message-id="{id}"]',
bubble: '.bubble',
content: '.message-content-body',
attachment: '.attachment-container',
downloadButton: 'button.download'
});
function MessageAccessor(client) {
this.client = client.scope({ searchTimeout: 50 });
this.actions = client.loader.getActions();
}
MessageAccessor.prototype = {
find: function(messageId) {
return this.client.findElement(SELECTORS.main.replace('{id}', messageId));
},
parse: function(messageNode) {
// It's workaround for the case when message node reference is stale,
// so when we try to access MarionetteElement from the node that's not
// attached to the dom we'll get an exception.
try {
var messageClass = messageNode.getAttribute('class');
var checkbox = messageNode.findElement('input[type=checkbox]');
// Since most of the messages don't contain any attachments, let's make
// it much faster.
var attachments = this.client.scope({ searchTimeout: 50 }).findElements(
SELECTORS.attachment, null /* default search method */, messageNode.id
);
return {
id: +messageNode.getAttribute('data-message-id'),
type: messageClass.indexOf('mms') >= 0 ? 'mms' : 'sms',
content: messageNode.findElement(SELECTORS.content).text(),
attachments: attachments.map(function(attachment) {
return {
screenshot: function() {
return this.client.screenshot({ element: attachment });
}.bind(this),
type: attachment.getAttribute('data-attachment-type')
};
}, this),
isDownloaded: messageClass.indexOf('not-downloaded') < 0,
isPending: messageClass.indexOf('pending') >= 0,
isInEditMode: messageNode.findElement('.pack-checkbox').displayed(),
isSelected: checkbox.getAttribute('checked') === 'true',
isDisplayed: messageNode.displayed()
};
} catch(e) {
console.warn('Message node is not available', e, e.stack);
return null;
}
},
download: function(messageId) {
var messageNode = this.find(messageId);
if (this.parse(messageNode).isDownloaded) {
throw new Error('Message has been already downloaded!');
}
messageNode.findElement(SELECTORS.downloadButton).tap();
}
};
module.exports = MessageAccessor;

View file

@ -0,0 +1,287 @@
'use strict';
/* global module */
var ConversationAccessor = require('./accessors');
var MessageAccessor = require('./message_accessors');
var ComposerAccessor = require('../shared/composer_accessors');
var MenuAccessor = require('../shared/menu_accessors');
var Tools = require('../shared/tools.js');
var appRoot = require('app-root-path');
// TODO Change the path once requireFromApp becomes its own module
var fromApp = require(appRoot + '/shared/test/integration/require_from_app');
var Contacts = fromApp('contacts').require('lib/contacts');
function ConversationView(client) {
this.client = client;
this.accessors = new ConversationAccessor(client);
this.messageAccessors = new MessageAccessor(client);
this.composerAccessors = new ComposerAccessor(client);
this.menuAccessors = new MenuAccessor(client);
}
ConversationView.prototype = {
// TODO Move these constants to marionette, see bug 1207516
KEYS: {
backspace: '\ue003'
},
get headerTitle() {
return this.accessors.headerTitle.text();
},
get editHeaderTitle() {
return this.accessors.editHeaderTitle.text();
},
get toggleSelectionButtonTitle() {
return this.accessors.toggleSelectionButton.text();
},
/**
* Returns the messages in this conversation, following an optional filter.
* @param {Object} [filter] Optional filter
* @param {Number} [filter.first] Will return the first n messages.
* @returns {ParsedMessage[]} List of the messages found.
*/
messages: function(filter) {
filter = filter || [];
var messages = this.accessors.messages;
if (filter.first) {
messages = messages.slice(0, filter.first);
}
return messages.map(function(message) {
return this.messageAccessors.parse(message);
}, this);
},
get carrierHeaderPhoneNumber() {
return this.accessors.carrierHeaderPhoneNumber.text();
},
findMessage: function(messageId) {
var messageNode = this.messageAccessors.find(messageId);
return messageNode && this.messageAccessors.parse(messageNode);
},
waitForFullRendering: function(thread) {
var lastId = thread.messages[thread.messages.length - 1].id;
this.accessors.findMessage(lastId);
},
downloadMessage: function(messageId) {
this.messageAccessors.download(messageId);
},
get attachments() {
return this.composerAccessors.attachments;
},
get messageText() {
return this.composerAccessors.messageInput.text().trim();
},
get subject() {
return this.composerAccessors.subjectInput.text().trim();
},
back: function() {
this.client.switchToShadowRoot(this.accessors.header);
this.accessors.headerActionButton.tap();
this.client.switchToShadowRoot();
},
callContact: function() {
this.accessors.callButton.tap();
var Dialer = fromApp('dialer').require('lib/dialer');
var dialer = new Dialer(this.client);
dialer.switchTo();
return dialer;
},
openCreateNewContact: function() {
this.accessors.headerTitle.tap();
this.accessors.createNewContactOption.tap();
var contacts = new Contacts(this.client);
contacts.switchToCreateNewContactActivity();
return contacts;
},
openAddToExistingContact: function() {
this.accessors.headerTitle.tap();
this.accessors.addToExistingContactOption.tap();
return this._switchToContactsApp();
},
openParticipants: function() {
this.accessors.headerTitle.tap();
var ParticipantsView = require('../participants/view');
var participantsView = new ParticipantsView(this.client);
participantsView.accessors.waitToAppear();
return participantsView;
},
openReport: function(messageId) {
var message = this.accessors.findMessage(messageId);
// Show context menu on message bubble.
this.client.loader.getActions().longPress(message, 1 /* sec */).perform();
this.menuAccessors.selectAppMenuOption('View message report');
var ReportView = require('../report/view');
var reportView = new ReportView(this.client, this);
reportView.accessors.waitToAppear();
return reportView;
},
deleteMessage: function(messageId) {
var lastMessageWillBeDeleted = this.messages().length === 1;
var message = this.accessors.findMessage(messageId);
// Show context menu on message bubble.
this.client.loader.getActions().longPress(message, 1 /* sec */).perform();
this.menuAccessors.selectAppMenuOption('Delete');
var dialogView = new (require('../dialog/view'))(this.client);
dialogView.chooseAction('Delete');
// If deleted message was the last one, user will be returned back to Inbox.
var currentView;
if (lastMessageWillBeDeleted) {
currentView = new (require('../inbox/view'))(this.client);
currentView.accessors.waitToAppear();
} else {
currentView = this;
}
return currentView;
},
clearMessage: function() {
var messageInput = this.composerAccessors.messageInput;
messageInput.tap();
while (messageInput.text() !== '') {
messageInput.sendKeys(this.KEYS.backspace);
}
},
typeMessage: function(message) {
this.composerAccessors.messageInput.sendKeys(message);
},
addAttachment: function() {
return this.composerAccessors.addAttachment();
},
/**
* Take a screenshot of an attachment thumbnail in the composer.
* @param {Number} The attachment index
* @return {String} The screenshot
*/
takeComposerAttachmentScreenshot: function(index) {
return this.composerAccessors.takeAttachmentScreenshot(index);
},
typeSubject: function(subject) {
this.composerAccessors.subjectInput.sendKeys(subject);
},
showOptions: function() {
this.accessors.optionsButton.tap();
},
showSubject: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Add subject');
this.client.helper.waitForElement(this.composerAccessors.subjectInput);
},
/**
* Launch the "include someone else" action.
* @returns {NewMessageView} the new message view we navigated to.
*/
includeSomeoneElse: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Include someone else');
var NewMessageView = require('../new-message/view');
var newMessageView = new NewMessageView(this.client);
newMessageView.accessors.waitToAppear();
return newMessageView;
},
hideSubject: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Remove subject');
this.client.helper.waitForElementToDisappear(
this.composerAccessors.subjectInput
);
},
enterEditMode: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Select Messages');
},
exitEditMode: function() {
this.back();
},
tapOnMessage: function(messageId) {
var messageNode = this.messageAccessors.find(messageId);
messageNode.scriptWith(function(node) {
node.scrollIntoView();
});
messageNode.tap();
},
toggleMessagesSelection: function() {
var header = this.accessors.header;
// This is a workaround needed as the screen is moving app is
// moving outside the screen on running tapOnMessage().
// This is needed to get the header back in the frame
header.scriptWith(function(node) {
node.scrollIntoView();
});
this.accessors.toggleSelectionButton.tap();
},
isSubjectVisible: function() {
var subjectInput = this.composerAccessors.subjectInput;
return subjectInput && this.composerAccessors.subjectInput.displayed();
},
fakeScrollUpTo: function(scrollTop) {
this.accessors.fakeScrollUpTo(scrollTop);
},
assertMessageInputFocused: function(message) {
Tools.assertElementFocused(this.composerAccessors.messageInput, message);
},
backToInbox: function() {
this.back();
var InboxView = require('../inbox/view');
var inboxView = new InboxView(this.client);
inboxView.accessors.waitToAppear();
return inboxView;
},
_switchToContactsApp: function() {
var contacts = new Contacts(this.client);
contacts.switchToApp();
return contacts;
}
};
module.exports = ConversationView;

Some files were not shown because too many files have changed in this diff Show more