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,18 @@
{
"extends": "../../../../../.jshintrc",
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"module",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,50 @@
'use strict';
var Dialer = require('./lib/dialer');
var CallLogEntriesManager = require('./fake/call_log_entries_manager.js');
marionette('Dialer > Call log', function() {
var client = marionette.client(Dialer.config);
var dialer;
var callLog;
setup(function() {
dialer = new Dialer(client);
dialer.launch();
client.loader.getMockManager('dialer').inject('navigator_moz_icc_manager');
var entriesManager = new CallLogEntriesManager(dialer);
var partialEntries = [
{},
{'number': '123'},
{'number': '456'}
];
entriesManager.generateAndAdd(partialEntries);
callLog = dialer.tabs.goToCallLog(partialEntries.length);
});
test('Entering the edit mode hides the filters', function() {
callLog.enterEditMode(); // Hide verification is done there
});
suite('Edit mode should be hidden', function() {
setup(function() {
callLog.enterEditMode();
});
test('when exiting it with cancel', function() {
callLog.exitEditMode(); // Hide verification is done there
});
test('when deleting some (but not all) call log entries', function() {
callLog.deleteEntries([0, 1]); // Hide verification is done there
});
test('when deleting all call log entries', function() {
callLog.deleteAllEntries(); // Hide verification is done there
});
});
});

View file

@ -0,0 +1,41 @@
'use strict';
var _ = require('lodash');
var DEFAULT_ENTRY = {
'date': Date.now(),
'type': 'incoming',
'number': '5551234567',
'serviceId': 0,
'emergency': false,
'voicemail': false,
'status': 'connected'
};
function CallLogEntries(dialer) {
this.dialer = dialer;
this.client = dialer.client;
}
CallLogEntries.prototype = {
generateAndAdd: function(entriesParameters) {
entriesParameters = Array.isArray(entriesParameters) ?
entriesParameters : [entriesParameters];
entriesParameters.forEach(function(entryParameters) {
this._generateOneAndAdd(entryParameters);
}, this);
this.dialer.relaunch();
},
_generateOneAndAdd: function(parameters) {
var entry = _.assign({}, DEFAULT_ENTRY, parameters);
this.client.executeScript(function(entry) {
window.wrappedJSObject.CallLogDBManager.add(entry);
}, [entry]);
}
};
module.exports = CallLogEntries;

View file

@ -0,0 +1,332 @@
'use strict';
var Dialer = require('./lib/dialer');
var ReflowHelper =
require('../../../../../tests/jsmarionette/plugins/reflow_helper.js');
marionette('Dialer > Keypad', function() {
var assert = require('assert');
var client = marionette.client(Dialer.config);
var subject;
var selectors;
var actions;
var reflowHelper;
setup(function() {
actions = client.loader.getActions();
subject = new Dialer(client);
subject.launch();
selectors = Dialer.Selectors;
var keypad = subject.client.findElement(selectors.keypadView);
client.waitFor(function() {
return keypad.displayed();
});
reflowHelper = new ReflowHelper(client);
});
function typeNumber() {
var one = subject.client.findElement(selectors.one);
keypadTap(one);
var two = subject.client.findElement(selectors.two);
keypadTap(two);
var three = subject.client.findElement(selectors.three);
keypadTap(three);
}
// Taps digits in 1, 2, 3 sequence until producing an ellipsis. We make this
// instead of entering fixed number as the digit at which the ellipsis
// appears depends on the integration environment.
function typeLongNumber() {
var number = subject.client.findElement(selectors.phoneNumber);
var numberTyped = '';
var one = subject.client.findElement(selectors.one);
var two = subject.client.findElement(selectors.two);
var three = subject.client.findElement(selectors.three);
var sequence = [one, two, three];
var length = sequence.length;
for (var i = 0, digit; (digit = sequence[i]); i = (i+1) % length) {
keypadTap(digit);
numberTyped += digit.getAttribute('data-value');
if (number.getAttribute('value').indexOf('\u2026') === 0) {
return numberTyped;
}
}
}
// Avoid "all the same number" patterns like 111111... because, under
// ellipsis scenaries, the value of the input won't change and the test
// will timeout.
function keypadTap(elem, longPress) {
var number = subject.client.findElement(selectors.phoneNumber);
var previousValue = number.getAttribute('value');
if (longPress) {
actions.longPress(elem, 1).perform();
} else {
actions.tap(elem).perform();
}
client.waitFor(function () {
return number.getAttribute('value') !== previousValue;
});
}
function loadSuggestionDOM() {
typeNumber();
var del = subject.client.findElement(selectors.del);
actions.longPress(del, 1).perform();
var number = subject.client.findElement(selectors.phoneNumber);
client.waitFor(function() {
return (number.getAttribute('value') === '');
});
}
test('Add a contact from dialer', function() {
subject.keypadView.typePhoneNumber(12345);
var addToContactsActivities = subject.keypadView.addToContacts();
var newContact = addToContactsActivities.pickCreateNew();
var contactName = 'Name';
var dialer = newContact.createNewContactAndReturnToDialer(contactName);
var contactList = dialer.tabs.goToContactList();
var contactCreated = contactList.getContactFirstName();
assert.strictEqual(contactCreated, contactName);
});
test('Entering a 3 digits number with the keypad', function() {
loadSuggestionDOM();
reflowHelper.startTracking(Dialer.URL + '/manifest.webapp');
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
assert.equal(number.getAttribute('value'), '123');
var reflowCount = reflowHelper.getCount();
assert.equal(reflowCount, 0);
reflowHelper.stopTracking();
});
test('Entering a digit in the middle of the number', function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = numberElement.selectionEnd = 1;
numberElement.click();
});
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero);
assert.equal(number.getAttribute('value'), '1023');
});
test('Replace a selection in the middle of the number', function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = 1;
numberElement.selectionEnd = 2;
numberElement.click();
});
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero);
assert.equal(number.getAttribute('value'), '103');
});
test('Replace a selection in the middle of the number with a long press',
function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = 1;
numberElement.selectionEnd = 2;
numberElement.click();
});
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero, true);
assert.equal(number.getAttribute('value'), '1+3');
});
test('Entering a long press in the middle of the number', function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = numberElement.selectionEnd = 1;
numberElement.click();
});
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero, true);
assert.equal(number.getAttribute('value'), '1+23');
});
test('Entering a digit in the middle of a long number', function() {
var insPosition = 3; // index where the cursor would be placed by the user
var realInsPosition = insPosition + 2; // index inside the complete number
// +2 for the space of the ellipsis
var typedNumber = typeLongNumber();
var numberAfterInsertion =
typedNumber.substr(0, realInsPosition) + '0' +
typedNumber.substr(realInsPosition);
var expectedValueAfterInsertion =
'\u2026' + numberAfterInsertion.substr(4); // 1 for the overflowing char,
// 2 for the ellipsis itself,
// 1 more for the new char.
var number = subject.client.findElement(selectors.phoneNumber);
var args = [number, insPosition];
client.executeScript(function (numberElement, insPosition) {
numberElement.selectionStart = numberElement.selectionEnd = insPosition;
numberElement.click();
}, args);
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero);
assert.equal(number.getAttribute('value'), expectedValueAfterInsertion);
});
test('Entering a long press in the middle of a long number', function() {
var insPosition = 3; // index where the cursor would be placed by the user
var realInsPosition = insPosition + 2; // index inside the complete number
// +2 for the space of the ellipsis
var typedNumber = typeLongNumber();
var numberAfterInsertion =
typedNumber.substr(0, realInsPosition) + '+' +
typedNumber.substr(realInsPosition);
var expectedValueAfterInsertion =
'\u2026' + numberAfterInsertion.substr(4); // 1 for the overflowing char,
// 2 for the ellipsis itself,
// 1 more for the new char.
var number = subject.client.findElement(selectors.phoneNumber);
var args = [number, insPosition];
client.executeScript(function (numberElement, insPosition) {
numberElement.selectionStart = numberElement.selectionEnd = insPosition;
numberElement.click();
}, args);
var zero = subject.client.findElement(selectors.zero);
keypadTap(zero, true);
assert.equal(number.getAttribute('value'), expectedValueAfterInsertion);
});
test('Using the special extention key', function() {
var zero = subject.client.findElement(selectors.zero);
var number = subject.client.findElement(selectors.phoneNumber);
keypadTap(zero, true);
assert.equal(number.getAttribute('value'), '+');
keypadTap(zero, false);
assert.equal(number.getAttribute('value'), '+0');
});
test('Deleting a digit', function() {
typeNumber();
var del = subject.client.findElement(selectors.del);
actions.tap(del).perform();
var number = subject.client.findElement(selectors.phoneNumber);
client.waitFor(function() {
return (number.getAttribute('value').length == 2);
});
assert.equal(number.getAttribute('value'), '12');
});
test('Deleting a digit in the middle of the number', function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = numberElement.selectionEnd = 2;
numberElement.click();
});
var del = subject.client.findElement(selectors.del);
actions.tap(del).perform();
client.waitFor(function() {
return (number.getAttribute('value').length == 2);
});
assert.equal(number.getAttribute('value'), '13');
});
test('Deleting a digit in the middle of a long number', function() {
var delPosition = 3; // index where the cursor would be placed by the user
var realDelPosition = delPosition + 2; // index inside the complete number
// +2 for the space of the ellipsis
var typedNumber = typeLongNumber();
var expectedNumberAfterDeletion =
typedNumber.substr(0, realDelPosition - 1) +
typedNumber.substr(realDelPosition);
var number = subject.client.findElement(selectors.phoneNumber);
var args = [number, delPosition];
client.executeScript(function (numberElement, delPosition) {
numberElement.selectionStart = numberElement.selectionEnd = delPosition;
numberElement.click();
}, args);
var del = subject.client.findElement(selectors.del);
actions.tap(del).perform();
client.waitFor(function() {
return (number.getAttribute('value') == expectedNumberAfterDeletion);
});
assert.ok(true);
});
test('Deleting a couple of digits in the middle of the number', function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = numberElement.selectionEnd = 2;
numberElement.click();
});
var del = subject.client.findElement(selectors.del);
actions.doubleTap(del).perform();
assert.equal(number.getAttribute('value'), '3');
});
test('Clearing the number partially by long pressing the delete key',
function() {
typeNumber();
var number = subject.client.findElement(selectors.phoneNumber);
number.scriptWith(function (numberElement) {
numberElement.selectionStart = numberElement.selectionEnd = 2;
numberElement.click();
});
var del = subject.client.findElement(selectors.del);
actions.longPress(del, 1).perform();
client.waitFor(function() {
return (number.getAttribute('value') === '3');
});
assert.ok(true, 'cleaned the phone number view');
});
});

View file

@ -0,0 +1,90 @@
'use strict';
/* global module */
var KeypadView = require('./views/keypad/views.js');
var TabsView = require('./views/tabs/view.js');
/**
* Abstraction around dialer app.
* @constructor
* @param {Marionette.Client} client for operations.
*/
function Dialer(client) {
this.client = client;
}
/**
* @type String Origin of dialer app
*/
Dialer.URL = 'app://communications.gaiamobile.org';
Dialer.config = {
profile: {
settings: {
'devtools.overlay': true,
'hud.reflows': true
},
prefs: {
'devtools.debugger.forbid-certified-apps': false
}
},
desiredCapabilities: {
'raisesAccessibilityExceptions': false
}
};
Dialer.Selectors = {
phoneNumber: '#phone-number-view',
keypadView: '#keyboard-view',
del: '#keypad-delete',
zero: '.keypad-key[data-value="0"]',
one: '.keypad-key[data-value="1"]',
two: '.keypad-key[data-value="2"]',
three: '.keypad-key[data-value="3"]',
keypadCallBarAddContact: '#keypad-callbar-add-contact',
contactsTabItem: '#option-contacts',
callLogTabItem: '#option-recents',
callLogTabs: '#call-log-filter',
callLogEditForm: '#edit-mode',
addToExistingContactMenuItem: 'button[data-l10n-id="addToExistingContact"]'
};
Dialer.prototype = {
/**
* Launches dialer app and focuses on frame.
*/
launch: function() {
this.client.apps.launch(Dialer.URL, 'dialer');
this.switchTo();
this.client.helper.waitForElement('body');
},
relaunch: function() {
this.client.apps.close(Dialer.URL, 'dialer');
this.launch();
},
switchTo: function() {
this.client.switchToFrame();
// switchToApp already waits for the app to be displayed
this.client.apps.switchToApp(Dialer.URL, 'dialer');
},
get phoneNumber() {
return this.client.helper.waitForElement(Dialer.Selectors.phoneNumber)
.getAttribute('value');
},
get keypadView() {
return new KeypadView(this.client);
},
get tabs() {
return new TabsView(this.client);
}
};
module.exports = Dialer;

View file

@ -0,0 +1,18 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
addNewContact: 'button[data-l10n-id="createNewContact"]',
});
function ActivitiesAccessor(client) {
this.client = client;
}
ActivitiesAccessor.prototype = {
get addNewContactMenuItem() {
return this.client.helper.waitForElement(SELECTORS.addNewContact);
}
};
module.exports = ActivitiesAccessor;

View file

@ -0,0 +1,25 @@
'use strict';
/* global module */
var ActivitiesAccessor = require('./accessors');
function ActivitiesView(client) {
this.client = client;
this.accessors = new ActivitiesAccessor(client);
}
ActivitiesView.prototype = {
pickCreateNew: function() {
this.client.waitFor(function() {
return this.accessors.addNewContactMenuItem.enabled();
}.bind(this));
var button = this.accessors.addNewContactMenuItem;
button.tap();
var contacts = this.client.loader.getAppClass('contacts');
contacts.switchToCreateNewContactActivity();
return contacts;
}
};
module.exports = ActivitiesView;

View file

@ -0,0 +1,77 @@
'use strict';
var SELECTORS = Object.freeze({
editButton: '#call-log-icon-edit',
callLogTabs: '#call-log-filter',
callLogItem: '.log-item',
callLogNoResults: '#no-result-message',
editHeader: '#edit-mode-header',
selectAllButton: '#select-all-threads',
deleteButton: '#delete-button',
confirmDeleteButton: '#confirmation-message [data-l10n-id="delete"]'
});
function CallLogAccessors(client) {
this.client = client;
}
CallLogAccessors.prototype = {
get callLogFilter() {
return this.client.helper.waitForElement(SELECTORS.callLogTabs);
},
get callLogNoResults() {
return this.client.helper.waitForElement(SELECTORS.callLogNoResults);
},
get logEntries() {
return this.client.findElements(SELECTORS.callLogItem);
},
get editButton() {
return this.client.helper.waitForElement(SELECTORS.editButton);
},
get editHeader() {
return new EditHeaderAccessors(this.client, SELECTORS.editHeader);
},
get selectAllButton() {
return this.client.helper.waitForElement(SELECTORS.selectAllButton);
},
get deleteButton() {
return this.client.helper.waitForElement(SELECTORS.deleteButton);
},
get dialogConfirmButton() {
return this.client.helper.waitForElement(SELECTORS.confirmDeleteButton);
}
};
var HEADER_SELECTORS = Object.freeze({
title: 'h1',
});
function EditHeaderAccessors(client, rootSelector) {
this.client = client;
this.rootSelector = rootSelector;
}
EditHeaderAccessors.prototype = {
get root() {
return this.client.helper.waitForElement(this.rootSelector);
},
get title() {
return this.root.findElement(HEADER_SELECTORS.title);
},
get closeButton() {
return this.root;
}
};
module.exports = CallLogAccessors;

View file

@ -0,0 +1,70 @@
'use strict';
var CallLogAccessors = require('./accessors');
function CallLogView(client, expectedNumberOfItems) {
this.client = client;
this.accessors = new CallLogAccessors(client);
this.accessors.callLogFilter;
if (expectedNumberOfItems === 0) {
this.accessors.callLogNoResults;
} else {
var self = this;
this.client.waitFor(function() {
return self.accessors.logEntries.length === expectedNumberOfItems;
});
}
}
CallLogView.prototype = {
enterEditMode: function() {
var filter = this.accessors.callLogFilter;
this.accessors.editButton.tap();
this.client.helper.waitForElementToDisappear(filter);
var self = this;
this.client.waitFor(function() {
return self.accessors.editHeader.title.getAttribute('data-l10n-id') ===
'edit';
});
},
exitEditMode: function() {
var editHeader = this.accessors.editHeader.root;
// TODO: Use a GaiaHeader view, once bug 1241813 lands
this.accessors.editHeader.closeButton.tap(10,10);
this.accessors.callLogFilter;
this.client.helper.waitForElementToDisappear(editHeader);
},
deleteEntries: function(indexesToDelete) {
var logEntries = this.accessors.logEntries;
var entriesToDelete = logEntries.filter(function(_, index) {
return indexesToDelete.indexOf(index) > -1;
});
entriesToDelete.forEach(function(entry) {
entry.tap();
});
this._performDelete();
},
deleteAllEntries: function() {
this.accessors.selectAllButton.tap();
this._performDelete();
},
_performDelete: function() {
var editHeader = this.accessors.editHeader.root;
this.accessors.deleteButton.tap();
this.accessors.dialogConfirmButton.tap();
this.client.helper.waitForElementToDisappear(editHeader);
}
};
module.exports = CallLogView;

View file

@ -0,0 +1,23 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
goToCallLog: '#option-recents',
goToContactList: '#option-contacts'
});
function DialerOptionsAccessor(client) {
this.client = client;
}
DialerOptionsAccessor.prototype = {
get callLogButton() {
return this.client.helper.waitForElement(SELECTORS.goToCallLog);
},
get contactListButton() {
return this.client.helper.waitForElement(SELECTORS.goToContactList);
}
};
module.exports = DialerOptionsAccessor;

View file

@ -0,0 +1,28 @@
'use strict';
/* global module */
var DialerOptionsAccessor = require('./tabAccessors');
function DialerOptionsView(client) {
this.client = client;
this.accessors = new DialerOptionsAccessor(client);
}
DialerOptionsView.prototype = {
goToCallLog: function() {
var callLogTab = this.accessors.callLogButton;
callLogTab.tap();
var CallLogView = require('../call_log/views.js');
return new CallLogView(this.client);
},
goToContactList: function() {
var contactListTab = this.accessors.contactListButton;
contactListTab.tap();
var contacts = this.client.loader.getAppClass('contacts');
return contacts;
}
};
module.exports = DialerOptionsView;

View file

@ -0,0 +1,27 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
keypadCallBarAddContact: '#keypad-callbar-add-contact',
});
function KeypadAccessor(client) {
this.client = client;
}
KeypadAccessor.prototype = {
get addContactButton() {
return this.client.helper.waitForElement(SELECTORS.keypadCallBarAddContact);
},
key: function(keyNumber) {
return this.client.helper.waitForElement(
this.getKeypadKeySelector(keyNumber)
);
},
getKeypadKeySelector: function(key) {
return '.keypad-key[data-value="' + key +'"]';
}
};
module.exports = KeypadAccessor;

View file

@ -0,0 +1,36 @@
'use strict';
/* global module */
var KeypadAccessor = require('./accessors');
function KeypadView(client) {
this.client = client;
this.accessors = new KeypadAccessor(client);
}
KeypadView.prototype = {
typePhoneNumber: function(number) {
var i;
var typeNumberArray = String(number).split('');
for (i in typeNumberArray){
if (i === '+') {
var zero_key = this.key(0);
this.actions.longPress(zero_key, 1).perform();
} else {
i = parseInt(typeNumberArray[i]);
this.accessors.key(i).tap();
}
}
},
addToContacts: function() {
var addToContactsButton = this.accessors.addContactButton;
addToContactsButton.tap();
var ActivitiesView = require('../activities/views.js');
var activitiesView = new ActivitiesView(this.client);
return activitiesView;
}
};
module.exports = KeypadView;

View file

@ -0,0 +1,22 @@
'use strict';
var SELECTORS = Object.freeze({
callLogTabItem: '#option-recents',
contactsTabItem: '#option-contacts'
});
function TabsAccessors(client) {
this.client = client;
}
TabsAccessors.prototype = {
get callLogButton() {
return this.client.helper.waitForElement(SELECTORS.callLogTabItem);
},
get contactListButton() {
return this.client.helper.waitForElement(SELECTORS.contactsTabItem);
}
};
module.exports = TabsAccessors;

View file

@ -0,0 +1,25 @@
'use strict';
var TabsAccessors = require('./accessors');
var CallLogView = require('../call_log/view.js');
function TabsView(client) {
this.client = client;
this.accessors = new TabsAccessors(client);
}
TabsView.prototype = {
goToCallLog: function(expectedNumberOfItems) {
expectedNumberOfItems = expectedNumberOfItems || 0;
this.accessors.callLogButton.tap();
return new CallLogView(this.client, expectedNumberOfItems);
},
goToContactList: function() {
this.accessors.contactListButton.tap();
var contacts = this.client.loader.getAppClass('contacts');
return contacts;
},
};
module.exports = TabsView;

View file

@ -0,0 +1,3 @@
[navigation_test.js]
[keypad_test.js]
[call_log_test.js]

View file

@ -0,0 +1,46 @@
'use strict';
var Dialer = require('./lib/dialer');
marionette('Dialer > Navigation', function() {
var assert = require('assert');
var client = marionette.client(Dialer.config);
var subject;
var selectors;
var actions;
setup(function() {
actions = client.loader.getActions();
subject = new Dialer(client);
subject.launch();
selectors = Dialer.Selectors;
var keypad = subject.client.findElement(selectors.keypadView);
client.waitFor(function() {
return keypad.displayed();
});
});
test('Call Log', function() {
var tabItem = subject.client.findElement(selectors.callLogTabItem);
actions.tap(tabItem).perform();
var callLogTabs = subject.client.findElement(selectors.callLogTabs);
client.waitFor(function() {
return callLogTabs.displayed();
});
assert.ok(true, 'displayed the call log tabs');
});
test('Contacts', function() {
var tabItem = subject.client.findElement(selectors.contactsTabItem);
actions.tap(tabItem).perform();
var iframe = subject.client.findElement('#iframe-contacts');
client.waitFor(function() {
return iframe.displayed();
});
assert.ok(true, 'displayed the contacts iframe');
});
});