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