VulpesOS/gaia/apps/email/test/marionette/lib/email.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

768 lines
24 KiB
JavaScript
Executable file

/*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);