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,32 @@
'use strict';
/*global module, console, process */
// Run a test with DEBUG=true in front to get console listing of
// helper method calls:
// DEBUG=true ./bin/gaia-marionette whatever_test.js
var debug = process.env.DEBUG;
module.exports = function(logPrefix, obj) {
if (!debug) {
return;
}
// Optionally log all calls done to prototype methods. Uncomment this
// section to get traces when trying to debug where flow gets stuck.
Object.keys(obj).forEach(function(key) {
var desc = Object.getOwnPropertyDescriptor(obj, key);
if (!desc.get && !desc.set && typeof obj[key] === 'function') {
var oldMethod = obj[key];
obj[key] = function() {
var args = Array.prototype.slice.call(arguments, 0).map(function(arg) {
return String(arg);
}).join(', ');
console.log(logPrefix + '.' + key + '(' + args + ')');
return oldMethod.apply(this, arguments);
};
}
});
};

View file

@ -0,0 +1,768 @@
/*jshint node: true, browser: true */
'use strict';
function Email(client) {
this.client = client.scope({ searchTimeout: 20000 });
}
module.exports = Email;
Email.EMAIL_ORIGIN = 'app://email.gaiamobile.org';
var Selector = {
setupNameInput: 'cards-setup-account-info .sup-info-name',
setupEmailInput: 'cards-setup-account-info .sup-info-email',
nextButton: 'cards-setup-account-info .sup-info-next-btn',
prefsNextButton: 'cards-setup-account-prefs .sup-info-next-btn',
manualSetupNameInput: '.sup-manual-form .sup-info-name',
manualSetupEmailInput: '.sup-manual-form .sup-info-email',
manualSetupImapPasswordInput:
'.sup-manual-form .sup-manual-composite-password',
manualSetupImapUsernameInput:
'.sup-manual-form .sup-manual-composite-username',
manualSetupImapHostnameInput:
'.sup-manual-form .sup-manual-composite-hostname',
manualSetupImapPortInput: '.sup-manual-form .sup-manual-composite-port',
manualSetupImapSocket: '.sup-manual-form .sup-manual-composite-socket',
manualSetupSmtpUsernameInput: '.sup-manual-form .sup-manual-smtp-username',
manualSetupSmtpHostnameInput: '.sup-manual-form .sup-manual-smtp-hostname',
manualSetupSmtpPasswordInput: '.sup-manual-form .sup-manual-smtp-password',
manualSetupSmtpPortInput: '.sup-manual-form .sup-manual-smtp-port',
manualSetupSmtpSocket: '.sup-manual-form .sup-manual-smtp-socket',
manualNextButton: 'cards-setup-manual-config .sup-manual-next-btn',
msgDownBtn: 'cards-message-reader .msg-down-btn',
msgListScrollOuter: 'cards-message-list .msg-list-scrollouter',
editMode: 'cards-message-list .msg-edit-btn',
editModeCheckBoxes:
'cards-message-list .msg-header-item[data-index="0"] label.pack-checkbox',
editModeTrash: 'cards-message-list button.msg-delete-btn',
msgUpBtn: 'cards-message-reader .msg-up-btn',
msgEnvelopeSubject: 'cards-message-reader .msg-envelope-subject',
showMailButton: 'cards-setup-done .sup-show-mail-btn',
confirmDialogOkButton: '.confirm-dialog-form .confirm-dialog-ok',
manualConfigButton: '.scrollregion-below-header .sup-manual-config-btn',
composeButton: '.msg-list-header .msg-compose-btn',
composeEmailContainer: 'cards-compose .cmp-to-container',
composeEmailInput: 'cards-compose .cmp-addr-text',
composeSubjectInput: 'cards-compose .cmp-subject-text',
composeBodyInput: 'cards-compose .cmp-body-text',
composeSendButton: 'cards-compose .cmp-send-btn',
composeBackButton: 'cards-compose .cmp-back-btn',
composeDraftDiscard: '#cmp-draft-discard',
composeDraftSave: '#cmp-draft-save',
composeErrorMessage: 'cards-compose .cmp-error-message',
refreshButton: '.card.center .msg-refresh-btn',
messageHeaderItem:
'.msg-vscroll-container .msg-header-item',
cardMessageReader: 'cards-message-reader',
currentCardInputs: '.card.center input[type="text"]',
replyMenuButton: '.msg-reply-btn',
replyMenu: '.msg-reply-menu',
replyMenuReply: '.msg-reply-menu-reply',
replyMenuForward: '.msg-reply-menu-forward',
replyMenuAll: '.msg-reply-menu-reply-all',
searchTextTease: '.msg-search-text-tease',
searchCard: 'cards-message-list-search',
folderListButton: '.msg-list-header .msg-folder-list-btn',
folderListCloseButton: 'cards-folder-picker .fld-header-back',
folderListContents: 'cards-folder-picker .fld-acct-scrollinner',
settingsButton: '.fld-nav-toolbar',
settingsDoneButton: 'cards-settings-main [data-l10n-id="settings-done"]',
addAccountButton: 'cards-settings-main .tng-account-add',
accountListButton: '.fld-acct-header',
accountListContainer: '.fld-accountlist-container',
settingsMainAccountItems: '.tng-accounts-container .tng-account-item',
syncIntervalSelect: '.tng-account-check-interval ',
// Checkboxes are weird: hidden to marionette, but the associated label
// is clickable and does the job.
notifyEmailCheckbox: '.tng-notify-mail-label',
accountSettingsBackButton: 'cards-settings-account .tng-back-btn',
localDraftsItem: '.fld-folders-container a[data-type=localdrafts]',
outboxItem: '.fld-folders-container a[data-type=outbox]',
outboxItemSyncIcon: '.msg-header-syncing-section',
msgLastSync: '.msg-last-synced-value',
toaster: 'section[role="status"]'
};
Email.prototype = {
/**
* Send some emails and then receive them.
*
* @param {Array} messages list of messages with to, subject, and body.
* @param {Number} [messageSyncIndex] the index into the list of messages in
* the message list that indicates synchronization is complete. Uses the last
* index in messages by default.
*/
sendAndReceiveMessages: function(messages, messageSyncIndex) {
if (messageSyncIndex === undefined) {
messageSyncIndex = messages.length - 1;
}
messages.forEach(function(message) {
this.tapCompose();
this.typeTo(message.to);
this.typeSubject(message.subject);
this.typeBody(message.body);
this.tapSend();
}.bind(this));
this.tapRefreshButton();
this.waitForSynchronized(messageSyncIndex);
},
waitForToaster: function() {
var toaster = this.client.helper.waitForElement(Selector.toaster);
this.client.helper.waitForElementToDisappear(toaster);
},
get msgDownBtn() {
return this.client.helper.waitForElement(Selector.msgDownBtn);
},
get msgListScrollOuter() {
return this.client.helper.waitForElement(Selector.msgListScrollOuter);
},
get msgUpBtn() {
return this.client.helper.waitForElement(Selector.msgUpBtn);
},
/**
* @param {boolean} up whether we're advancing up or down.
*/
advanceMessageReader: function(up) {
var el = up ? this.msgUpBtn : this.msgDownBtn;
el.click();
this.waitForMessageReader();
},
getMessageReaderSubject: function() {
return this.client.helper
.waitForElement(Selector.msgEnvelopeSubject)
.text();
},
getComposeTo: function() {
var container =
this.client.helper.waitForElement(Selector.composeEmailContainer);
var text = container.text();
return text;
},
getComposeErrorMessage: function() {
return this.client.helper
.waitForElement(Selector.composeErrorMessage)
.text();
},
getLastSyncText: function() {
return this.client.helper
.waitForElement(Selector.msgLastSync)
.text();
},
manualSetupImapEmail: function(server, finalActionName) {
// setup a IMAP email account
var email = server.imap.username + '@' + server.imap.hostname;
// wait for the setup page is loaded
this._setupTypeName(server.imap.username);
this._setupTypeEmail(email);
this._waitForElementNoTransition(Selector.manualConfigButton).tap();
this._waitForTransitionEnd('setup_manual_config');
this._manualSetupTypeImapUsername(server.imap.username);
this._manualSetupTypeImapHostname(server.imap.hostname);
this._manualSetupTypeImapPassword(server.imap.password);
this._manualSetupTypeImapPort(server.imap.port);
this._manualSetupUpdateSocket('manualSetupImapSocket');
this._manualSetupTypeSmtpUsername(server.smtp.username);
this._manualSetupTypeSmtpHostname(server.smtp.hostname);
this._manualSetupTypeSmtpPassword(server.smtp.password);
this._manualSetupTypeSmtpPort(server.smtp.port);
this._manualSetupUpdateSocket('manualSetupSmtpSocket');
this._finishSetup(Selector.manualNextButton, finalActionName);
},
_finishSetup: function(nextSelector, finalActionName) {
this._tapNext(nextSelector);
this._waitForElementNoTransition(Selector.prefsNextButton);
this._tapNext(Selector.prefsNextButton, 'setup_done');
this._waitForElementNoTransition(Selector.showMailButton);
this.client.helper
.waitForElement(Selector.showMailButton)
.tap();
return this[finalActionName || 'waitForMessageList']();
},
// Waits for confirm dialog to show up, then clicks OK to confirm
// going to setting up a new account after triggering email launch
// from an activity.
confirmWantAccount: function() {
this.client.switchToFrame();
this.client.apps.switchToApp(Email.EMAIL_ORIGIN);
this.client.helper.waitForElement(Selector.confirmDialogOkButton).tap();
this.client.helper.waitForElement(Selector.manualConfigButton);
this.client.helper.waitForElement(Selector.manualConfigButton).tap();
},
tapFolderListButton: function() {
this._tapSelector(Selector.folderListButton);
this._waitForElementNoTransition(Selector.settingsButton);
this._waitForTransitionEnd('folder_picker');
this.client.helper.waitForElement(Selector.folderListContents);
},
tapFolderListCloseButton: function() {
this._tapSelector(Selector.folderListCloseButton);
this.waitForMessageList();
},
tapAccountListButton: function() {
// XXX: Workaround util http://bugzil.la/912873 is fixed.
// Wait for 500ms to let the element be clickable
this.client.helper.wait(500);
this.client.helper.waitForElement(Selector.accountListButton).tap();
this.client.helper.waitForElement(Selector.accountListContainer);
},
tapLocalDraftsItem: function() {
// we should already be looking at the folder list, no need to wait.
this._waitForElementNoTransition(Selector.localDraftsItem).tap();
// clicking that transitions us back to the message list; wait for us
// to get there.
this._waitForTransitionEnd('message_list');
},
tapOutboxItem: function() {
this._waitForElementNoTransition(Selector.outboxItem).tap();
this._waitForTransitionEnd('message_list');
},
getOutboxItemSyncIconForIndex: function(index) {
var header = this.getHeaderAtIndex(index);
var iconEl = header.findElement(Selector.outboxItemSyncIcon);
var className = iconEl.getAttribute('className');
if (/-syncing$/.test(className)) {
return 'syncing';
} else if (/-error$/.test(className)) {
return 'error';
} else {
return '';
}
},
switchAccount: function(number) {
var accountSelector = '.fld-accountlist-container ' +
'a:nth-child(' + number + ')';
this.client.helper
.waitForElement(accountSelector)
.tap();
this._waitForTransitionEnd('folder_picker');
},
tapSettingsButton: function() {
this.client.helper
.waitForElement(Selector.settingsButton)
.tap();
this._waitForTransitionEnd('settings_main');
},
tapSettingsDoneButton: function() {
this.client.helper
.waitForElement(Selector.settingsDoneButton)
.tap();
this._waitForTransitionEnd('folder_picker');
},
tapSettingsAccountIndex: function(index) {
var elements = this.client.findElements(Selector.settingsMainAccountItems);
this.client.helper.waitForElement(elements[index]).tap();
this._waitForTransitionEnd('settings_account');
},
tapAddAccountButton: function() {
this.client.helper
.waitForElement(Selector.addAccountButton)
.tap();
this._waitForTransitionEnd('setup_account_info');
},
tapNotifyEmailCheckbox: function() {
this._tapSelector(Selector.notifyEmailCheckbox);
},
tapAccountSettingsBackButton: function() {
this.client.helper
.waitForElement(Selector.accountSettingsBackButton)
.tap();
this._waitForTransitionEnd('settings_main');
},
tapCompose: function() {
this._tapSelector(Selector.composeButton);
this.waitForCompose();
},
typeTo: function(email) {
this.client.helper
.waitForElement(Selector.composeEmailInput)
.sendKeys(email);
},
typeSubject: function(subject) {
this.client.helper
.waitForElement(Selector.composeSubjectInput)
.sendKeys(subject);
},
typeBody: function(string) {
var bodyInput = this.client.
findElement(Selector.composeBodyInput);
bodyInput.click();
bodyInput.sendKeys(string);
},
/**
* Retrieve the textual representation of the contenteditable that is our
* compose area using the app logic that does this for when we save and
* send the draft. Using innerHTML would not be realistic and textContent
* over-simplifies our newline handling as well as risking oddities in the
* future if we inline various affordances proposed by UX.
*/
getComposeBody: function() {
return this.client.executeScript(function() {
var cards = window.wrappedJSObject.require('cards'),
card = cards._cardStack[cards.activeCardIndex];
if (card.nodeName.toLowerCase() !== 'cards-compose') {
throw new Error('active card should be compose!');
}
return card.fromEditor();
}, []);
},
/**
* Enters edit mode.
* Waits for an edit checkbox to appear.
*/
editMode: function() {
this.client.helper
.waitForElement(Selector.editMode)
.tap();
this.client.helper
.waitForElement(Selector.editModeCheckBoxes);
},
/**
* Returns the edit mode checkboxes.
*/
editModeCheckboxes: function() {
var elements = this.client.findElements(Selector.editModeCheckBoxes);
return elements;
},
isElementDisabled: function(selector) {
var client = this.client;
client.helper.waitForElement(selector);
client.waitFor(function() {
return client.executeScript(function(selector) {
var doc = window.wrappedJSObject.document,
selectNode = doc.querySelector(selector);
return selectNode.disabled;
}, [selector]);
});
},
/**
* Taps the trash button in edit mode.
*/
isEditModeTrashDisabled: function() {
this.isElementDisabled(Selector.editModeTrash);
},
abortCompose: function(cardId) {
this._waitForElementNoTransition(Selector.composeBackButton).tap();
this._waitForElementNoTransition(Selector.composeDraftDiscard).tap();
this._waitForTransitionEnd(cardId);
},
/**
* Returns the visible input elements for the current card.
*/
getVisibleCardInputs: function() {
var elements = this.client.findElements(Selector.currentCardInputs)
.filter(function(element) {
return element.displayed();
});
return elements;
},
saveLocalDrafts: function() {
this._waitForElementNoTransition(Selector.composeBackButton).tap();
this._waitForElementNoTransition(Selector.composeDraftSave).tap();
this._waitForTransitionEnd('message_list');
},
tapSend: function() {
var client = this.client;
/*
* We cannot tap the Selector.composeSendButton element with (0, 0) offset,
* because the attachment button covers the left side of it a little bit.
*
* We could refer the css style in:
* https://github.com/mozilla-b2g/gaia/blob/master/shared/style/headers.css#L117
* And the patch in http://bugzil.la/907061 make us skip the issue luckily.
*
* We discuss and fix the issue in http://bugzil.la/907092
*/
client.helper
.waitForElement(Selector.composeSendButton)
.tap();
// wait for being in the email list page
this._waitForElementNoTransition(Selector.refreshButton);
this.waitForMessageList();
},
tapRefreshButton: function() {
this.client.helper
.waitForElement(Selector.refreshButton)
.tap();
},
tapSearchArea: function() {
this.client.helper
.waitForElement(Selector.searchTextTease)
.sendKeys('a');
var client = this.client;
client.waitFor(function() {
return client.executeScript(function(selector) {
var doc = window.wrappedJSObject.document,
selectNode = doc.querySelector(selector);
// Synthesize an event since focus does not work
// through marionette API
var event = document.createEvent('Event');
event.initEvent('focus', true, true);
selectNode.dispatchEvent(event);
return true;
}, [Selector.searchTextTease]);
});
this.client.helper
.waitForElement(Selector.searchCard);
this._waitForTransitionEnd('message_list_search');
},
waitForMessageList: function() {
this._waitForTransitionEnd('message_list');
},
waitForMessageReader: function() {
this._waitForTransitionEnd('message_reader');
},
waitForCompose: function() {
this._waitForTransitionEnd('compose');
},
waitForSynchronized: function(index) {
this.getHeaderAtIndex(index);
},
launch: function() {
var client = this.client;
client.apps.launch(Email.EMAIL_ORIGIN);
client.apps.switchToApp(Email.EMAIL_ORIGIN);
// wait for the document body to know we're really launched
client.helper.waitForElement('body');
},
close: function() {
var client = this.client;
client.apps.close(Email.EMAIL_ORIGIN);
},
getHeaderAtIndex: function(index) {
var element = this.client.findElement(Selector.messageHeaderItem +
'[data-index="' + index + '"]');
return this.client.helper.waitForElement(element);
},
tapEmailAtIndex: function(index) {
var element = this.getHeaderAtIndex(index);
element.tap();
this.waitForMessageReader();
},
tapEmailBySubject: function(subject, cardId) {
// The emails may not be present in the list yet. So keep checking until
// we see one. Then tap on it.
this.client.waitFor(function() {
var element = this.getEmailBySubject(subject);
if (!element) {
return false;
}
element.tap();
this._waitForTransitionEnd(cardId);
return true;
}.bind(this));
},
getMessageCount: function() {
return this.client.findElements(Selector.messageHeaderItem).length;
},
getEmailBySubject: function(subject) {
var messageHeaders = this.client.findElements(Selector.messageHeaderItem),
messageHeadersLength = messageHeaders.length,
element;
for (var i = 0; i < messageHeadersLength; i++) {
var header = this.client.helper
.waitForChild(messageHeaders[i], '.msg-header-subject')
.text();
if (header === subject) {
element = messageHeaders[i];
}
}
return element;
},
/**
* Opens the reply menu and selects 'reply', 'all', or 'forward'.
*/
tapReply: function(mode) {
var client = this.client;
// open the reply menu
client.helper.waitForElement(Selector.replyMenuButton).tap();
client.helper.waitForElement(Selector.replyMenu);
// select the appropriate option
var whichButton;
switch (mode) {
case 'all':
whichButton = Selector.replyMenuAll;
break;
case 'forward':
whichButton = Selector.replyMenuForward;
break;
case 'reply':
whichButton = Selector.replyMenuReply;
break;
default:
whichButton = Selector.replyMenuReply;
break;
}
client.helper.waitForElement(whichButton).tap();
this._waitForTransitionEnd('compose');
},
setSyncIntervalSelectValue: function(value) {
return this._setSelectValue(Selector.syncIntervalSelect, value);
},
// TODO: switch to https://github.com/mozilla-b2g/marionette-plugin-forms
// once this bug is fixed:
// https://bugzilla.mozilla.org/show_bug.cgi?id=915324
_setSelectValue: function(selector, value) {
var client = this.client;
client.waitFor(function() {
return client.executeScript(function(selector, value) {
var doc = window.wrappedJSObject.document,
selectNode = doc.querySelector(selector);
selectNode.value = value;
// Synthesize an event since changing the value on its own does
// not trigger change listeners.
var event = document.createEvent('Event');
event.initEvent('change', true, true);
selectNode.dispatchEvent(event);
return true;
}, [selector, value]);
});
},
_onTransitionEndScriptTimeout: function(cardId) {
var result = this.client.executeScript(function(cardId) {
var cards = window.wrappedJSObject.require('cards'),
htmlCache = window.wrappedJSObject.require('html_cache'),
cardNode = cards._cardStack[cards.activeCardIndex];
return {
cardNode: !!cardNode,
centered: cardNode && cardNode.classList.contains('center'),
correctId: cardNode && htmlCache.nodeToKey(cardNode) === cardId,
eventsClear: !cards._eatingEventsUntilNextCard
};
}, [cardId]);
console.log('TRANSITION END TIMEOUT:');
console.log(JSON.stringify(result, null, ' '));
},
_waitForTransitionEnd: function(cardId) {
var client = this.client;
// To find out what is wrong with an intermittent failure in here,
// log the script test criteria
client.onScriptTimeout = this._onTransitionEndScriptTimeout
.bind(this, cardId);
client.waitFor(function() {
return client.executeScript(function(cardId) {
var cards = window.wrappedJSObject.require('cards'),
htmlCache = window.wrappedJSObject.require('html_cache'),
cardNode = cards._cardStack[cards.activeCardIndex];
return !!cardNode && cardNode.classList.contains('center') &&
htmlCache.nodeToKey(cardNode) === cardId &&
!cards._eatingEventsUntilNextCard;
}, [cardId]);
});
client.onScriptTimeout = null;
},
_onNoTransitionScriptTimeout: function() {
var result = this.client.executeScript(function() {
var cards = window.wrappedJSObject.require('cards');
return {
cards: !!cards,
eventsClear: !!cards && !cards._eatingEventsUntilNextCard
};
});
console.log('NO TRANSITION TIMEOUT:');
console.log(JSON.stringify(result, null, ' '));
},
_waitForNoTransition: function() {
var client = this.client;
// To find out what is wrong with an intermittent failure in here,
// log the script test criteria
client.onScriptTimeout = this._onNoTransitionScriptTimeout
.bind(this);
client.waitFor(function() {
return client.executeScript(function() {
var cards = window.wrappedJSObject.require('cards');
return !cards._eatingEventsUntilNextCard;
});
});
client.onScriptTimeout = null;
},
_setupTypeName: function(name) {
this.client.helper
.waitForElement(Selector.setupNameInput)
.sendKeys(name);
},
_setupTypeEmail: function(email) {
this.client.helper
.waitForElement(Selector.setupEmailInput)
.sendKeys(email);
},
_waitForElementNoTransition: function(selector) {
this._waitForNoTransition();
return this.client.helper.waitForElement(selector);
},
_tapSelector: function(selector) {
this.client.helper.waitForElement(selector).tap();
},
_tapNext: function(selector, cardId) {
this._tapSelector(selector);
if (cardId) {
this._waitForTransitionEnd(cardId);
}
},
_clearAndSendKeys: function(selector, value) {
var el = this.client.helper.waitForElement(selector);
el.clear();
el.sendKeys(value);
},
_manualSetupTypeName: function(name) {
this._clearAndSendKeys(Selector.manualSetupNameInput, name);
},
_manualSetupTypeEmail: function(email) {
this._clearAndSendKeys(Selector.manualSetupEmailInput, email);
},
_manualSetupTypeImapPassword: function(password) {
this._clearAndSendKeys(Selector.manualSetupImapPasswordInput, password);
},
_manualSetupTypeSmtpPassword: function(password) {
this._clearAndSendKeys(Selector.manualSetupSmtpPasswordInput, password);
},
_manualSetupTypeImapUsername: function(name) {
this._clearAndSendKeys(Selector.manualSetupImapUsernameInput, name);
},
_manualSetupTypeImapHostname: function(hostname) {
this._clearAndSendKeys(Selector.manualSetupImapHostnameInput, hostname);
},
_manualSetupTypeImapPort: function(port) {
this._clearAndSendKeys(Selector.manualSetupImapPortInput, port);
},
_manualSetupTypeSmtpUsername: function(name) {
this._clearAndSendKeys(Selector.manualSetupSmtpUsernameInput, name);
},
_manualSetupTypeSmtpHostname: function(hostname) {
this._clearAndSendKeys(Selector.manualSetupSmtpHostnameInput, hostname);
},
_manualSetupTypeSmtpPort: function(port) {
this._clearAndSendKeys(Selector.manualSetupSmtpPortInput, port);
},
/**
* Because we never expose "plain" (zero security for users) as an option we
* need to hack the html to expose it (the backend will know about this).
*/
_manualSetupUpdateSocket: function(type) {
var element = this.client.helper.waitForElement(Selector[type]);
// select is a real dom select element
element.scriptWith(function(select) {
// create the option
var option = document.createElement('option');
option.value = 'plain';
select.add(option, select.options[select.options.length - 1]);
// update the form to plain so we can use insecure sockets for the
// fakeserver.
select.value = 'plain';
});
}
};
require('./debug')('email', Email.prototype);

View file

@ -0,0 +1,68 @@
'use strict';
/*jshint node: true, browser: true */
/*global marionetteScriptFinished */
/**
* Manipulates email's data APIs in the app.
*/
function EmailData(client) {
this.client = client;
}
module.exports = EmailData;
EmailData.prototype = {
/**
* Gets the current account for the given account ID. Just returns the
* _wireRep as that will survive the serialization hops.
* @param {String} accountId
* @return {Object} the account._wireRep object, or undefined if not found.
*/
getCurrentAccount: function() {
var client = this.client;
return client.executeAsyncScript(function() {
var model = window.wrappedJSObject.require('model_create').defaultModel;
model.latest('account', function(account) {
marionetteScriptFinished(account._wireRep);
});
});
},
/**
* Gets the current account for the given account ID. Just returns the
* _wireRep as that will survive the serialization hops.
* @param {String} accountId
* @return {Object} the account._wireRep object, or undefined if not found.
*/
waitForCurrentAccountUpdate: function(key, value) {
var client = this.client;
return client.executeAsyncScript(function(key, value) {
var account,
model = window.wrappedJSObject.require('model_create').defaultModel,
acctsSlice = model.api.viewAccounts(false);
function checkDone() {
if (account[key] === value) {
acctsSlice.die();
marionetteScriptFinished(true);
}
}
acctsSlice.oncomplete = function() {
account = acctsSlice.defaultAccount;
checkDone();
};
acctsSlice.onchange = function(item) {
if (account) {
if (account.id === item.id) {
checkDone();
}
}
};
}, [key, value]);
}
};
require('./debug')('email_data', EmailData.prototype);

View file

@ -0,0 +1,44 @@
'use strict';
/*jshint node: true, browser: true */
var SHARED_PATH = __dirname + '/../../../../../shared/test/integration';
function EmailSync(client) {
this.client = client;
}
module.exports = EmailSync;
EmailSync.prototype = {
/**
* Call this in the setup() for the test.
*/
setup: function() {
this.client.contentScript.inject(
SHARED_PATH + '/mock_navigator_moz_set_message_handler.js'
);
this.client.contentScript.inject(
SHARED_PATH + '/mock_navigator_mozalarms.js'
);
},
/**
* Does the work to trigger a sync using helpers in
* mock_navigator_moz_set_message_handler.js.
*/
triggerSync: function() {
// trigger sync in Email App
this.client.executeScript(function() {
var interval = 1000;
var date = new Date(Date.now() + interval).getTime();
var alarm = {
data: {
type: 'sync',
accountIds: ['0', '1'],
interval: interval,
timestamp: date
}
};
return window.wrappedJSObject.fireMessageHandler(alarm, 'alarm');
});
}
};

View file

@ -0,0 +1,37 @@
/*global Components, Services */
'use strict';
Components.utils.import('resource://gre/modules/Services.jsm');
Services.obs.addObserver(function(document) {
if (!document || !document.location) {
return;
}
var window = document.defaultView.wrappedJSObject;
var messageHandler;
window.navigator.__defineGetter__('mozHasPendingMessage', function() {
return function(type) {
return type === 'activity';
};
});
window.navigator.__defineGetter__('mozSetMessageHandler', function() {
return function(type, callback) {
if (type === 'activity') {
messageHandler = callback;
}
};
});
window.__defineGetter__('fireMessageHandler', function() {
return function(data) {
if (messageHandler && typeof(messageHandler) === 'function') {
messageHandler(data);
return true;
}
return false;
};
});
}, 'document-element-inserted', false);

View file

@ -0,0 +1,98 @@
/*jshint node: true, browser: true */
'use strict';
var assert = require('assert'),
containerSelector = '#desktop-notifications-container .other-notifications';
function Notification(client) {
this.client = client;
}
module.exports = Notification;
Notification.prototype = {
getContainer: function() {
return this.client.findElement(containerSelector);
},
assertNoNotification: function() {
this.getContainer().findElement('div', function(error) {
assert.ok(!!error);
});
},
/**
* Uses Notification.get to get the first notification to show up. This
* approach is used instead of something like tapping on the notification,
* since the notification UI is normally hidden and it is non-trivial to open,
* and the data API really reflects the result of the notification work done
* by the email app.
*/
triggerFirstNotification: function() {
var client = this.client;
return client.executeAsyncScript(function() {
function getFirstNotification() {
window.Notification.get().then(function(notifications) {
if (!notifications) {
marionetteScriptFinished(false);
} else if (!notifications.length) {
// The check was done too fast. Wait for some time to pass
// and check again.
window.setTimeout(getFirstNotification, 300);
} else {
var notification = notifications[0],
evt = window.wrappedJSObject.require('evt');
evt.emit('notification', {
clicked: true,
imageURL: notification.imageURL,
data: notification.data,
tag: notification.tag
});
marionetteScriptFinished(true);
}
}).catch(function(error) {
console.error('Notification.get failed with: ' + error);
marionetteScriptFinished(false);
});
}
getFirstNotification();
});
},
/**
* Uses Notification.get to get the first notification's data.
*/
getFirstNotificationData: function() {
var client = this.client;
return client.executeAsyncScript(function() {
function getFirstNotification() {
window.Notification.get().then(function(notifications) {
if (!notifications) {
marionetteScriptFinished(false);
} else if (!notifications.length) {
// The check was done too fast. Wait for some time to pass
// and check again.
window.setTimeout(getFirstNotification, 300);
} else {
var notification = notifications[0];
marionetteScriptFinished({
imageURL: notification.imageURL,
data: notification.data,
tag: notification.tag
});
}
}).catch(function(error) {
console.error('Notification.get failed with: ' + error);
marionetteScriptFinished(false);
});
}
getFirstNotification();
});
}
};
require('./debug')('notification', Notification.prototype);

View file

@ -0,0 +1,52 @@
/**
* Primary interface to expose server details to tests. Internally abstracts the
* details of what kind of server (aside from general protocol) so we can also
* use fake servers.
*/
/**
* Example interface for server setup.
*
* var object = require('exported_module').use(options, this);
*
* // object is expected to have one or more top level server types exposed.
*
* // example imap interface
* object.imap.port
* object.imap.username
* object.imap.hostname
* object.imap.password
*
* // example imap interface
* object.smtp.port
* object.smtp.username
* object.smtp.hostname
* object.smtp.password
*
* While the "port" may be optional (you might need a url instead)
* username/password should always be included.
*
*/
//function serverSetup(options) {};
'use strict';
/* jshint node: true */
/**
* Expose a server lifecycle to a given test.
*
* marionette('xfoo', function() {
* require('./lib/server_helper').use({}, this);
* });
*
* @param {Object} options for server.
* @param {Object} mochaContext
* (usually the |this| of a marionette/suite block).
*/
function determineServer(options, mochaContext) {
// XXX: right now this is hardcoded to the fake imap server logic.
return require('./servers/fakeimap').use(options, mochaContext);
}
module.exports.use = determineServer;

View file

@ -0,0 +1,99 @@
'use strict';
/* jshint node: true */
var server = require('mail-fakeservers');
var DEFAULT_OPTIONS = Object.freeze({
// convention from GELAM
credentials: {
username: 'testy',
password: 'testy'
}
});
/**
* Updates given object with the state from an imapStack.
*
* @param {Object} state target.
* @param {Object} stack to pull updates from.
* @param {Object} options for setup.
*/
function updateState(state, stack, options) {
if (!options) {
options = DEFAULT_OPTIONS;
}
state.imap = {
port: stack.imapPort,
username: options.credentials.username,
password: options.credentials.password,
hostname: 'localhost'
};
state.smtp = {
port: stack.smtpPort,
username: 'outgoingUsername' in options.credentials ?
options.credentials.outgoingUsername : options.credentials.username,
password: 'outgoingPassword' in options.credentials ?
options.credentials.outgoingPassword : options.credentials.password,
hostname: 'localhost'
};
}
/**
* @param {Object} options for setup.
* @param {Object} mochaContext from callee.
*/
function use(options, mochaContext) {
/**
* Reused for every test/setup/teardown contains internal state so tests can
* just directly reference the same object instead of manging state
* themselves.
*
* @type {Object}
* @private
*/
var state = {};
// spawns servers
var controlServer;
// current imap/smtp servers
var imapStack;
if (options === null) {
options = DEFAULT_OPTIONS;
}
suiteSetup(function(done) {
this.timeout('20s');
server.create(function(err, control) {
controlServer = control;
done(err);
});
});
// we need a new stack each test
setup(function(done) {
controlServer.createImapStack(options, function(err, imap) {
// update the state information
updateState(state, imap, options);
imapStack = imap;
done(err);
});
});
// clear away old servers
teardown(function(done) {
controlServer.cleanupStacks(done);
});
// so node can exit close the server processes.
suiteTeardown(function() {
controlServer.kill();
});
return state;
}
module.exports.use = use;