/** * ownCloud * * @author Vincent Petry * @copyright 2016 Vincent Petry * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE * License as published by the Free Software Foundation; either * comment 3 of the License, or any later comment. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU AFFERO GENERAL PUBLIC LICENSE for more details. * * You should have received a copy of the GNU Affero General Public * License along with this library. If not, see . * */ describe('OCA.Comments.CommentsTabView tests', function() { var view, fileInfoModel; var fetchStub; var avatarStub; var testComments; var clock; /** * Creates a dummy message with the given length * * @param {int} len length * @return {string} message */ function createMessageWithLength(len) { var bigMessage = ''; for (var i = 0; i < len; i++) { bigMessage += 'a'; } return bigMessage; } beforeEach(function() { clock = sinon.useFakeTimers(Date.UTC(2016, 1, 3, 10, 5, 9)); fetchStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'fetchNext'); avatarStub = sinon.stub($.fn, 'avatar'); view = new OCA.Comments.CommentsTabView(); fileInfoModel = new OCA.Files.FileInfoModel({ id: 5, name: 'One.txt', mimetype: 'text/plain', permissions: 31, path: '/subdir', size: 123456789, etag: 'abcdefg', mtime: Date.UTC(2016, 1, 0, 0, 0, 0) }); view.render(); var comment1 = new OCA.Comments.CommentModel({ id: 1, actorType: 'users', actorId: 'user1', actorDisplayName: 'User One', objectType: 'files', objectId: 5, message: 'First', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 0)).toUTCString() }); var comment2 = new OCA.Comments.CommentModel({ id: 2, actorType: 'users', actorId: 'user2', actorDisplayName: 'User Two', objectType: 'files', objectId: 5, message: 'Second\nNewline', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 0, 0)).toUTCString() }); var comment3 = new OCA.Comments.CommentModel({ id: 3, actorId: 'anotheruser', actorDisplayName: 'Another User', actorType: 'users', verb: 'comment', message: 'Hail to thee, @macbeth. Yours faithfully, @banquo', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(), mentions: { 0: { mentionDisplayName: "Thane of Cawdor", mentionId: "macbeth", mentionTye: "user" }, 1: { mentionDisplayName: "Lord Banquo", mentionId: "banquo", mentionTye: "user" } } }); testComments = [comment1, comment2, comment3]; }); afterEach(function() { view.remove(); view = undefined; fetchStub.restore(); avatarStub.restore(); clock.restore(); }); describe('rendering', function() { it('reloads matching comments when setting file info model', function() { view.setFileInfo(fileInfoModel); expect(fetchStub.calledOnce).toEqual(true); }); it('renders loading icon while fetching comments', function() { view.setFileInfo(fileInfoModel); view.collection.trigger('request'); expect(view.$el.find('.loading').length).toEqual(1); expect(view.$el.find('.comments li').length).toEqual(0); }); it('renders comments', function() { view.setFileInfo(fileInfoModel); view.collection.set(testComments); var $comments = view.$el.find('.comments>li'); expect($comments.length).toEqual(3); var $item = $comments.eq(0); expect($item.find('.author').text()).toEqual('User One'); expect($item.find('.date').text()).toEqual('seconds ago'); expect($item.find('.message').text()).toEqual('First'); $item = $comments.eq(1); expect($item.find('.author').text()).toEqual('User Two'); expect($item.find('.date').text()).toEqual('5 minutes ago'); expect($item.find('.message').html()).toEqual('Second
Newline'); }); it('renders comments from deleted user differently', function() { testComments[0].set('actorType', 'deleted_users', {silent: true}); view.collection.set(testComments); var $item = view.$el.find('.comment[data-id=1]'); expect($item.find('.author').text()).toEqual('[Deleted user]'); expect($item.find('.avatar').attr('data-username')).not.toBeDefined(); }); it('renders mentioned user id to avatar and displayname', function() { view.collection.set(testComments); var $comment = view.$el.find('.comment[data-id=3] .message'); expect($comment.length).toEqual(1); expect($comment.find('.avatar[data-user=macbeth]').length).toEqual(1); expect($comment.find('strong:first').text()).toEqual('Thane of Cawdor'); expect($comment.find('.avatar[data-user=macbeth] ~ .contactsmenu-popover').length).toEqual(1); expect($comment.find('.avatar[data-user=banquo]').length).toEqual(1); expect($comment.find('.avatar[data-user=banquo] ~ strong').text()).toEqual('Lord Banquo'); expect($comment.find('.avatar[data-user=banquo] ~ .contactsmenu-popover').length).toEqual(1); }); }); describe('more comments', function() { var hasMoreResultsStub; beforeEach(function() { view.collection.set(testComments); hasMoreResultsStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'hasMoreResults'); }); afterEach(function() { hasMoreResultsStub.restore(); }); it('shows "More comments" button when more comments are available', function() { hasMoreResultsStub.returns(true); view.collection.trigger('sync'); expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(false); }); it('does not show "More comments" button when more comments are available', function() { hasMoreResultsStub.returns(false); view.collection.trigger('sync'); expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(true); }); it('fetches and appends the next page when clicking the "More" button', function() { hasMoreResultsStub.returns(true); expect(fetchStub.notCalled).toEqual(true); view.$el.find('.showMore').trigger('click'); expect(fetchStub.calledOnce).toEqual(true); }); it('appends comment to the list when added to collection', function() { var comment4 = new OCA.Comments.CommentModel({ id: 4, actorType: 'users', actorId: 'user3', actorDisplayName: 'User Three', objectType: 'files', objectId: 5, message: 'Third', creationDateTime: new Date(Date.UTC(2016, 1, 3, 5, 0, 0)).toUTCString() }); view.collection.add(comment4); expect(view.$el.find('.comments>li').length).toEqual(4); var $item = view.$el.find('.comments>li').eq(3); expect($item.find('.author').text()).toEqual('User Three'); expect($item.find('.date').text()).toEqual('5 hours ago'); expect($item.find('.message').html()).toEqual('Third'); }); }); describe('posting comments', function() { var createStub; var currentUserStub; var $newCommentForm; beforeEach(function() { view.collection.set(testComments); createStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'create'); currentUserStub = sinon.stub(OC, 'getCurrentUser'); currentUserStub.returns({ uid: 'testuser', displayName: 'Test User' }); $newCommentForm = view.$el.find('.newCommentForm'); // Required for the absolute selector used to find the new comment // after a successful creation in _onSubmitSuccess. $('#testArea').append(view.$el); }); afterEach(function() { createStub.restore(); currentUserStub.restore(); }); it('creates a new comment when clicking post button', function() { $newCommentForm.find('.message').text('New message'); $newCommentForm.submit(); expect(createStub.calledOnce).toEqual(true); expect(createStub.lastCall.args[0]).toEqual({ actorId: 'testuser', actorDisplayName: 'Test User', actorType: 'users', verb: 'comment', message: 'New message', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() }); }); it('creates a new comment when typing enter', function() { $newCommentForm.find('.message').text('New message'); var keydownEvent = new $.Event('keydown', {keyCode: 13}); $newCommentForm.find('.message').trigger(keydownEvent); expect(createStub.calledOnce).toEqual(true); expect(createStub.lastCall.args[0]).toEqual({ actorId: 'testuser', actorDisplayName: 'Test User', actorType: 'users', verb: 'comment', message: 'New message', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() }); expect(keydownEvent.isDefaultPrevented()).toEqual(true); }); it('creates a new mention when typing enter in the autocomplete popover', function() { var autoCompleteStub = sinon.stub(view, '_onAutoComplete'); autoCompleteStub.callsArgWith(1, [{"id":"userId", "label":"User Name", "source":"users"}]); // Force the autocomplete to be initialized view._initAutoComplete($newCommentForm.find('.message')); // PhantomJS does not seem to handle typing in a contenteditable, so // some tricks are needed to show the autocomplete popover. // // Instead of sending key events to type "@u" the characters are // programatically set in the input field. $newCommentForm.find('.message').text('Mention to @u'); // When focusing on the input field the caret is not guaranteed to // be at the end; instead of calling "focus()" on the input field // the caret is explicitly set at the end of the input field, that // is, after "@u". var range = document.createRange(); range.selectNodeContents($newCommentForm.find('.message')[0]); range.collapse(false); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); // As PhantomJS does not handle typing in a contenteditable the key // typed here is in practice ignored by At.js, but despite that it // will cause the popover to be shown. $newCommentForm.find('.message').trigger(new $.Event('keydown', {keyCode: 's'})); $newCommentForm.find('.message').trigger(new $.Event('keyup', {keyCode: 's'})); expect(autoCompleteStub.calledOnce).toEqual(true); var keydownEvent = new $.Event('keydown', {keyCode: 13}); $newCommentForm.find('.message').trigger(keydownEvent); expect(createStub.calledOnce).toEqual(false); expect($newCommentForm.find('.message').html()).toContain('Mention to User Name'); expect($newCommentForm.find('.message').text()).not.toContain('@'); // In this case the default behaviour is prevented by the // "onKeydown" event handler of At.js. expect(keydownEvent.isDefaultPrevented()).toEqual(true); }); it('creates a new line when typing shift+enter', function() { $newCommentForm.find('.message').text('New message'); var keydownEvent = new $.Event('keydown', {keyCode: 13, shiftKey: true}); $newCommentForm.find('.message').trigger(keydownEvent); expect(createStub.calledOnce).toEqual(false); // PhantomJS does not seem to handle typing in a contenteditable, so // instead of looking for a new line the best that can be done is // checking that the default behaviour would have been executed. expect($newCommentForm.find('.message').text()).toContain('New message'); expect(keydownEvent.isDefaultPrevented()).toEqual(false); }); it('creates a new comment with mentions when clicking post button', function() { $newCommentForm.find('.message').text('New message @anotheruser'); $newCommentForm.submit(); var createStubExpectedData = { actorId: 'testuser', actorDisplayName: 'Test User', actorType: 'users', verb: 'comment', message: 'New message @anotheruser', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() }; expect(createStub.calledOnce).toEqual(true); expect(createStub.lastCall.args[0]).toEqual(createStubExpectedData); var model = new OCA.Comments.CommentModel(_.extend({id: 4}, createStubExpectedData)); var fetchStub = sinon.stub(model, 'fetch'); // simulate the fact that create adds the model to the collection view.collection.add(model, {at: 0}); createStub.yieldTo('success', model); expect(fetchStub.calledOnce).toEqual(true); // simulate the fact that fetch sets the attribute model.set('mentions', { 0: { mentionDisplayName: "Another User", mentionId: "anotheruser", mentionTye: "user" } }); fetchStub.yieldTo('success', model); // comment was added to the list var $comment = view.$el.find('.comment[data-id=4]'); expect($comment.length).toEqual(1); var $message = $comment.find('.message'); expect($message.html()).toContain('New message'); expect($message.find('.avatar').length).toEqual(1); expect($message.find('.avatar[data-user=anotheruser]').length).toEqual(1); expect($message.find('.avatar[data-user=anotheruser] ~ strong').text()).toEqual('Another User'); expect($message.find('.avatar[data-user=anotheruser] ~ .contactsmenu-popover').length).toEqual(1); }); it('does not create a comment if the field is empty', function() { $newCommentForm.find('.message').val(' '); $newCommentForm.submit(); expect(createStub.notCalled).toEqual(true); }); it('does not create a comment if the field length is too large', function() { var bigMessage = ''; for (var i = 0; i < view._commentMaxLength * 2; i++) { bigMessage += 'a'; } $newCommentForm.find('.message').val(bigMessage); $newCommentForm.submit(); expect(createStub.notCalled).toEqual(true); }); describe('limit indicator', function() { var tooltipStub; var $message; var $submitButton; beforeEach(function() { tooltipStub = sinon.stub($.fn, 'tooltip'); $message = $newCommentForm.find('.message'); $submitButton = $newCommentForm.find('.submit'); }); afterEach(function() { tooltipStub.restore(); }); it('does not displays tooltip when limit is far away', function() { $message.val(createMessageWithLength(3)); $message.trigger('change'); expect(tooltipStub.calledWith('show')).toEqual(false); expect($submitButton.prop('disabled')).toEqual(false); expect($message.hasClass('error')).toEqual(false); }); it('displays tooltip when limit is almost reached', function() { $message.text(createMessageWithLength(view._commentMaxLength - 2)); $message.trigger('change'); expect(tooltipStub.calledWith('show')).toEqual(true); expect($submitButton.prop('disabled')).toEqual(false); expect($message.hasClass('error')).toEqual(false); }); it('displays tooltip and disabled button when limit is exceeded', function() { $message.text(createMessageWithLength(view._commentMaxLength + 2)); $message.trigger('change'); expect(tooltipStub.calledWith('show')).toEqual(true); expect($submitButton.prop('disabled')).toEqual(true); expect($message.hasClass('error')).toEqual(true); }); }); }); describe('editing comments', function() { var saveStub; var fetchStub; var currentUserStub; beforeEach(function() { saveStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'save'); fetchStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'fetch'); currentUserStub = sinon.stub(OC, 'getCurrentUser'); currentUserStub.returns({ uid: 'testuser', displayName: 'Test User' }); view.collection.add({ id: 1, actorId: 'testuser', actorDisplayName: 'Test User', actorType: 'users', verb: 'comment', message: 'New message', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString() }); view.collection.add({ id: 2, actorId: 'anotheruser', actorDisplayName: 'Another User', actorType: 'users', verb: 'comment', message: 'New message from another user', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(), }); view.collection.add({ id: 3, actorId: 'testuser', actorDisplayName: 'Test User', actorType: 'users', verb: 'comment', message: 'Hail to thee, @macbeth. Yours faithfully, @banquo', creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(), mentions: { 0: { mentionDisplayName: "Thane of Cawdor", mentionId: "macbeth", mentionTye: "user" }, 1: { mentionDisplayName: "Lord Banquo", mentionId: "banquo", mentionTye: "user" } } }); }); afterEach(function() { saveStub.restore(); fetchStub.restore(); currentUserStub.restore(); }); it('shows edit link for owner comments', function() { var $comment = view.$el.find('.comment[data-id=1]'); expect($comment.length).toEqual(1); $comment.find('.action.more').trigger('click'); expect($comment.find('.action.edit').length).toEqual(1); }); it('does not show edit link for other user\'s comments', function() { var $comment = view.$el.find('.comment[data-id=2]'); expect($comment.length).toEqual(1); $comment.find('.action.more').trigger('click'); expect($comment.find('.action.edit').length).toEqual(0); }); it('shows edit form when clicking edit', function() { var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.more').trigger('click'); $comment.find('.action.edit').trigger('click'); expect($comment.hasClass('hidden')).toEqual(true); var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); expect($formRow.length).toEqual(1); }); it('saves message and updates comment item when clicking save', function() { var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.more').trigger('click'); $comment.find('.action.edit').trigger('click'); var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); expect($formRow.length).toEqual(1); $formRow.find('div.message').text('modified message'); $formRow.find('form').submit(); expect(saveStub.calledOnce).toEqual(true); expect(saveStub.lastCall.args[0]).toEqual({ message: 'modified message' }); var model = view.collection.get(1); // simulate the fact that save sets the attribute model.set('message', 'modified\nmessage'); saveStub.yieldTo('success', model); view.collection.get(model); expect(fetchStub.called).toEqual(true); fetchStub.yieldTo('success', model); // original comment element is visible again expect($comment.hasClass('hidden')).toEqual(false); // and its message was updated expect($comment.find('.message').html()).toEqual('modified
message'); // form row is gone $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); expect($formRow.length).toEqual(0); }); it('saves message and updates comment item with mentions when clicking save', function() { var $comment = view.$el.find('.comment[data-id=3]'); $comment.find('.action.more').trigger('click'); $comment.find('.action.edit').trigger('click'); var $formRow = view.$el.find('.newCommentRow.comment[data-id=3]'); expect($formRow.length).toEqual(1); $formRow.find('div.message').text('modified\nmessage @anotheruser'); $formRow.find('form').submit(); expect(saveStub.calledOnce).toEqual(true); expect(saveStub.lastCall.args[0]).toEqual({ message: 'modified\nmessage @anotheruser' }); var model = view.collection.get(3); // simulate the fact that save sets the attribute model.set('message', 'modified\nmessage @anotheruser'); saveStub.yieldTo('success', model); expect(fetchStub.called).toEqual(true); // simulate the fact that fetch sets the attribute model.set('mentions', { 0: { mentionDisplayName: "Another User", mentionId: "anotheruser", mentionTye: "user" } }); fetchStub.yieldTo('success', model); // original comment element is visible again expect($comment.hasClass('hidden')).toEqual(false); // and its message was updated var $message = $comment.find('.message'); expect($message.html()).toContain('modified
message'); expect($message.find('.avatar').length).toEqual(1); expect($message.find('.avatar[data-user=anotheruser]').length).toEqual(1); expect($message.find('.avatar[data-user=anotheruser] ~ strong').text()).toEqual('Another User'); expect($message.find('.avatar[data-user=anotheruser] ~ .contactsmenu-popover').length).toEqual(1); // form row is gone $formRow = view.$el.find('.newCommentRow.comment[data-id=3]'); expect($formRow.length).toEqual(0); }); it('restores original comment when cancelling', function() { var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.more').trigger('click'); $comment.find('.action.edit').trigger('click'); var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); expect($formRow.length).toEqual(1); $formRow.find('textarea').val('modified\nmessage'); $formRow.find('.cancel').trigger('click'); expect(saveStub.notCalled).toEqual(true); // original comment element is visible again expect($comment.hasClass('hidden')).toEqual(false); // and its message was not updated expect($comment.find('.message').html()).toEqual('New message'); // form row is gone $formRow = view.$el.find('.newCommentRow.comment[data-id=1]'); expect($formRow.length).toEqual(0); }); it('destroys model when clicking delete', function() { var destroyStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'destroy'); var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.more').trigger('click'); $comment.find('.action.delete').trigger('click'); expect(destroyStub.calledOnce).toEqual(true); expect(destroyStub.thisValues[0].id).toEqual(1); destroyStub.yieldTo('success'); // original comment element is gone $comment = view.$el.find('.comment[data-id=1]'); expect($comment.length).toEqual(0); destroyStub.restore(); }); it('does not submit comment if the field is empty', function() { var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.edit').trigger('click'); $comment.find('.message').val(' '); $comment.find('form').submit(); expect(saveStub.notCalled).toEqual(true); }); it('does not submit comment if the field length is too large', function() { var $comment = view.$el.find('.comment[data-id=1]'); $comment.find('.action.edit').trigger('click'); $comment.find('.message').val(createMessageWithLength(view._commentMaxLength * 2)); $comment.find('form').submit(); expect(saveStub.notCalled).toEqual(true); }); }); describe('read marker', function() { var updateMarkerStub; beforeEach(function() { updateMarkerStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'updateReadMarker'); }); afterEach(function() { updateMarkerStub.restore(); }); it('resets the read marker after REPORT', function() { testComments[0].set('isUnread', true, {silent: true}); testComments[1].set('isUnread', true, {silent: true}); view.collection.set(testComments); view.collection.trigger('sync', 'REPORT'); expect(updateMarkerStub.calledOnce).toEqual(true); expect(updateMarkerStub.lastCall.args[0]).toBeFalsy(); }); it('does not reset the read marker if there was no unread comments', function() { view.collection.set(testComments); view.collection.trigger('sync', 'REPORT'); expect(updateMarkerStub.notCalled).toEqual(true); }); it('does not reset the read marker when posting comments', function() { testComments[0].set('isUnread', true, {silent: true}); testComments[1].set('isUnread', true, {silent: true}); view.collection.set(testComments); view.collection.trigger('sync', 'POST'); expect(updateMarkerStub.notCalled).toEqual(true); }); }); });