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,3 @@
{
"extends": "../../../../test/unit/.jshintrc"
}

View file

@ -0,0 +1,511 @@
/*global MocksHelper, MockL10n, loadBodyHTML, Attachment, AttachmentRenderer,
AssetsHelper,
ImageUtils,
Promise
*/
'use strict';
require('/views/conversation/js/attachment_renderer.js');
require('/views/shared/js/utils.js');
require('/shared/js/image_utils.js');
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/test/unit/mock_attachment.js');
require('/views/shared/test/unit/mock_utils.js');
var MocksHelperForAttachment = new MocksHelper([
'Attachment',
'Utils'
]).init();
suite('AttachmentRenderer >', function() {
MocksHelperForAttachment.attachTestHelpers();
var testImageBlob;
var testImageBlob_small;
var testImageBlob_bogus;
var testAudioBlob;
var testVideoBlob;
var testVcardBlob;
function assertThumbnailPreview(el) {
assert.ok(el.classList.contains('preview'));
assert.isFalse(el.classList.contains('nopreview'));
assert.isNull(el.querySelector('div.placeholder'));
var thumbnail = el.querySelector('div.thumbnail');
assert.ok(thumbnail);
assert.include(thumbnail.style.backgroundImage, 'blob:');
}
function assertThumbnailPlaceholder(el, type) {
assert.ok(el.classList.contains('nopreview'));
assert.isFalse(el.classList.contains('preview'));
assert.isNull(el.querySelector('div.thumbnail'));
assert.isFalse(el.hasAttribute('data-thumbnail'));
var placeholder = el.querySelector('div.thumbnail-placeholder');
assert.ok(placeholder);
assert.ok(placeholder.classList.contains(type + '-placeholder'));
}
suiteSetup(function(done) {
this.realMozL10n = document.l10n;
document.l10n = MockL10n;
// create a bogus image blob (should be rendered with a `corrupted' class)
testImageBlob_bogus = new Blob(['This is an image message'], {
type: 'image/jpeg'
});
var mediaFolder = '/views/shared/test/unit/media';
var blobPromises = [
AssetsHelper.generateImageBlob(300, 300, 'image/jpeg', 0.25).then(
(blob) => testImageBlob_small = blob
),
AssetsHelper.generateImageBlob(1400, 1400, 'image/jpeg', 1).then(
(blob) => testImageBlob = blob
),
AssetsHelper.loadFileBlob(`${mediaFolder}/audio.oga`).then(
(blob) => testAudioBlob = blob
),
AssetsHelper.loadFileBlob(`${mediaFolder}/video.ogv`).then(
(blob) => testVideoBlob = blob
),
AssetsHelper.loadFileBlob(`${mediaFolder}/contacts.vcf`).then(
(blob) => testVcardBlob = blob
)
];
Promise.all(blobPromises).then(() => {
done(() => {
var mmsSizeLimit = 300 * 1024;
assert.isTrue(
testImageBlob_small.size < mmsSizeLimit,
'Image blob should be greater than MMS size limit'
);
assert.isTrue(
testImageBlob.size > mmsSizeLimit,
'Image blob should be greater than MMS size limit'
);
});
}, done);
});
suiteTeardown(function() {
document.l10n = this.realMozL10n;
});
setup(function() {
loadBodyHTML('/index.html');
});
teardown(function() {
document.body.textContent = '';
});
suite('render >', function() {
test('bogus image attachment', function(done) {
var attachment = new Attachment(testImageBlob_bogus, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
// broken image => no thumbnail and `corrupted' class
assertThumbnailPlaceholder(attachmentContainer, 'img');
assert.ok(attachmentContainer.querySelector('div.corrupted'));
}).then(done, done);
});
test('small image attachment (thumbnail)', function(done) {
var attachment = new Attachment(testImageBlob_small, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
// image < message limit => thumbnail blob
assertThumbnailPreview(attachmentContainer);
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
assert.isDefined(attachmentContainer.dataset.thumbnail);
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
}).then(done, done);
});
test('HUGE (fake) image attachment', function(done) {
var attachment = new Attachment({
size: 10 * 1024 * 1024,
type: 'image/jpeg'
}, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
// image > message limit => no thumbnail preview
assertThumbnailPlaceholder(attachmentContainer, 'img');
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
}).then(done, done);
});
test('big image attachment', function(done) {
var attachment = new Attachment(testImageBlob, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'img');
// image < message limit => thumbnail blob
assertThumbnailPreview(attachmentContainer);
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
assert.isDefined(attachmentContainer.dataset.thumbnail);
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
}).then(done, done);
});
test('audio attachment', function(done) {
var attachment = new Attachment(testAudioBlob, {
name: 'Audio attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'audio');
// not an image => no thumbnail preview
assertThumbnailPlaceholder(attachmentContainer, 'audio');
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
}).then(done, done);
});
test('video attachment', function(done) {
var attachment = new Attachment(testVideoBlob, {
name: 'Video attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'video');
// not an image => no thumbnail preview
assertThumbnailPlaceholder(attachmentContainer, 'video');
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
}).then(done, done);
});
test('vcard attachment', function(done) {
var attachment = new Attachment(testVcardBlob, {
name: 'vcard.vcf'
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.ok(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, 'vcard');
// not an image => no thumbnail preview
assertThumbnailPlaceholder(attachmentContainer, 'vcard');
assert.isNull(attachmentContainer.querySelector('div.corrupted'));
}).then(done, done);
});
suite('draft attachments > ', function() {
test('> vcard', function(done) {
var attachment = new Attachment(testVcardBlob, {
name: 'vcard.vcf',
isDraft: true
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer = attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.equal(attachmentContainer.tagName, 'IFRAME');
assert.equal(attachmentContainer.dataset.attachmentType, 'vcard');
var doc = attachmentContainer.contentDocument;
var attachmentNode = doc.querySelector('.attachment');
assert.ok(attachmentNode);
assert.isNull(attachmentNode.querySelector('div.corrupted'));
assert.ok(attachmentNode.querySelector('.vcard-placeholder'));
var fileNameNode = doc.querySelector('.file-name');
assert.ok(fileNameNode);
assert.isNull(fileNameNode.firstElementChild);
assert.equal(fileNameNode.textContent, attachment.name);
}).then(done, done);
document.body.appendChild(attachmentContainer);
});
suite('> image', function() {
function testDraftImage(testName, attachmentName) {
test(testName + ' attachment', function(done) {
var attachment = new Attachment(testImageBlob, {
name: attachmentName,
isDraft: true
});
var attachmentRenderer = AttachmentRenderer.for(attachment),
attachmentContainer =
attachmentRenderer.getAttachmentContainer();
attachmentRenderer.render().then(() => {
assert.equal(attachmentContainer.tagName, 'IFRAME');
var doc = attachmentContainer.contentDocument;
var attachmentNode = doc.querySelector('.attachment');
assert.ok(attachmentNode);
assert.isNull(attachmentNode.querySelector('div.corrupted'));
assert.ok(attachmentNode.querySelector('.thumbnail'));
assert.isDefined(attachmentContainer.dataset.thumbnail);
assert.include(attachmentContainer.dataset.thumbnail, 'blob:');
var fileNameNode = doc.querySelector('.file-name');
assert.ok(fileNameNode);
assert.isNull(fileNameNode.firstElementChild);
assert.equal(fileNameNode.textContent, attachment.name);
}).then(done, done);
document.body.appendChild(attachmentContainer);
});
}
testDraftImage('normal', 'Image attachment');
testDraftImage(
'malicious script',
'%3Cscript%3Ealert(%22I%20am%20dangerous%22)%3C%2Fscript%3E'
);
testDraftImage(
'malicious image',
'%3Cimg%20src%3D%22http%3A%2F%2Fmalicious.server.ru%2Fpingback%22%3E'
);
});
});
});
suite('getThumbnail >', function() {
setup(function() {
this.sinon.stub(ImageUtils, 'getSizeAndType');
this.sinon.spy(ImageUtils.Downsample, 'sizeNoMoreThan');
});
test('Use ImageUtils for no resize case', function(done) {
var attachmentRenderer = AttachmentRenderer.for(new Attachment(
testImageBlob, {
name: 'auto_generated'
}
));
ImageUtils.getSizeAndType.returns(
Promise.resolve({
width: 100,
height: 100
})
);
attachmentRenderer.getThumbnail().then(function(thumbnail) {
assert.include(thumbnail.url, 'blob:');
assert.equal(thumbnail.fragment.toString(), '');
sinon.assert.calledWith(
ImageUtils.Downsample.sizeNoMoreThan,
sinon.match.number
);
}).then(done, done);
});
test('Use ImageUtils for resize needed case', function(done) {
var attachmentRenderer = AttachmentRenderer.for(new Attachment(
testImageBlob, {
name: 'auto_generated'
}
));
ImageUtils.getSizeAndType.returns(
Promise.resolve({
width: 300,
height: 300
})
);
attachmentRenderer.getThumbnail().then(function(thumbnails) {
assert.include(thumbnails.url, 'blob:');
assert.include(thumbnails.fragment.toString(), '#-moz-samplesize=');
sinon.assert.calledWith(
ImageUtils.Downsample.sizeNoMoreThan,
sinon.match.number
);
}).then(done, done);
});
test('throw error if image is corrupted', function(done) {
var attachmentRenderer = AttachmentRenderer.for(
new Attachment(testImageBlob_bogus, {
name: 'auto_generated'
})
);
var imgError = 'image error';
ImageUtils.getSizeAndType.returns(
Promise.reject(imgError)
);
attachmentRenderer.getThumbnail().catch(function(error) {
sinon.assert.calledWith(ImageUtils.getSizeAndType, testImageBlob_bogus);
assert.equal(imgError, error);
}).then(done, done);
});
});
suite('getAttachmentContainer >', function() {
test('uses only one container per attachment renderer', function() {
var attachment = new Attachment(testImageBlob_bogus, {
name: 'Image attachment'
});
var attachmentRenderer1 = AttachmentRenderer.for(attachment),
attachmentRenderer2 = AttachmentRenderer.for(attachment);
var attachmentContainer1 = attachmentRenderer1.getAttachmentContainer(),
attachmentContainer2 = attachmentRenderer2.getAttachmentContainer();
assert.isNotNull(attachmentContainer1);
assert.isNotNull(attachmentContainer2);
assert.notEqual(attachmentContainer1, attachmentContainer2);
var theSameAttachmentContainer1 =
attachmentRenderer1.getAttachmentContainer();
var theSameAttachmentContainer2 =
attachmentRenderer2.getAttachmentContainer();
assert.equal(attachmentContainer1, theSameAttachmentContainer1);
assert.equal(attachmentContainer2, theSameAttachmentContainer2);
});
test('returns correct container for non-draft attachment', function() {
var attachment = new Attachment(testImageBlob_bogus, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment);
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
assert.isNotNull(attachmentContainer);
assert.equal(attachmentContainer.nodeName.toLowerCase(), 'div');
assert.isTrue(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, attachment.type);
});
test('returns correct container for draft attachment', function() {
var attachment = new Attachment(testImageBlob_bogus, {
name: 'Image attachment',
isDraft: true
});
var attachmentRenderer = AttachmentRenderer.for(attachment);
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
assert.isNotNull(attachmentContainer);
assert.equal(attachmentContainer.nodeName.toLowerCase(), 'iframe');
assert.isTrue(
attachmentContainer.classList.contains('attachment-container')
);
assert.equal(attachmentContainer.dataset.attachmentType, attachment.type);
assert.equal(
attachmentContainer.getAttribute('sandbox'),
'allow-same-origin'
);
});
});
suite('updateFileSize >', function() {
test('updates size in the same container', function(done) {
var attachment = new Attachment(testImageBlob, {
name: 'Image attachment'
});
var attachmentRenderer = AttachmentRenderer.for(attachment);
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
var currentFileSize, sizeInfo;
attachmentRenderer.render().then(() => {
sizeInfo = attachmentContainer.querySelector('.size-indicator');
currentFileSize = sizeInfo.dataset.l10nArgs;
attachment.blob = testImageBlob_small;
return attachmentRenderer.updateFileSize();
}).then(() => {
var newFileSize = sizeInfo.dataset.l10nArgs;
assert.notEqual(newFileSize, currentFileSize);
}).then(done, done);
});
test('updates size in iframes too', function(done) {
this.sinon.spy(document.l10n, 'translateFragment');
var attachment = new Attachment(testImageBlob, {
name: 'Image attachment',
isDraft: true
});
var attachmentRenderer = AttachmentRenderer.for(attachment);
var attachmentContainer = attachmentRenderer.getAttachmentContainer();
document.body.appendChild(attachmentContainer);
var currentFileSize, sizeInfo, node;
attachmentRenderer.render().then(() => {
node = attachmentContainer.contentDocument.documentElement;
sizeInfo = node.querySelector('.size-indicator');
currentFileSize = sizeInfo.dataset.l10nArgs;
attachment.blob = testImageBlob_small;
return attachmentRenderer.updateFileSize();
}).then(() => {
var newFileSize = sizeInfo.dataset.l10nArgs;
assert.notEqual(newFileSize, currentFileSize);
sinon.assert.calledWith(document.l10n.translateFragment, node);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,267 @@
/*global MocksHelper, MockL10n, Attachment,
AttachmentRenderer, MimeMapper, MockMozActivity, Promise, Utils,
AssetsHelper
*/
'use strict';
require('/views/conversation/js/attachment.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_attachment_renderer.js');
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/shared/test/unit/mock_moz_activity.js');
require('/views/shared/test/unit/mock_mime_mapper.js');
var MocksHelperForAttachment = new MocksHelper([
'Utils',
'MozActivity',
'MimeMapper',
'AttachmentRenderer'
]).init();
suite('attachment_test.js', function() {
MocksHelperForAttachment.attachTestHelpers();
var testImageBlob;
suiteSetup(function(done) {
// this sometimes takes longer because we fetch 4 assets via XHR
this.realMozL10n = document.l10n;
document.l10n = MockL10n;
AssetsHelper.generateImageBlob(1400, 1400, 'image/jpeg', 1).then((blob) => {
done(() => {
assert.isTrue(
blob.size > 300 * 1024,
'Image blob should be greater than MMS size limit'
);
testImageBlob = blob;
});
}, done);
});
suiteTeardown(function() {
document.l10n = this.realMozL10n;
});
test('Name property defaults to a string value', function() {
var attachment = new Attachment(new Blob());
assert.typeOf(attachment.name, 'string');
});
test('Filename is not a string', function() {
var attachment = new Attachment(testImageBlob, {});
assert.equal(attachment.name, '');
});
test('correct enumerable properties', function() {
var expectedProperties = ['name', 'blob', 'isDraft'];
var attachment = new Attachment(new Blob());
attachment.render();
expectedProperties.forEach((prop) => assert.ok(prop in attachment));
var keys = Object.keys(attachment);
assert.equal(keys.length, expectedProperties.length);
});
suite('render attachment', function() {
var attachmentRendererMock;
var attachmentContainer;
setup(function() {
attachmentContainer = document.createElement('x-container');
this.sinon.stub(AttachmentRenderer.prototype, 'getAttachmentContainer');
AttachmentRenderer.prototype.getAttachmentContainer.returns(
attachmentContainer
);
attachmentRendererMock = new AttachmentRenderer();
this.sinon.stub(AttachmentRenderer, 'for').returns(
attachmentRendererMock
);
});
test('returns attachment container', function() {
var attachment = new Attachment(new Blob(), {
name: 'Image attachment'
});
assert.equal(attachment.render(), attachmentContainer);
});
test('calls ready callback in fail and success cases', function(done) {
var attachmentRendererResult = Promise.resolve();
this.sinon.stub(attachmentRendererMock, 'render', function() {
return attachmentRendererResult;
});
var attachment = new Attachment(new Blob(), {
name: 'Image attachment'
});
var successfulDeferred = Utils.Promise.defer();
attachment.render(function() {
successfulDeferred.resolve();
});
attachmentRendererResult = Promise.reject();
var failedDeferred = Utils.Promise.defer();
attachment.render(function() {
failedDeferred.resolve();
});
successfulDeferred.promise.then(() => failedDeferred.promise).
then(done, done);
});
test('updateFileSize delegates to the renderer', function() {
var firstAttachmentRenderer = new AttachmentRenderer();
AttachmentRenderer.for.onCall(0).returns(
firstAttachmentRenderer
);
this.sinon.spy(AttachmentRenderer.prototype, 'render');
this.sinon.spy(AttachmentRenderer.prototype, 'updateFileSize');
var attachment = new Attachment(new Blob());
attachment.render();
attachment.updateFileSize();
sinon.assert.calledOnce(AttachmentRenderer.for);
sinon.assert.alwaysCalledOn(
firstAttachmentRenderer.render, firstAttachmentRenderer
);
sinon.assert.alwaysCalledOn(
firstAttachmentRenderer.updateFileSize, firstAttachmentRenderer
);
});
});
suite('view attachment with open activity', function() {
setup(function() {
this.sinon.spy(MimeMapper, 'guessTypeFromFileProperties');
this.sinon.spy(MimeMapper, 'ensureFilenameMatchesType');
});
test('Open normal image attachment', function(done) {
var fileName = 'jpeg_image.jpg';
var attachment = new Attachment(testImageBlob, {
name: fileName
});
var typeSpy = MimeMapper.guessTypeFromFileProperties;
var matchSpy = MimeMapper.ensureFilenameMatchesType;
attachment.view().then(() => {
assert.ok(typeSpy.calledWith(fileName, 'image/jpeg'));
assert.ok(matchSpy.calledWith(fileName, typeSpy.returnValues[0]));
assert.equal(MockMozActivity.calls.length, 1);
}).then(done, done);
});
test('Filename has no extension', function(done) {
var fileName = 'jpeg_image';
var attachment = new Attachment(testImageBlob, {
name: fileName
});
var typeSpy = MimeMapper.guessTypeFromFileProperties;
var matchSpy = MimeMapper.ensureFilenameMatchesType;
attachment.view().then(() => {
assert.ok(typeSpy.calledWith(fileName, 'image/jpeg'));
assert.ok(matchSpy.calledWith(fileName, typeSpy.returnValues[0]));
assert.equal(MockMozActivity.calls.length, 1);
}).then(done, done);
});
test('Filename is overridden using single attachment folder',
function(done) {
var attachment1 = new Attachment(testImageBlob, {
name: '/some_path/.hidden_folder/attachment1.jpg'
});
attachment1.view().then(() => {
assert.equal(MockMozActivity.calls.length, 1);
assert.equal(
MockMozActivity.calls[0].data.filename,
'sms-attachments/attachment1.jpg'
);
var attachment2 = new Attachment(testImageBlob, {
name: 'attachment2.jpg'
});
return attachment2.view();
}).then(() => {
assert.equal(MockMozActivity.calls.length, 2);
assert.equal(
MockMozActivity.calls[1].data.filename,
'sms-attachments/attachment2.jpg'
);
}).then(done, done);
});
test('Filename is undefined',
function(done) {
var attachment1 = new Attachment(testImageBlob, {
name: undefined
});
attachment1.view().then(() => {
assert.equal(MockMozActivity.calls.length, 1);
assert.equal(
MockMozActivity.calls[0].data.filename,
'sms-attachments/unnamed-attachment.jpg'
);
}).then(done, done);
});
suite('Activity errors >', function() {
var activity;
setup(function(done) {
this.sinon.spy(window, 'MozActivity');
this.sinon.stub(Utils, 'alert').returns(Promise.resolve());
var attachment = new Attachment(testImageBlob, {
name: 'jpeg_image.jpg'
});
attachment.view().then(() => {
activity = window.MozActivity.firstCall.thisValue;
}).then(done, done);
});
test('No handler for this image', function() {
activity.onerror.call({
error: { name: 'NO_PROVIDER' }
});
sinon.assert.calledWith(Utils.alert, 'attachmentOpenError');
});
test('Activity is canceled', function() {
activity.onerror.call({
error: { name: 'ActivityCanceled' }
});
sinon.assert.notCalled(Utils.alert);
});
test('Activity is canceled (on some other environment)', function() {
activity.onerror.call({
error: { name: 'USER_ABORT' }
});
sinon.assert.notCalled(Utils.alert);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,555 @@
/*global MocksHelper, MockL10n, ConversationView,
loadBodyHTML, Compose, MessageManager, Navigation */
'use strict';
if (typeof GestureDetector === 'undefined') {
require('/shared/js/gesture_detector.js');
}
require('/shared/js/event_dispatcher.js');
require('/shared/test/unit/mocks/mock_gesture_detector.js');
require('/shared/test/unit/mocks/mock_l20n.js');
require('/views/shared/test/unit/mock_contact.js');
require('/services/test/unit/mock_message_manager.js');
require('/views/shared/test/unit/mock_moz_activity.js');
require('/views/shared/test/unit/mock_information.js');
require('/views/shared/test/unit/mock_navigation.js');
require('/views/shared/js/utils.js');
require('/views/shared/js/settings.js');
require('/views/conversation/js/subject_composer.js');
require('/views/conversation/js/compose.js');
require('/views/shared/js/contacts.js');
require('/views/conversation/js/recipients.js');
require('/views/inbox/js/inbox.js');
require('/views/conversation/js/conversation.js');
require('/views/conversation/js/attachment.js');
require('/views/shared/js/contact_renderer.js');
require('/views/shared/js/navigation.js');
require('/services/test/unit/mock_threads.js');
require('/services/test/unit/activity/mock_activity_client.js');
var mHelperIntegration = new MocksHelper([
'MessageManager',
'MozActivity',
'Information',
'Navigation',
'Threads',
'ActivityClient'
]).init();
suite('ConversationView Integration', function() {
var realMozL10n;
var recipients;
var children;
var fixture;
var sendButton;
var input;
mHelperIntegration.attachTestHelpers();
if (typeof loadBodyHTML === 'undefined') {
require('/shared/test/unit/load_body_html_helper.js');
}
suiteSetup(function() {
realMozL10n = document.l10n;
document.l10n = MockL10n;
loadBodyHTML('/index.html');
Navigation.init();
ConversationView.init();
});
suiteTeardown(function() {
document.l10n = realMozL10n;
});
setup(function() {
sendButton = document.getElementById('messages-send-button');
input = document.getElementById('messages-input');
fixture = {
name: 'foo',
number: '999',
email: 'a@b.com',
source: 'none',
// Mapped to node attr, not true boolean
editable: 'true'
};
ConversationView.recipients = null;
ConversationView.initRecipients();
});
teardown(function() {
recipients = null;
fixture = null;
});
suite('Search Contact Events', function() {
setup(function() {
this.sinon.spy(ConversationView, 'searchContact');
});
test('toFieldInput handler, successful', function() {
var fakeEvent = {
target: {
isPlaceholder: true,
textContent: 'abc'
}
};
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
sinon.assert.calledOnce(ConversationView.searchContact);
sinon.assert.calledWith(ConversationView.searchContact, 'abc');
});
test('toFieldInput handler, unsuccessful', function() {
var fakeEvent = {
target: {
isPlaceholder: true,
textContent: ''
}
};
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
sinon.assert.called(ConversationView.searchContact);
sinon.assert.calledWith(ConversationView.searchContact, '');
fakeEvent.target.textContent = 'abd';
fakeEvent.target.isPlaceholder = false;
ConversationView.toFieldInput.call(ConversationView, fakeEvent);
assert.isFalse(ConversationView.searchContact.calledTwice);
});
});
suite('Recipient List Display', function() {
test('Always begins in singleline mode', function() {
// Assert initial state: #messages-recipients-list is singleline
assert.isFalse(
ConversationView.recipientsList.classList.contains('multiline')
);
assert.isTrue(
ConversationView.recipientsList.classList.contains('singleline')
);
// Modify state
ConversationView.recipients.visible('multiline');
// Assert modified state: #messages-recipients-list is multiline
assert.isTrue(
ConversationView.recipientsList.classList.contains('multiline')
);
assert.isFalse(
ConversationView.recipientsList.classList.contains('singleline')
);
// Reset state
ConversationView.initRecipients();
// Assert initial/reset state: #messages-recipients-list is singleline
assert.isFalse(
ConversationView.recipientsList.classList.contains('multiline')
);
assert.isTrue(
ConversationView.recipientsList.classList.contains('singleline')
);
});
test('Typing in list will switch to singline mode', function(done) {
var toField = ConversationView.toField;
var recipientsList = ConversationView.recipientsList;
children = recipientsList.children;
recipients = ConversationView.recipients;
// Fill the recipients list with enough recipients to
// enable the "pan" event
recipients.add({
number: '999999999999999999999999999999'
});
recipients.add({
number: '888888888888888888888888888888'
});
// Since the offsetHeight is lazily captured during any
// first event, trigger an event to ensure that the
// "pan" event will have a height to calculate with.
//
toField.click();
// Simulate list growth
recipientsList.style.height = '100px';
// Listen for the pan event, simulated immediately following.
toField.addEventListener('pan', function panTest() {
done(function() {
assert.isTrue(toField.classList.contains('multiline'));
toField.removeEventListener('pan', panTest, false);
// Once the pan event has occured, the following steps
// will simulate the user actions necessary for testing
// the return to single line mode
// 1. Enter text into the editable "placeholder"
children[2].textContent = '0';
// 2. Trigger a "keyup" event
children[2].dispatchEvent(
new CustomEvent('keyup', {
bubbles: true
})
);
// toField should now be "singleline" again
assert.isTrue(toField.classList.contains('singleline'));
});
});
// Simulate pan event
toField.dispatchEvent(
new CustomEvent('pan', {
detail: {
absolute: {
dy: 1
}
}
})
);
});
test('Clicking in list will switch to singline mode', function(done) {
var toField = ConversationView.toField;
var recipientsList = ConversationView.recipientsList;
children = recipientsList.children;
recipients = ConversationView.recipients;
// Fill the recipients list with enough recipients to
// enable the "multiline" view
recipients.add({
number: '999999999999999999999999999999'
});
recipients.add({
number: '888888888888888888888888888888'
});
// Simulate list growth
recipientsList.style.height = '100px';
// Listen for the pan event, simulated immediately following.
toField.addEventListener('click', function clickTest() {
done(function() {
toField.removeEventListener('click', clickTest, false);
// toField should now be "singleline" again
assert.isTrue(toField.classList.contains('singleline'));
});
});
// Artificially set the list to multiline view
recipients.visible('multiline');
// Click "anywhere"
toField.click();
});
});
suite('Recipient Input Behaviours', function() {
var is = {
corresponding: function(recipient, avatar, value) {
return is.recipient(recipient, value) &&
is.avatar(avatar, value);
},
recipient: function(candidate, value) {
return (candidate.name === value || candidate.number === value);
},
avatar: function(candidate, value) {
return !is.placeholder(candidate) &&
!is.editable(candidate) && candidate.textContent.trim() === value;
},
placeholder: function(candidate, opts) {
opts = opts || { isEmpty: true };
return candidate.isPlaceholder &&
((opts.isEmpty && candidate.textContent.trim() === '') || true);
},
editable: function(candidate) {
return candidate.contentEditable === false;
}
};
setup(function() {
this.sinon.stub(Navigation, 'isCurrentPanel').returns(false);
Navigation.isCurrentPanel.withArgs('composer').returns(true);
this.sinon.stub(MessageManager, 'sendSMS');
});
test('Assimilate stranded recipients (message input)', function() {
ConversationView.recipients.add({
number: '999'
});
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// There are one recipients...
assert.equal(recipients.length, 1);
// And two displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 2);
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
assert.ok(is.placeholder(children[1]));
// Set text in the placeholder, as if the user has typed
// something before jumping to the input field
children[1].textContent = '000';
// Simulate input field focus/entry
input.dispatchEvent(new CustomEvent('focus'));
// There are now two recipients...
assert.equal(recipients.length, 2);
// And three displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 3);
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
assert.ok(is.corresponding(recipients.list[1], children[1], '000'));
assert.ok(is.placeholder(children[2]));
});
test('Assimilate stranded recipients (attachButton)', function() {
ConversationView.recipients.add({
number: '777'
});
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// There are one recipients...
assert.equal(recipients.length, 1);
// And two displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 2);
assert.ok(is.corresponding(recipients.list[0], children[0], '777'));
assert.ok(is.placeholder(children[1]));
// Set text in the placeholder, as if the user has typed
// something before jumping to the input field
children[1].textContent = '555';
// Simulate input field focus/entry
document.getElementById('messages-attach-button').click();
// There are now two recipients...
assert.equal(recipients.length, 2);
// And three displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 3);
assert.ok(is.corresponding(recipients.list[0], children[0], '777'));
assert.ok(is.corresponding(recipients.list[1], children[1], '555'));
assert.ok(is.placeholder(children[2]));
});
test('Assimilate stranded recipients (sendButton)', function() {
// To ensure the onSendClick handler will succeed:
// 1. Add some content to the message
Compose.append('foo');
// 2. Create a recipient
ConversationView.recipients.add({
number: '999'
});
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// There are one recipients...
assert.equal(recipients.length, 1);
// And two displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 2);
assert.ok(is.corresponding(recipients.list[0], children[0], '999'));
assert.ok(is.placeholder(children[1]));
// Set text in the placeholder, as if the user has typed
// something before jumping to the input field
children[1].textContent = '000';
// Simulate sendButton click
ConversationView.onSendClick();
ConversationView.simSelectedCallback(undefined, 0);
// Ensure that the "unaccepted" recipient was assimilated
// and included in the recipients list when message was sent
sinon.assert.calledWithMatch(MessageManager.sendSMS, {
recipients: ['999', '000'],
content: 'foo'
});
});
/* Bug:909641 test fails on ci
test('Assimilate stranded recipients (contactPickButton)', function(done) {
// To ensure the recipient wrapped before picker return:
// 1. Add some content to the message
Compose.append('foo');
// 2. Create a recipient
ConversationView.recipients.add({
number: '111'
});
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// Set text in the placeholder, as if the user has typed
// something before jumping to the input field
children[1].textContent = '222';
// Simulate contact pick
ConversationView.requestContact();
// Simulate the picker activity success
setTimeout(function onsuccess() {
ConversationView.recipients.add({
number: '333'
});
// There are now three recipients after picker activity success
assert.equal(recipients.length, 3);
// And four displayed children,
// (the recipient "avatars" and a
// placeholder for the next entry)
assert.equal(children.length, 4);
assert.ok(is.corresponding(recipients.list[0], children[0], '111'));
assert.ok(is.corresponding(recipients.list[1], children[1], '222'));
assert.ok(is.corresponding(recipients.list[2], children[2], '333'));
assert.ok(is.placeholder(children[3]));
done();
});
});
*/
test('Lone ";" are not recipients', function() {
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// Set ";" in the placeholder, as if the user has typed
children[0].textContent = ';';
// Simulate input field focus/entry
input.click();
// There are no recipients...
assert.equal(recipients.length, 0);
// And one displayed child...
assert.equal(children.length, 1);
// The the ";" has been removed from the
// recipients list
assert.equal(
ConversationView.recipientsList.children[0].textContent, ''
);
});
test('Taps on in-progress recipients do nothing special', function() {
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// Set text in the placeholder, as if the user has typed
children[0].textContent = '9999999999';
// Simulate a tap on the placeholder
children[0].click();
// There should be no recipients created
assert.equal(recipients.length, 0);
// And only the placeholder is displayed
assert.equal(children.length, 1);
assert.isTrue(children[0].isPlaceholder);
});
/**
* The code in this test works exactly as it should
* when run as "standalone".
*
* The test runner appears to cause a loss of
* node references that are expected to exist
test('<delete> removes "known" contact to the left', function() {
function backspace(target) {
var doc = target.ownerDocument;
var view = doc.defaultView;
var event = doc.createEvent('KeyboardEvent');
event.initKeyEvent(
'keypress', true, true, view,
false, false, false, false,
KeyEvent.DOM_VK_BACK_SPACE, 0
);
target.dispatchEvent(event);
return event;
}
ConversationView.recipients = new Recipients({
outer: 'messages-to-field',
inner: 'messages-recipients-list',
template: new Template('messages-recipient-tmpl')
});
ConversationView.recipients.add({
source: 'contacts',
name: 'Rick',
number: '99999'
});
children = ConversationView.recipientsList.children;
recipients = ConversationView.recipients;
// Simulate backspace on the current placeholder
backspace(children[1]);
// There are no recipients...
assert.equal(recipients.length, 0);
// And one displayed placeholder
assert.equal(children.length, 1);
});
*/
});
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,154 @@
/*global MocksHelper, MockL10n, LinkActionHandler, ActivityPicker,
ConversationView,
Contacts
*/
'use strict';
require('/shared/test/unit/mocks/mock_l20n.js');
require('/shared/test/unit/mocks/mock_option_menu.js');
require('/views/conversation/js/link_action_handler.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_activity_picker.js');
require('/views/shared/test/unit/mock_contacts.js');
require('/views/shared/test/unit/mock_moz_activity.js');
require('/views/shared/test/unit/mock_conversation.js');
require('/views/shared/test/unit/mock_utils.js');
var mocksHelperLAH = new MocksHelper([
'ActivityPicker',
'Contacts',
'MozActivity',
'OptionMenu',
'ConversationView',
'Utils'
]).init();
suite('LinkActionHandler', function() {
var realMozL10n, events;
suiteSetup(function() {
realMozL10n = document.l10n;
document.l10n = MockL10n;
mocksHelperLAH.suiteSetup();
events = {
phone: {
target: {
dataset: {
action: 'dial-link',
dial: '999'
}
}
},
email: {
target: {
dataset: {
action: 'email-link',
email: 'a@b.com'
}
}
},
url: {
target: {
dataset: {
action: 'url-link',
url: 'http://mozilla.com'
}
}
}
};
});
suiteTeardown(function() {
document.l10n = realMozL10n;
mocksHelperLAH.suiteTeardown();
});
setup(function() {
Object.keys(events).forEach(function(type) {
events[type].preventDefault = this.sinon.spy();
events[type].stopPropagation = this.sinon.spy();
}, this);
mocksHelperLAH.setup();
});
teardown(function() {
mocksHelperLAH.teardown();
});
suite('onClick', function() {
setup(function() {
mocksHelperLAH.setup();
});
teardown(function() {
mocksHelperLAH.teardown();
});
test('dial-link: (known) delegates to promptContact ', function() {
this.sinon.stub(ConversationView, 'promptContact');
this.sinon.stub(Contacts, 'findByPhoneNumber')
.callsArgWith(1, [{
name: ['Huey'],
tel: {
value: '999'
}
}]);
LinkActionHandler.onClick(events.phone);
assert.deepEqual(ConversationView.promptContact.args[0][0], {
number: '999',
inMessage: true
});
assert.ok(events.phone.preventDefault.called);
assert.ok(events.phone.stopPropagation.called);
});
test('dial-link: (unknown) delegates to promptContact ', function() {
this.sinon.stub(ConversationView, 'promptContact');
this.sinon.stub(Contacts, 'findByPhoneNumber')
.callsArgWith(1, []);
LinkActionHandler.onClick(events.phone);
assert.deepEqual(ConversationView.promptContact.args[0][0], {
number: '999',
inMessage: true
});
assert.ok(events.phone.preventDefault.called);
assert.ok(events.phone.stopPropagation.called);
});
test('email-link: delegates to prompt ', function() {
this.sinon.stub(ConversationView, 'prompt');
LinkActionHandler.onClick(events.email);
assert.ok(ConversationView.prompt.called);
assert.deepEqual(ConversationView.prompt.args[0][0], {
email: 'a@b.com',
inMessage: true
});
});
test('url-link: go directly to the action ', function() {
this.sinon.stub(ActivityPicker, 'url');
LinkActionHandler.onClick(events.url);
assert.isTrue(ActivityPicker.url.called);
assert.ok(events.url.preventDefault.called);
assert.ok(events.url.stopPropagation.called);
});
});
});

View file

@ -0,0 +1,377 @@
/*global LinkHelper, FixturePhones */
/*
Link Helper Tests
*/
'use strict';
require('/views/conversation/js/link_helper.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
suite('link_helper_test.js', function() {
suite('LinkHelper URL replacements', function() {
function url2msg(url, addprefix) {
var link = url;
if (addprefix) {
url = 'http://' + url;
}
return '<a data-url="' + url + '"' +
' data-action="url-link" >' + link + '</a>';
}
function testURLMatch(url, match, addprefix) {
var expected = url.replace(match, url2msg(match, addprefix));
var result = LinkHelper.searchAndLinkClickableData(url);
assert.equal(result, expected);
}
function testURLOK(url, addprefix) {
var expected = url2msg(url, addprefix);
var result = LinkHelper.searchAndLinkClickableData(url);
assert.equal(result, expected);
}
function testURLNOK(url) {
// we only test url here to make sure it doesn't match
var result = LinkHelper.searchAndLinkUrl(url);
assert.equal(result, url);
}
suite('Matches', function() {
test('Simple URL', function() {
testURLOK('http://www.mozilla.org/');
});
test('Simple URL .co.uk', function() {
testURLOK('http://www.mozilla.co.uk/');
});
test('Simple short URL', function() {
testURLOK('mzl.la', true);
});
test('Simple short URL with parens', function() {
testURLMatch('(mzl.la)', 'mzl.la', true);
});
test('Simple short URL with trailing dot', function() {
testURLMatch('mzl.la.', 'mzl.la', true);
});
test('Simple short URL with trailing dot and port', function() {
testURLMatch('mzl.la.:527', 'mzl.la', true);
});
test('Simple short URL with value', function() {
testURLOK('mzl.la/ac', true);
});
test('Simple URL https', function() {
testURLOK('https://www.mozilla.org');
});
test('Simple URL https without www.', function() {
testURLOK('https://mozilla.org');
});
test('Simple URL https with trailing dot on domain.', function() {
testURLOK('https://mozilla.org./blah');
});
test('Simple URL http with port', function() {
testURLOK('http://www.mozilla.org:8080/');
});
test('Simple URL http without www. and with port', function() {
testURLOK('http://mozilla.org:8080/');
});
test('Simple URL with IPv4', function() {
testURLOK('http://8.8.8.8/');
});
test('Simple URL with IPv4 with zero', function() {
testURLOK('http://8.0.8.8/');
});
test('Simple URL rtsp', function() {
testURLOK('rtsp://www.mozilla.org/video.mov');
});
test('<br>prefix', function() {
testURLMatch('<br>mozilla.org', 'mozilla.org', true);
});
test('Trailing question mark', function() {
testURLMatch('OMG mozilla.org? it is cool!', 'mozilla.org', true);
});
test('Trailing colon', function() {
testURLMatch('OMG mozilla.org: it is cool!', 'mozilla.org', true);
});
test('Trailing semi-colon', function() {
testURLMatch('OMG mozilla.org; it is cool!', 'mozilla.org', true);
});
test('Trailing bang', function() {
testURLMatch('OMG mozilla.org! it is cool!', 'mozilla.org', true);
});
test('Trailing period', function() {
testURLMatch('Check out mozilla.org.', 'mozilla.org', true);
});
test('Trailing comma', function() {
testURLMatch('Check out mozilla.org, or not', 'mozilla.org', true);
});
test('Trailing parens', function() {
testURLMatch('(Check out mozilla.org)', 'mozilla.org', true);
});
test('Trailing question mark on path', function() {
testURLMatch('OMG mzl.la/huh? it is cool!', 'mzl.la/huh', true);
});
test('Trailing colon on path', function() {
testURLMatch('OMG mzl.la/huh: it is cool!', 'mzl.la/huh', true);
});
test('Trailing semi-colon on path', function() {
testURLMatch('OMG mzl.la/huh; it is cool!', 'mzl.la/huh', true);
});
test('Trailing bang on path', function() {
testURLMatch('OMG mzl.la/huh! it is cool!', 'mzl.la/huh', true);
});
test('Trailing parens on path', function() {
testURLMatch('(Check out mzl.la/ac)', 'mzl.la/ac', true);
});
test('Trailing period on path', function() {
testURLMatch('Check out mzl.la/ac.', 'mzl.la/ac', true);
});
test('Trailing comma on path', function() {
testURLMatch('Check out mzl.la/ac, or not', 'mzl.la/ac', true);
});
test('wiki link that has paren', function() {
testURLOK('http://en.wikipedia.org/wiki/Arete_(disambiguation)');
});
test('Crazy URL from #887146', function() {
testURLOK('http://sani.tiz.ed.com/wap/videos/files_mm/' +
'3GPP%20MPEG4%20-%20176×144_7fps_15kpbs_AACmono_8KHz_5kbps.3gp');
});
test('One letter second-level', function() {
testURLOK('http://x.com');
});
test('URL with phone number in the middle', function() {
testURLOK('http://somesite.com/q,12288296666/');
});
test('Full width char prefix', function() {
testURLMatch(':mozilla.org', 'mozilla.org', true);
});
test('non ascii prefix', function() {
testURLMatch('àmozilla.org', 'mozilla.org', true);
});
test('Bug 941763: Add dot as URL prefix', function() {
testURLMatch('.website.com', 'website.com', true);
testURLMatch('https://.website.com', 'website.com', true);
testURLMatch('htt://www.mozilla.org', 'mozilla.org', true);
});
});
suite('Failures', function() {
test('Simple invalid URL with slashes', function() {
testURLNOK('http://www.a/b/d.com');
});
test('Simple invalid TLD URL', function() {
testURLNOK('http://www.mozilla.o');
});
test('Simple ellipse', function() {
testURLNOK('aaa...ccc');
});
test('Simple URL with username', function() {
testURLNOK('http://user@mozilla.org');
});
test('Simple URL with credentials', function() {
testURLNOK('http://user:pass@mozilla.org');
});
test('Simple URL with IPv6', function() {
testURLNOK('http://[::1]/');
});
test('Bug 890342 1', function() {
testURLNOK('2.74');
});
test('Bug 890342 2', function() {
testURLNOK('21.72');
});
test('Bug 923739', function() {
testURLNOK('foo.zz');
testURLNOK('foo.foo');
testURLNOK('foo.quux');
testURLOK('http://whatever.stuff');
testURLOK('https://whatever.stuff');
});
});
});
suite('LinkHelper Email replacements', function() {
function email2msg(email) {
return '<a data-email="' + email + '"' +
' data-action="email-link">' + email + '</a>';
}
function testEmailOK(email) {
var exp = email2msg(email);
var res = LinkHelper.searchAndLinkClickableData(email);
assert.equal(res, exp);
}
function testEmailNOK(email) {
var res = LinkHelper.searchAndLinkEmail(email);
assert.equal(res, email);
}
test('Simple email', function() {
testEmailOK('user@hostname.tld');
});
test('Simple email with alias', function() {
testEmailOK('user+alias@hostname.tld');
});
test('Simple email with subdomain', function() {
testEmailOK('user@cs.subdhostname.tld');
});
test('Invalid email no hostname', function() {
testEmailNOK('user@');
});
test('Invalid email no user', function() {
testEmailNOK('@hostname.tld');
});
});
suite('LinkHelper Phone replacements', function() {
function testPhoneMatch(text, match) {
var expected = text.replace(match, phone2msg(match));
var result = LinkHelper.searchAndLinkClickableData(text);
assert.equal(result, expected);
}
function phone2msg(phone) {
return '<a dir="ltr" data-dial="' + phone + '"' +
' data-action="dial-link">' + phone + '</a>';
}
function testPhoneOK(phone) {
var exp = phone2msg(phone);
var res = LinkHelper.searchAndLinkClickableData(phone);
assert.equal(res, exp);
}
function testPhoneNOK(phone) {
var res = LinkHelper.searchAndLinkPhone(phone);
assert.equal(res, phone);
}
suite('Matches', function() {
test('Simple french mobile nat.', function() {
testPhoneOK('0612345678');
});
test('Simple french mobile intl', function() {
testPhoneOK('+33612345678');
});
test('Phone from #887737', function() {
testPhoneOK('+5511 98907-6047');
});
test('word before trailing number', function() {
testPhoneMatch('Test1 600123123', '600123123');
});
test('word after preceding number', function() {
testPhoneMatch('600123123 1Test', '600123123');
});
[
// Bug 900413 - detect 5 digit short codes
'24242',
'10086',
'+1234',
// bug 941976 - 7 digit 4 digit with symbols should still work
'123-1234 1234'
].forEach(function(phone) {
test(phone, testPhoneOK.bind(null, phone));
});
});
suite('Failures', function() {
[
'0.34',
'1200',
'5000',
'0.122',
'0921',
// Bug 900413 - 5 digit codes with separators not allowed
'1-10-13',
'12-3-13',
'1-800-A',
'1(234)5',
'1234..5',
'1 2 3 45',
'1\t23\t45',
'12+34'
].forEach(function(phone) {
test(phone, testPhoneNOK.bind(null, phone));
});
});
suite('Varied cases', function() {
FixturePhones.forEach(function(fixture) {
if (fixture.isTestable) {
suite(fixture.title, function() {
fixture.values.forEach(function(value) {
test(value, function() {
testPhoneOK(value);
});
});
});
}
});
});
suite('Tricky Problems', function() {
test('Two 9 digit numbers separated by space (#892480)', function() {
var test = '123456789 987654321';
var expected = test.split(' ').map(phone2msg).join(' ');
var result = LinkHelper.searchAndLinkClickableData(test);
assert.equal(result, expected);
});
test('Two 6 digit numbers separated by newline (#892480)', function() {
var test = '222333\n333222';
var expected = test.split('\n').map(phone2msg).join('\n');
var result = LinkHelper.searchAndLinkClickableData(test);
assert.equal(result, expected);
});
test('7 digit and 9 digit (#941976)', function() {
var test = '1234567 987654321';
var expected = test.split(' ').map(phone2msg).join(' ');
var result = LinkHelper.searchAndLinkClickableData(test);
assert.equal(result, expected);
});
test('7 digit and 5 digit (#941976)', function() {
var test = '1234567 12345';
var expected = test.split(' ').map(phone2msg).join(' ');
var result = LinkHelper.searchAndLinkClickableData(test);
assert.equal(result, expected);
});
test('7 digit and 5 digit(with plus) (#941976)', function() {
var test = '1234567 +1234';
var expected = test.split(' ').map(phone2msg).join(' ');
var result = LinkHelper.searchAndLinkClickableData(test);
assert.equal(result, expected);
});
test('7 digit and 4 digit (#941976)', function() {
testPhoneMatch('1234567 1234', '1234567');
});
});
});
suite('Multiple in the same string', function() {
test('Hits every case', function() {
var test = 'Hey, check out http://stackoverflow.com/q/12882966/ and ' +
'call me at +18155551212 or (e-mail user@hostname.tld)';
var expected = 'Hey, check out ' +
'<a data-url="http://stackoverflow.com/q/12882966/" ' +
'data-action="url-link" >http://stackoverflow.com/q/12882966/</a>' +
' and call me at ' +
'<a dir="ltr" data-dial="+18155551212" data-action="dial-link">' +
'+18155551212</a> or (e-mail <a data-email="user@hostname.tld"' +
' data-action="email-link">user@hostname.tld</a>)';
assert.equal(LinkHelper.searchAndLinkClickableData(test), expected);
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,154 @@
/*global App,
ConversationView,
Information,
InterInstanceEventDispatcher,
LazyLoader,
LocalizationHelper,
MessageManager,
MessagingClient,
MozMobileConnectionsClient,
MocksHelper,
Navigation,
Settings,
Startup,
TimeHeaders,
Utils
*/
'use strict';
require('/views/shared/test/unit/mock_time_headers.js');
require('/views/shared/test/unit/mock_navigation.js');
require('/views/shared/test/unit/mock_settings.js');
require('/views/shared/test/unit/mock_inter_instance_event_dispatcher.js');
require('/views/shared/test/unit/mock_conversation.js');
require('/views/shared/test/unit/mock_information.js');
require('/views/shared/test/unit/mock_localization_helper.js');
require('/views/shared/test/unit/mock_app.js');
require('/shared/test/unit/mocks/mock_lazy_loader.js');
require('/services/test/unit/messaging/mock_messaging_client.js');
require('/services/test/unit/moz_mobile_connections/' +
'mock_moz_mobile_connections_client.js');
require('/services/test/unit/mock_message_manager.js');
require('/views/shared/js/utils.js');
require('/views/shared/test/unit/mock_utils.js');
require('/views/conversation/js/startup.js');
var MocksHelperForInboxStartup = new MocksHelper([
'App',
'ConversationView',
'Information',
'InterInstanceEventDispatcher',
'LazyLoader',
'LocalizationHelper',
'MessageManager',
'MessagingClient',
'MozMobileConnectionsClient',
'Navigation',
'TimeHeaders',
'Settings',
'Utils'
]).init();
suite('ConversationView Startup,', function() {
MocksHelperForInboxStartup.attachTestHelpers();
setup(function() {
this.sinon.spy(MessageManager, 'init');
this.sinon.spy(Navigation, 'init');
this.sinon.spy(ConversationView, 'init');
this.sinon.spy(LocalizationHelper, 'init');
this.sinon.spy(Information, 'initDefaultViews');
this.sinon.spy(TimeHeaders, 'init');
this.sinon.spy(Settings, 'init');
this.sinon.spy(Navigation, 'setReady');
this.sinon.spy(InterInstanceEventDispatcher, 'connect');
this.sinon.stub(ConversationView, 'once');
this.sinon.stub(LazyLoader, 'load').returns(Promise.resolve());
this.sinon.stub(MessageManager, 'getThreads');
this.sinon.stub(Navigation, 'isDefaultPanel');
this.sinon.spy(MessagingClient, 'init');
this.sinon.spy(MozMobileConnectionsClient, 'init');
this.sinon.spy(Utils, 'initializeShimHost');
});
suite('In default panel,', function() {
setup(function() {
Navigation.isDefaultPanel.returns(true);
Startup.init();
});
test('correctly initializes dependencies', function(done) {
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
sinon.assert.calledOnce(MessageManager.init);
sinon.assert.calledOnce(ConversationView.init);
// Workaround to deal with temporary shim code.
MessageManager.getThreads.lastCall.args[0].done();
Promise.resolve().then(
() => sinon.assert.calledOnce(Navigation.init)
).then(done, done);
});
test('starts non-critical initializations only once view is visually ready',
function() {
// Lazy loading is not started yet and headers are not processed either.
sinon.assert.notCalled(LazyLoader.load);
ConversationView.once.withArgs('visually-loaded').yield();
sinon.assert.calledOnce(LazyLoader.load);
});
test('correctly initializes lazy dependencies', function(done) {
ConversationView.once.withArgs('visually-loaded').yield();
LazyLoader.load.lastCall.returnValue.then(() => {
sinon.assert.calledOnce(LocalizationHelper.init);
sinon.assert.calledOnce(TimeHeaders.init);
sinon.assert.calledOnce(Settings.init);
sinon.assert.calledOnce(Navigation.setReady);
sinon.assert.calledOnce(Information.initDefaultViews);
sinon.assert.calledOnce(InterInstanceEventDispatcher.connect);
}).then(done, done);
});
});
suite('In a subpanel,', function() {
setup(function() {
Navigation.isDefaultPanel.returns(false);
Startup.init();
});
test('correctly initializes dependencies', function(done) {
sinon.assert.calledWith(Utils.initializeShimHost, App.instanceId);
sinon.assert.calledOnce(MessageManager.init);
sinon.assert.calledOnce(ConversationView.init);
// Workaround to deal with temporary shim code.
MessageManager.getThreads.lastCall.args[0].done();
Promise.resolve().then(
() => sinon.assert.calledOnce(Navigation.init)
).then(done, done);
});
test('starts non-critical initializations right away', function() {
sinon.assert.notCalled(ConversationView.once);
sinon.assert.calledOnce(LazyLoader.load);
});
test('correctly initializes lazy dependencies', function(done) {
LazyLoader.load.lastCall.returnValue.then(() => {
sinon.assert.calledOnce(LocalizationHelper.init);
sinon.assert.calledOnce(TimeHeaders.init);
sinon.assert.calledOnce(Settings.init);
sinon.assert.calledOnce(Navigation.setReady);
sinon.assert.calledOnce(Information.initDefaultViews);
sinon.assert.calledOnce(InterInstanceEventDispatcher.connect);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,367 @@
/*global loadBodyHTML,
FocusEvent,
InputEvent,
KeyEvent,
KeyboardEvent,
SubjectComposer
*/
'use strict';
require('/shared/js/event_dispatcher.js');
require('/views/conversation/js/subject_composer.js');
suite('SubjectComposer >', function() {
var node, subjectComposer, input;
setup(function() {
loadBodyHTML('/index.html');
node = document.querySelector('.js-subject-composer');
subjectComposer = new SubjectComposer(node);
input = node.querySelector('.subject-composer-input');
});
teardown(function() {
subjectComposer.reset();
});
test('correctly instantiate', function() {
assert.throws(
() => new SubjectComposer(),
'Subject node is required'
);
assert.ok(new SubjectComposer(node));
assert.isTrue(
node.classList.contains('hide'),
'Should be hidden by default'
);
});
suite('visibility >', function() {
teardown(function() {
subjectComposer.reset();
});
test('show and hide work correctly', function() {
var visibilityChangeStub = sinon.stub();
subjectComposer.on('visibility-change', visibilityChangeStub);
subjectComposer.show();
assert.isFalse(node.classList.contains('hide'));
sinon.assert.called(visibilityChangeStub);
subjectComposer.hide();
assert.isTrue(node.classList.contains('hide'));
sinon.assert.called(visibilityChangeStub);
});
test('keeps value between show and hide', function() {
subjectComposer.show();
subjectComposer.setValue('my value');
assert.equal(subjectComposer.getValue(), 'my value');
assert.equal(input.textContent, 'my value');
// Hide subject and show again
subjectComposer.hide();
subjectComposer.show();
assert.equal(subjectComposer.getValue(), 'my value');
assert.equal(input.textContent, 'my value');
});
test('isVisible tracks visibility correctly', function() {
assert.isFalse(subjectComposer.isVisible());
subjectComposer.show();
assert.isTrue(subjectComposer.isVisible());
subjectComposer.hide();
assert.isFalse(subjectComposer.isVisible());
});
});
suite('value management >', function() {
setup(function() {
subjectComposer.show();
});
teardown(function() {
subjectComposer.reset();
});
test('correctly sets and gets value', function() {
assert.throws(
() => subjectComposer.setValue(), 'Value should be a valid string'
);
assert.equal(subjectComposer.getValue(), '');
subjectComposer.setValue('test');
assert.equal(subjectComposer.getValue(), 'test');
assert.equal(input.textContent, 'test');
input.innerHTML = 'test#2<br><br>';
input.dispatchEvent(new InputEvent('input'));
assert.equal(subjectComposer.getValue(), 'test#2');
assert.equal(input.textContent, 'test#2');
subjectComposer.setValue('test#3');
assert.equal(subjectComposer.getValue(), 'test#3');
assert.equal(input.textContent, 'test#3');
input.innerHTML = '<br>test#4<br><br>';
input.dispatchEvent(new InputEvent('input'));
assert.equal(subjectComposer.getValue(), 'test#4');
assert.equal(input.textContent, 'test#4');
});
test('correctly pre-process value', function() {
var content = 'Line\u00A01<br>\n Line 2<br><br><br><br>\nLine\n 3<br>';
subjectComposer.setValue(content);
assert.equal(
subjectComposer.getValue(),
'Line 1<br> Line 2<br><br><br><br> Line 3<br>'
);
subjectComposer.setValue('');
input.innerHTML = content;
input.dispatchEvent(new InputEvent('input'));
assert.equal(subjectComposer.getValue(), 'Line 1 Line 2 Line 3');
subjectComposer.setValue('');
input.textContent = content;
input.dispatchEvent(new InputEvent('input'));
assert.equal(
subjectComposer.getValue(),
'Line 1<br> Line 2<br><br><br><br> Line 3<br>'
);
});
test('fires "change" only if value actually changes via setValue',
function() {
var onChange = sinon.stub();
subjectComposer.on('change', onChange);
subjectComposer.setValue('test');
sinon.assert.calledOnce(onChange);
subjectComposer.setValue('test');
sinon.assert.calledOnce(onChange);
subjectComposer.setValue('test#2');
sinon.assert.calledTwice(onChange);
});
test('fires "change" only if value actually changes via innerHTML',
function() {
var onChange = sinon.stub();
subjectComposer.on('change', onChange);
input.innerHTML = 'test';
input.dispatchEvent(new InputEvent('input'));
sinon.assert.calledOnce(onChange);
input.innerHTML = 'test';
input.dispatchEvent(new InputEvent('input'));
sinon.assert.calledOnce(onChange);
input.innerHTML = 'test#2';
input.dispatchEvent(new InputEvent('input'));
sinon.assert.calledTwice(onChange);
});
test('correctly manages placeholder', function() {
var placeholder = node.querySelector(
'.subject-composer-placeholder'
);
assert.isNotNull(placeholder, 'Placeholder should be presented');
assert.equal(
placeholder.getAttribute('data-l10n-id'),
'messagesSubjectInput_placeholder'
);
subjectComposer.setValue('a');
assert.isNull(
node.querySelector('.subject-composer-placeholder'),
'Placeholder should be removed'
);
input.textContent = '';
input.dispatchEvent(new InputEvent('input'));
assert.isNotNull(
node.querySelector('.subject-composer-placeholder'),
'Placeholder should be presented'
);
});
});
suite('Backspace key handling >', function() {
var downEvent, upEvent, backspace;
setup(function() {
downEvent = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
keyCode: KeyEvent.DOM_VK_BACK_SPACE
});
upEvent = new KeyboardEvent('keyup', {
bubbles: true,
cancelable: true,
keyCode: KeyEvent.DOM_VK_BACK_SPACE
});
backspace = function() {
input.dispatchEvent(downEvent);
input.dispatchEvent(upEvent);
};
subjectComposer.show();
subjectComposer.setValue('Howdy!');
});
teardown(function() {
subjectComposer.reset();
});
test('<delete> in empty subject hides field', function() {
// 1. Assert the correct state condition updates have occurred,
// as described in step 1
assert.isTrue(subjectComposer.isVisible());
// 2. To simulate the user "deleting" the subject,
// set the value to an empty string.
input.textContent = '';
input.dispatchEvent(new InputEvent('input'));
// 3. Simulate backspace on the subject field
backspace();
// 4. Confirm that the state of the compose
// area has updated properly.
assert.isFalse(subjectComposer.isVisible());
});
test('<delete> in non-empty subject does not hide field', function() {
// 1. Assert the correct state condition updates have occurred,
// as described in step 1
assert.isTrue(subjectComposer.isVisible());
// 2. Simulate backspace on the subject field
backspace();
// 3. Confirm that the state of the compose area not changed.
assert.isTrue(subjectComposer.isVisible());
});
test('<delete> holding subject does not hide field', function() {
// 1. Assert the correct state condition updates have occurred,
// as described in step 1
assert.isTrue(subjectComposer.isVisible());
// 2. Simulate holding backspace on the subject field
for (var i = 0; i < 5; i++) {
input.dispatchEvent(downEvent);
}
// 3. This is the "release" from a holding state
input.dispatchEvent(upEvent);
// 4. Confirm that the state of the compose area not changed.
assert.isTrue(subjectComposer.isVisible());
});
test('<delete> holding subject, release and tap hides field', function() {
// 1. Assert the correct state condition updates have occurred,
// as described in step 1
assert.isTrue(subjectComposer.isVisible());
// 2. Simulate holding backspace on the subject field
for (var i = 0; i < 5; i++) {
input.dispatchEvent(downEvent);
}
// 3. This is the "release" from a holding state
input.dispatchEvent(upEvent);
// 4. To simulate the user "deleting" the subject,
// set the value to an empty string.
input.textContent = '';
input.dispatchEvent(new InputEvent('input'));
// 5. Simulate backspace on the subject field.
backspace();
// 6. Confirm that the state of the compose area not changed.
assert.isFalse(subjectComposer.isVisible());
});
});
suite('Return key handling >', function() {
test('Return key should be ignored at key down', function() {
subjectComposer.show();
subjectComposer.setValue('Howdy!');
var event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
keyCode: KeyEvent.DOM_VK_RETURN
});
this.sinon.spy(event, 'stopPropagation');
input.dispatchEvent(event);
assert.ok(event.defaultPrevented);
sinon.assert.called(event.stopPropagation);
});
});
test('correctly resets subject', function() {
var onChangeStub = sinon.stub();
subjectComposer.show();
assert.equal(subjectComposer.getValue(), '');
subjectComposer.setValue('test');
assert.equal(subjectComposer.getValue(), 'test');
assert.equal(input.textContent, 'test');
subjectComposer.on('visibility-change', onChangeStub);
subjectComposer.on('change', onChangeStub);
subjectComposer.reset();
assert.equal(subjectComposer.getValue(), '');
assert.equal(input.textContent, '');
assert.isFalse(subjectComposer.isVisible());
// Visibility change and change events should not be fired on reset
sinon.assert.notCalled(onChangeStub);
});
test('fires "focus" event', function() {
subjectComposer.show();
var onFocusStub = sinon.stub();
subjectComposer.on('focus', onFocusStub);
input.dispatchEvent(new FocusEvent('focus'));
sinon.assert.calledOnce(onFocusStub);
});
});