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,132 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
main: '.panel-ConversationView',
message: '.message',
messageBubble: '.message .bubble',
header: '#messages-header',
headerTitle: '#messages-header-text',
headerActionButton: '.action-button',
container: '#messages-container',
callButton: '#messages-call-number-button',
// TODO: Remove the next selector once Message has its own accessors
messageContent: '.message-content > p:first-child',
createNewContactMenuOption: '.contact-prompt ' +
'[data-l10n-id="createNewContact"]',
addToExistingContactOption: '.contact-prompt ' +
'[data-l10n-id="addToExistingContact"]',
carrierHeaderPhoneNumber: '#contact-carrier .phone-number',
optionsButton: '#messages-options-button'
});
var EDIT_MODE_SELECTORS = Object.freeze({
toggleSelectionButton: '#messages-check-uncheck-all-button',
editHeaderTitle: '#messages-edit-mode'
});
function ConversationAccessor(client) {
this.client = client;
this.actions = client.loader.getActions();
}
ConversationAccessor.prototype = {
get messages() {
return this.client.findElements(SELECTORS.message);
},
get message() {
return this.client.helper.waitForElement(SELECTORS.messageBubble);
},
get header() {
return this.client.helper.waitForElement(SELECTORS.header);
},
get headerTitle() {
return this.client.helper.waitForElement(SELECTORS.headerTitle);
},
get editHeaderTitle() {
return this.client.helper.waitForElement(
EDIT_MODE_SELECTORS.editHeaderTitle
);
},
get headerActionButton() {
return this.client.helper.waitForElement(SELECTORS.headerActionButton);
},
get carrierHeaderPhoneNumber() {
return this.client.helper.waitForElement(
SELECTORS.carrierHeaderPhoneNumber
);
},
get callButton() {
return this.client.helper.waitForElement(SELECTORS.callButton);
},
get optionsButton() {
return this.client.helper.waitForElement(SELECTORS.optionsButton);
},
get toggleSelectionButton() {
return this.client.helper.waitForElement(
EDIT_MODE_SELECTORS.toggleSelectionButton
);
},
get createNewContactOption() {
return this.client.helper.waitForElement(
SELECTORS.createNewContactMenuOption
);
},
get addToExistingContactOption() {
return this.client.helper.waitForElement(
SELECTORS.addToExistingContactOption
);
},
getMessageContent: function(message) {
return this.client.helper.waitForElement(
message.findElement(SELECTORS.messageContent)
);
},
findMessage: function(id) {
return this.client.findElement(
'.message[data-message-id="' + id + '"] .bubble'
);
},
scrollUp: function() {
var conversationContainer = this.client.findElement(
SELECTORS.container
);
this.actions.flick(conversationContainer, 50, 50, 50, 350).perform();
},
fakeScrollUpTo: function(scrollTop) {
var conversationContainer = this.client.findElement(
SELECTORS.container
);
conversationContainer.scriptWith(function(container, scrollTop) {
container.scrollTop = scrollTop;
}, [scrollTop]);
},
waitToAppear: function() {
var conversationPanel = this.client.helper.waitForElement(SELECTORS.main);
this.client.waitFor(function() {
return conversationPanel.location().x === 0;
});
return conversationPanel;
}
};
module.exports = ConversationAccessor;

View file

@ -0,0 +1,72 @@
'use strict';
/* global module */
var SELECTORS = Object.freeze({
main: '.message[data-message-id="{id}"]',
bubble: '.bubble',
content: '.message-content-body',
attachment: '.attachment-container',
downloadButton: 'button.download'
});
function MessageAccessor(client) {
this.client = client.scope({ searchTimeout: 50 });
this.actions = client.loader.getActions();
}
MessageAccessor.prototype = {
find: function(messageId) {
return this.client.findElement(SELECTORS.main.replace('{id}', messageId));
},
parse: function(messageNode) {
// It's workaround for the case when message node reference is stale,
// so when we try to access MarionetteElement from the node that's not
// attached to the dom we'll get an exception.
try {
var messageClass = messageNode.getAttribute('class');
var checkbox = messageNode.findElement('input[type=checkbox]');
// Since most of the messages don't contain any attachments, let's make
// it much faster.
var attachments = this.client.scope({ searchTimeout: 50 }).findElements(
SELECTORS.attachment, null /* default search method */, messageNode.id
);
return {
id: +messageNode.getAttribute('data-message-id'),
type: messageClass.indexOf('mms') >= 0 ? 'mms' : 'sms',
content: messageNode.findElement(SELECTORS.content).text(),
attachments: attachments.map(function(attachment) {
return {
screenshot: function() {
return this.client.screenshot({ element: attachment });
}.bind(this),
type: attachment.getAttribute('data-attachment-type')
};
}, this),
isDownloaded: messageClass.indexOf('not-downloaded') < 0,
isPending: messageClass.indexOf('pending') >= 0,
isInEditMode: messageNode.findElement('.pack-checkbox').displayed(),
isSelected: checkbox.getAttribute('checked') === 'true',
isDisplayed: messageNode.displayed()
};
} catch(e) {
console.warn('Message node is not available', e, e.stack);
return null;
}
},
download: function(messageId) {
var messageNode = this.find(messageId);
if (this.parse(messageNode).isDownloaded) {
throw new Error('Message has been already downloaded!');
}
messageNode.findElement(SELECTORS.downloadButton).tap();
}
};
module.exports = MessageAccessor;

View file

@ -0,0 +1,287 @@
'use strict';
/* global module */
var ConversationAccessor = require('./accessors');
var MessageAccessor = require('./message_accessors');
var ComposerAccessor = require('../shared/composer_accessors');
var MenuAccessor = require('../shared/menu_accessors');
var Tools = require('../shared/tools.js');
var appRoot = require('app-root-path');
// TODO Change the path once requireFromApp becomes its own module
var fromApp = require(appRoot + '/shared/test/integration/require_from_app');
var Contacts = fromApp('contacts').require('lib/contacts');
function ConversationView(client) {
this.client = client;
this.accessors = new ConversationAccessor(client);
this.messageAccessors = new MessageAccessor(client);
this.composerAccessors = new ComposerAccessor(client);
this.menuAccessors = new MenuAccessor(client);
}
ConversationView.prototype = {
// TODO Move these constants to marionette, see bug 1207516
KEYS: {
backspace: '\ue003'
},
get headerTitle() {
return this.accessors.headerTitle.text();
},
get editHeaderTitle() {
return this.accessors.editHeaderTitle.text();
},
get toggleSelectionButtonTitle() {
return this.accessors.toggleSelectionButton.text();
},
/**
* Returns the messages in this conversation, following an optional filter.
* @param {Object} [filter] Optional filter
* @param {Number} [filter.first] Will return the first n messages.
* @returns {ParsedMessage[]} List of the messages found.
*/
messages: function(filter) {
filter = filter || [];
var messages = this.accessors.messages;
if (filter.first) {
messages = messages.slice(0, filter.first);
}
return messages.map(function(message) {
return this.messageAccessors.parse(message);
}, this);
},
get carrierHeaderPhoneNumber() {
return this.accessors.carrierHeaderPhoneNumber.text();
},
findMessage: function(messageId) {
var messageNode = this.messageAccessors.find(messageId);
return messageNode && this.messageAccessors.parse(messageNode);
},
waitForFullRendering: function(thread) {
var lastId = thread.messages[thread.messages.length - 1].id;
this.accessors.findMessage(lastId);
},
downloadMessage: function(messageId) {
this.messageAccessors.download(messageId);
},
get attachments() {
return this.composerAccessors.attachments;
},
get messageText() {
return this.composerAccessors.messageInput.text().trim();
},
get subject() {
return this.composerAccessors.subjectInput.text().trim();
},
back: function() {
this.client.switchToShadowRoot(this.accessors.header);
this.accessors.headerActionButton.tap();
this.client.switchToShadowRoot();
},
callContact: function() {
this.accessors.callButton.tap();
var Dialer = fromApp('dialer').require('lib/dialer');
var dialer = new Dialer(this.client);
dialer.switchTo();
return dialer;
},
openCreateNewContact: function() {
this.accessors.headerTitle.tap();
this.accessors.createNewContactOption.tap();
var contacts = new Contacts(this.client);
contacts.switchToCreateNewContactActivity();
return contacts;
},
openAddToExistingContact: function() {
this.accessors.headerTitle.tap();
this.accessors.addToExistingContactOption.tap();
return this._switchToContactsApp();
},
openParticipants: function() {
this.accessors.headerTitle.tap();
var ParticipantsView = require('../participants/view');
var participantsView = new ParticipantsView(this.client);
participantsView.accessors.waitToAppear();
return participantsView;
},
openReport: function(messageId) {
var message = this.accessors.findMessage(messageId);
// Show context menu on message bubble.
this.client.loader.getActions().longPress(message, 1 /* sec */).perform();
this.menuAccessors.selectAppMenuOption('View message report');
var ReportView = require('../report/view');
var reportView = new ReportView(this.client, this);
reportView.accessors.waitToAppear();
return reportView;
},
deleteMessage: function(messageId) {
var lastMessageWillBeDeleted = this.messages().length === 1;
var message = this.accessors.findMessage(messageId);
// Show context menu on message bubble.
this.client.loader.getActions().longPress(message, 1 /* sec */).perform();
this.menuAccessors.selectAppMenuOption('Delete');
var dialogView = new (require('../dialog/view'))(this.client);
dialogView.chooseAction('Delete');
// If deleted message was the last one, user will be returned back to Inbox.
var currentView;
if (lastMessageWillBeDeleted) {
currentView = new (require('../inbox/view'))(this.client);
currentView.accessors.waitToAppear();
} else {
currentView = this;
}
return currentView;
},
clearMessage: function() {
var messageInput = this.composerAccessors.messageInput;
messageInput.tap();
while (messageInput.text() !== '') {
messageInput.sendKeys(this.KEYS.backspace);
}
},
typeMessage: function(message) {
this.composerAccessors.messageInput.sendKeys(message);
},
addAttachment: function() {
return this.composerAccessors.addAttachment();
},
/**
* Take a screenshot of an attachment thumbnail in the composer.
* @param {Number} The attachment index
* @return {String} The screenshot
*/
takeComposerAttachmentScreenshot: function(index) {
return this.composerAccessors.takeAttachmentScreenshot(index);
},
typeSubject: function(subject) {
this.composerAccessors.subjectInput.sendKeys(subject);
},
showOptions: function() {
this.accessors.optionsButton.tap();
},
showSubject: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Add subject');
this.client.helper.waitForElement(this.composerAccessors.subjectInput);
},
/**
* Launch the "include someone else" action.
* @returns {NewMessageView} the new message view we navigated to.
*/
includeSomeoneElse: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Include someone else');
var NewMessageView = require('../new-message/view');
var newMessageView = new NewMessageView(this.client);
newMessageView.accessors.waitToAppear();
return newMessageView;
},
hideSubject: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Remove subject');
this.client.helper.waitForElementToDisappear(
this.composerAccessors.subjectInput
);
},
enterEditMode: function() {
this.showOptions();
this.menuAccessors.selectAppMenuOption('Select Messages');
},
exitEditMode: function() {
this.back();
},
tapOnMessage: function(messageId) {
var messageNode = this.messageAccessors.find(messageId);
messageNode.scriptWith(function(node) {
node.scrollIntoView();
});
messageNode.tap();
},
toggleMessagesSelection: function() {
var header = this.accessors.header;
// This is a workaround needed as the screen is moving app is
// moving outside the screen on running tapOnMessage().
// This is needed to get the header back in the frame
header.scriptWith(function(node) {
node.scrollIntoView();
});
this.accessors.toggleSelectionButton.tap();
},
isSubjectVisible: function() {
var subjectInput = this.composerAccessors.subjectInput;
return subjectInput && this.composerAccessors.subjectInput.displayed();
},
fakeScrollUpTo: function(scrollTop) {
this.accessors.fakeScrollUpTo(scrollTop);
},
assertMessageInputFocused: function(message) {
Tools.assertElementFocused(this.composerAccessors.messageInput, message);
},
backToInbox: function() {
this.back();
var InboxView = require('../inbox/view');
var inboxView = new InboxView(this.client);
inboxView.accessors.waitToAppear();
return inboxView;
},
_switchToContactsApp: function() {
var contacts = new Contacts(this.client);
contacts.switchToApp();
return contacts;
}
};
module.exports = ConversationView;