Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,477 @@
'use strict';
/**
* Abstraction around contacts app.
* @constructor
* @param {Marionette.Client} client for operations.
*/
function Contacts(client) {
this.client = client;
this.client.setSearchTimeout(10000);
this.actions = this.client.loader.getActions();
}
/**
* @type String Origin of contacts app
*/
Contacts.URL = 'app://communications.gaiamobile.org';
Contacts.config = {
prefs: {
'device.storage.enabled': true,
'device.storage.testing': true,
'device.storage.prompt.testing': true
}
};
Contacts.Selectors = {
body: 'body',
bodyReady: 'body .view-body',
settingsButton: '#settings-button',
confirmHeader: '#confirmation-message h1',
confirmBody: '#confirmation-message p',
confirmDismiss: '#confirmation-message menu button',
confirmDelete: '#confirmation-message .danger',
details: '#view-contact-details',
detailsEditContact: '#edit-contact-button',
detailsTelLabelFirst: '#phone-details-template-0 h2',
detailsTelButtonFirst: '.button.icon-call[data-tel]',
detailsEmail: '#contact-detail-inner #email-details-template-0 div.item',
detailsAddress: '#contact-detail-inner #address-details-template-0 div.item',
detailsOrg: '#contact-detail-inner #org-title',
detailsNote: '#contact-detail-inner #note-details-template-0',
detailsFindDuplicate: '#contact-detail-inner #find-merge-button',
detailsFavoriteButton: '#toggle-favorite',
detailsContactName: '#contact-name-title',
detailsHeader: '#details-view-header',
detailsSocialLabel: '#contact-detail-inner #details-list #social-label',
detailsSocialTemplate: '#contact-detail-inner #details-list .social-actions',
detailsCoverImage: '#cover-img',
detailsShareButton: '#contact-detail-inner #share_button',
findDupsButton: '#details-list #find-merge-button',
duplicateFrame: 'iframe[src*="matching_contacts.html"]',
duplicateHeader: '#title',
duplicateClose: '#merge-close',
duplicateMerge: '#merge-action',
exportButton: '#exportContacts button',
exportSDCard: '#export-sd-option button',
form: '#view-contact-form',
formTitle: '#contact-form-title',
formCustomTag: '#custom-tag',
formCustomTagPage: '#view-select-tag',
formCustomTagDone: '#view-select-tag #settings-done',
formNew: '#add-contact-button',
formGivenName: '#givenName',
formOrg: '#org',
formFamilyName: '#familyName',
formSave: '#save-button',
formTel: '#contacts-form-phones input[type="tel"]',
formDelFirstTel: '#add-phone-0 .img-delete-button',
formTelLabelFirst: '#tel_type_0',
formTelNumberFirst: '#number_0',
formTelNumberSecond: '#number_1',
formEmailFirst: '#email_0',
formEmailSecond: '#email_1',
formPhotoButton: '#photo-button',
formAddNewTel: '#add-new-phone',
formAddNewEmail: '#add-new-email',
formHeader: '#contact-form-header',
formPhotoImg: '#thumbnail-photo',
deleteContact: '#delete-contact',
groupList: ' #groups-list',
contacts: '#groups-list .contact-item',
list: '#view-contacts-list',
listContactFirst: 'li:not([data-group="ice"]).contact-item',
listContactFirstText: 'li:not([data-group="ice"]).contact-item p',
contactsItems: 'li:not([data-group="ice"]).contact-item p',
contactListHeader: '#contacts-list-header',
contactsIframe: '#iframe-contacts',
searchLabel: '#search-start',
searchInput: '#search-contact',
searchCancel: '#cancel-search',
searchResultFirst: '#search-list .contact-item',
scrollbar: 'nav[data-type="scrollbar"]',
overlay: 'nav[data-type="scrollbar"] p',
settingsView: '#view-settings',
settingsClose: '#settings-close',
bulkDelete: '#bulkDelete',
orderSwitch: '#settingsOrder gaia-switch',
changeOrder: '#switch',
editForm: '#selectable-form',
editMenu: '#select-all-wrapper',
selectAction: '#select-action',
selectAllButton: '#select-all',
clearOrgButton: '#clear-org',
setIceButton: '#set-ice',
iceHeader: '#ice-header',
iceSettingsHeader: '#ice-settings-header',
iceSettings: '#ice-settings',
iceSwitch1: '#ice-contacts-1-switch',
iceInputSwitch1: '#ice-contacts-1-switch input[type="checkbox"]',
iceSwitch2: '#ice-contacts-2-switch',
iceInputSwitch2: '#ice-contacts-2-switch input[type="checkbox"]',
iceButton1: '#select-ice-contact-1',
iceButton2: '#select-ice-contact-2',
iceGroupOpen: '#section-group-ice',
iceContact: '#ice-group .contact-item',
importContacts: '#importContacts',
importSDCard: '#import-sd-option button',
importHeader: '#import-settings-header',
activityChooser: 'form[data-type="action"]',
buttonActivityChooser: 'form[data-type="action"] button',
actionMenu: '#action-menu',
actionMenuList: '#value-menu',
actionButton: '.action-button',
multipleSelectSave: '#save-button',
multipleSelectStatus: '#statusMsg p',
multipleSelectList: '#multiple-select-container',
systemMenu: 'form[data-z-index-level="action-menu"]',
galleryImage: '.thumbnail img',
galleryDone: '#crop-done-button',
header: '#edit-title',
noContacts: '#no-contacts'
};
Contacts.prototype = {
/**
* Launches contacts app and focuses on frame.
*/
launch: function() {
this.client.apps.launch(Contacts.URL, 'contacts');
this.switchToApp();
this.client.helper.waitForElement(Contacts.Selectors.bodyReady);
},
relaunch: function() {
this.close();
this.launch();
},
close: function() {
this.client.apps.close(Contacts.URL, 'contacts');
},
switchToApp: function() {
this.client.switchToFrame();
// switchToApp already waits for the app to be displayed
this.client.apps.switchToApp(Contacts.URL, 'contacts');
},
switchToCreateNewContactActivity: function() {
this._switchToActivity('views/form/form.html');
},
switchToMergeInterface: function() {
this._switchToActivity('views/matching/matching_contacts.html');
},
_switchToActivity: function(href) {
this.client.switchToFrame();
// switchToActivity already waits for the app to be displayed
this.client.apps.switchToActivity(Contacts.URL, '/contacts/' + href);
},
/**
* Returns a localized string from a properties file.
* @param {String} file to open.
* @param {String} key of the string to lookup.
*/
l10n: function(file, key) {
var ast = this.client.executeScript(function(file, key) {
var xhr = new XMLHttpRequest();
var data;
xhr.open('GET', file, false); // Intentional sync
xhr.onload = function(o) {
data = JSON.parse(xhr.response);
};
xhr.send(null);
return data;
}, [file, key]);
for (var i = 0; i < ast.length; i++) {
if (ast[i].$i === key) {
return ast[i].$v;
}
}
},
waitSlideLeft: function(elementKey) {
var element = this.client.findElement(Contacts.Selectors[elementKey]),
location;
var test = function() {
location = element.location();
return location.x <= 0;
};
this.client.waitFor(test);
},
waitForSlideDown: function(element) {
var bodyHeight = this.client.findElement(Contacts.Selectors.body).
size().height;
var test = function() {
return element.location().y >= bodyHeight;
};
this.client.waitFor(test);
},
waitForSlideUp: function(element) {
var test = function() {
return element.location().y <= 0;
};
this.client.waitFor(test);
},
waitForFadeIn: function(element) {
var test = function() {
var opacity = element.cssProperty('opacity');
var pointerEvents = element.cssProperty('pointer-events');
return opacity == 1 && pointerEvents == 'auto';
};
this.client.waitFor(test);
},
waitForFormShown: function() {
var form = this.client.helper.waitForElement(Contacts.Selectors.form),
location;
var test = function() {
location = form.location();
return location.y <= 0;
};
this.client.waitFor(test);
},
waitForFormTransition: function() {
var selectors = Contacts.Selectors;
var form = this.client.findElement(selectors.form);
try {
this.client.helper.waitForElementToDisappear(form);
} catch (e) {
// When the contact details pages was created in an activity and we close
// it, we might encounter errors like "GenericError: can't access dead
// object" or 'JavaScriptError: unload was called'. So we drop them as it
// makes the tests crash.
var isKnownError = e.message.indexOf(
'GenericError: can\'t access dead object') > -1 ||
e.message.indexOf('JavaScriptError: unload was called') > -1;
if (!isKnownError) {
throw e;
}
}
},
editContact: function() {
var selectors = Contacts.Selectors;
var edit = this.client.helper.waitForElement(selectors.detailsEditContact);
this.clickOn(edit);
this.waitForFadeIn(this.client.helper.waitForElement(selectors.form));
},
// Goes back to the Contact List view from a Details view
backToList: function() {
var selectors = Contacts.Selectors;
// Now we go back to the ICE settings and check that our ICE remains
this.waitForFadeIn(this.client.helper.waitForElement(selectors.details));
var header = this.client.helper.waitForElement(selectors.detailsHeader);
this.actions.wait(0.5).tap(header, 10, 10).perform();
this.waitSlideLeft('list');
},
enterContactDetails: function(details) {
var selectors = Contacts.Selectors;
details = details || {
givenName: 'Hello',
familyName: 'Contact',
org: 'Enterprise'
};
this.waitForFormShown();
for (var i in details) {
// Camelcase details to match form.* selectors.
var key = 'form' + i.charAt(0).toUpperCase() + i.slice(1);
this.client.findElement(selectors[key])
.sendKeys(details[i]);
}
this.client.findElement(selectors.formSave).click();
},
addContact: function(details) {
var selectors = Contacts.Selectors;
var addContact = this.client.findElement(selectors.formNew);
addContact.click();
this.enterContactDetails(details);
this.waitForFormTransition();
this.client.helper.waitForElement(selectors.list);
},
mergeContact: function(details) {
var selectors = Contacts.Selectors;
var addContact = this.client.findElement(selectors.formNew);
addContact.click();
this.enterContactDetails(details);
this.switchToMergeInterface();
var duplicateFrame = this.client.findElement(selectors.duplicateFrame);
var mergeAction =
this.client.helper.waitForElement(selectors.duplicateMerge);
this.clickOn(mergeAction);
this.switchTo();
this.waitForSlideDown(duplicateFrame);
},
addContactMultipleEmails: function(details) {
var selectors = Contacts.Selectors;
var addContact = this.client.findElement(selectors.formNew);
addContact.click();
this.client.helper.waitForElement(selectors.formAddNewEmail).click();
this.enterContactDetails(details);
this.waitForFormTransition();
this.client.helper.waitForElement(selectors.list);
},
tapContact: function(contactName) {
// There are currently no way to search on the text of an HTML Element
// That's why this function do the filter and check that no more than 1
// contact has the name we look up.
var elements = this.client.findElements(Contacts.Selectors.contacts);
var contactsWithName = elements.filter(function(el) {
var data = el.text();
return data && data.indexOf(contactName) > -1;
});
if (contactsWithName.length === 1) {
contactsWithName[0].tap();
} else if (contactsWithName.length > 1) {
throw new Error('There are more than 1 contact that contains "' +
contactName + '"');
} else {
throw new Error('"' + contactName + '" was not found');
}
},
getContactFirstName: function() {
var ci = this.client.findElement(Contacts.Selectors.contactsIframe);
this.client.switchToFrame(ci);
var contactInContactList = this.client.findElement(
Contacts.Selectors.listContactFirst
);
var contactName = contactInContactList.text();
return contactName;
},
tapSave: function() {
this.client.helper.waitForElement(Contacts.Selectors.formSave).click();
},
createNewContactAndReturnToDialer: function(contactName) {
this.client.helper.waitForElement(Contacts.Selectors.formGivenName);
this.client.helper.fillInputField(
Contacts.Selectors.formGivenName, contactName
);
this.client.helper.waitForElement(Contacts.Selectors.formSave).click();
var dialer = this.client.loader.getAppClass('dialer');
dialer.switchTo();
return dialer;
},
tapUpdate: function() {
this.tapSave();
},
get systemMenu() {
var selectors = Contacts.Selectors;
// Switch to the system app first.
this.client.switchToFrame();
return this.client.helper.waitForElement(selectors.systemMenu);
},
/**
* Helper method to simulate clicks on iFrames which is not currently
* working in the Marionette JS Runner.
* @param {Marionette.Element} element The element to simulate the click on.
**/
clickOn: function(element) {
element.scriptWith(function(elementEl) {
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
elementEl.dispatchEvent(event);
});
},
goToSettings: function() {
this.client.helper.waitForElement(
Contacts.Selectors.settingsButton).tap();
var settings = this.client.helper.waitForElement(
Contacts.Selectors.settingsView);
this.waitForFadeIn(settings);
},
getElementStyle: function(selector, type) {
return this.client.executeScript(function(selector, type) {
return document.querySelector(selector).style[type];
}, [selector, type]);
},
getNumberOfContacts: function() {
return this.contactsNames.length;
},
get contactsNames() {
try {
var contactElements = this.client.findElements(
Contacts.Selectors.contactsItems);
return contactElements.map((element) => {
return element.text();
});
} catch(e) {
return [];
}
}
};
module.exports = Contacts;

View file

@ -0,0 +1,126 @@
'use strict';
/* globals mozContact */
var fs = require('fs');
/* This module contains Contacts Data Layer functions */
function ContactsData(client) {
this.client = client;
}
ContactsData.prototype = {
// Creates a new FB Contact
createFbContact: function() {
this.client.importScript(fs.readFileSync(__dirname +
'/../data/facebook_contact_data.js',
'utf8'));
var saveFBContact = function() {
var LazyLoader = window.wrappedJSObject.LazyLoader;
var data = window.wrappedJSObject.data;
var fb = window.wrappedJSObject.fb;
var Contacts = window.wrappedJSObject.Contacts;
var DEPS = [
'/shared/js/contacts/utilities/image_square.js'
];
Contacts.loadFacebook(function() {
LazyLoader.load(DEPS, function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/contacts/style/images/f_logo.png');
xhr.responseType = 'blob';
xhr.onload = function() {
var fbContact = new fb.Contact();
data.fbContactData.fbInfo.photo = [xhr.response];
fbContact.setData(data.fbContactData);
var savingFBContact = fbContact.save();
savingFBContact.onsuccess = function() {
marionetteScriptFinished(data.fbContactData);
};
savingFBContact.onerror = function() {
marionetteScriptFinished();
};
};
xhr.onerror = marionetteScriptFinished;
xhr.send(null);
});
});
};
var fbContactData;
this.client.executeAsyncScript(saveFBContact, function(err, val) {
if (err) {
console.error(err);
}
fbContactData = val;
});
this.client.waitFor(function() {
return fbContactData;
});
return fbContactData;
},
createMozContact: function(contactData, withPhoto) {
var saveMozContact = function(contactData, withPhoto) {
var doSave = function doSave(contactData) {
var mozContacts = navigator.mozContacts;
var handler = function handler(event) {
mozContacts.removeEventListener('contactchange', handler);
marionetteScriptFinished(event.contactID);
};
mozContacts.addEventListener('contactchange', handler);
var contact = new mozContact(contactData);
var req = mozContacts.save(contact);
req.onerror = function() {
mozContacts.removeEventListener('contactchange', handler);
marionetteScriptFinished();
};
};
if (withPhoto) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/contacts/style/images/f_logo.png');
xhr.responseType = 'blob';
xhr.onload = function() {
contactData.photo = [xhr.response];
doSave(contactData);
};
xhr.onerror = marionetteScriptFinished;
xhr.send(null);
}
else {
doSave(contactData);
}
};
var contactID;
this.client.executeAsyncScript(saveMozContact, [contactData, withPhoto],
function(err, val) {
if (err) {
console.error(err);
}
contactID = val;
});
this.client.waitFor(function() {
return contactID;
});
return contactID;
}
};
module.exports = ContactsData;

View file

@ -0,0 +1,40 @@
'use strict';
/**
* Abstraction around sms app.
* This file can be moved to the sms app once we
* implement sms marionette tests.
* @constructor
* @param {Marionette.Client} client for operations.
*/
function Sms(client) {
this.client = client;
}
/**
* @type String Origin of sms app
*/
Sms.URL = 'app://sms.gaiamobile.org';
Sms.Selectors = {
listCompose: '#icon-add',
threadAddContact: '#messages-contact-pick-button'
};
Sms.prototype = {
/**
* Launches sms app and focuses on frame.
*/
launch: function() {
this.client.apps.launch(Sms.URL);
this.client.apps.switchToApp(Sms.URL);
this.client.helper.waitForElement('body');
},
relaunch: function() {
this.client.apps.close(Sms.URL);
this.launch();
}
};
module.exports = Sms;

View file

@ -0,0 +1,39 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
detailsFavoriteButton: '#toggle-favorite',
detailsHeader: '#details-view-header',
details: '#view-contact-details',
list: '#view-contacts-list'
});
function ContactsDetailsAccessor(client) {
this.client = client;
}
ContactsDetailsAccessor.prototype = {
get favoritesButton() {
return this.client.helper.waitForElement(
SELECTORS.detailsFavoriteButton);
},
get detailsHeader() {
return this.client.helper.waitForElement(
SELECTORS.detailsHeader);
},
get listSelector () {
return SELECTORS.list;
},
get detailsSelector () {
return SELECTORS.details;
},
get detailsHeaderSelector () {
return SELECTORS.detailsHeader;
}
};
module.exports = ContactsDetailsAccessor;

View file

@ -0,0 +1,66 @@
'use strict';
/* global module */
var ContactsDetailsAccessor = require('./accessors');
function ContactsDetailsView(client) {
this.client = client;
this.accessors = new ContactsDetailsAccessor(client);
}
ContactsDetailsView.prototype = {
makeFavorited: function() {
this.accessors.favoritesButton.tap();
this.waitForFavorited;
},
waitForFavorited: function() {
var self = this;
this.client.waitFor(
'waitForHeaderToContainFavoriteClass', function() {
return self.accessors.detailsHeader.getAttribute(
'class').indexOf('favorite') != -1;
});
},
//TO-DO: ADD THIS TO A CONTACT VIEW BASE CLASS
backToList: function() {
this.waitForFadeIn(this.client.helper.waitForElement(
this.accessors.detailsSelector));
var header = this.client.helper.waitForElement(
this.accessors.detailsHeaderSelector);
this.client.loader.getActions().wait(0.5).tap(header, 10, 10).perform();
this.waitSlideLeftList();
},
waitSlideLeftList: function() {
var element = this.client.findElement(
this.accessors.listSelector),
location;
var test = function() {
location = element.location();
return location.x <= 0;
};
this.client.waitFor(test);
},
waitForFadeIn: function(element) {
var test = function() {
var opacity = element.cssProperty('opacity');
var pointerEvents = element.cssProperty('pointer-events');
return opacity == 1 && pointerEvents == 'auto';
};
this.client.waitFor(test);
},
backtoContactsList: function() {
this.backToList();
var ContactsListView = require('../contact-list/view');
var contactsListView = new ContactsListView(this.client);
contactsListView.accessors.firstFavorite;
return contactsListView;
}
};
module.exports = ContactsDetailsView;

View file

@ -0,0 +1,31 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
listContactFirstText: 'li:not([data-group="ice"]).contact-item p',
listFavoritesFirstText: '#contacts-list-favorites .contact-item p',
details: '#view-contact-details'
});
function ContactsListAccessor(client) {
this.client = client;
}
ContactsListAccessor.prototype = {
get firstContact() {
return this.client.helper.waitForElement(
SELECTORS.listContactFirstText);
},
get firstFavorite() {
return this.client.helper.waitForElement(
SELECTORS.listFavoritesFirstText);
},
get detailsSelector() {
return SELECTORS.details;
}
};
module.exports = ContactsListAccessor;

View file

@ -0,0 +1,47 @@
'use strict';
/* global module */
var ContactsListAccessor = require('./accessors');
function ContactsListView(client) {
this.client = client;
this.accessors = new ContactsListAccessor(client);
}
ContactsListView.prototype = {
get firstContact() {
return this.accessors.firstContact;
},
get firstFavorite() {
return this.accessors.firstFavorite;
},
waitSlideLeftDetails: function() {
var element = this.client.findElement(
this.accessors.detailsSelector),
location;
var waitUntilPanelReachedLeftBorder = function() {
location = element.location();
return location.x <= 0;
};
this.client.waitFor(waitUntilPanelReachedLeftBorder);
},
goToContact: function() {
this.firstContact.tap();
this.waitSlideLeftDetails();
return this._createContactsDetailsView();
},
_createContactsDetailsView: function() {
var ContactsDetailsView = require('../contact-details/view');
var contactsDetailsView = new ContactsDetailsView(
this.client, this.subject);
return contactsDetailsView;
}
};
module.exports = ContactsListView;