diff options
author | Vincent Petry <vincent@nextcloud.com> | 2021-05-10 19:25:37 +0300 |
---|---|---|
committer | Vincent Petry <vincent@nextcloud.com> | 2021-05-11 15:44:43 +0300 |
commit | cd5823a8b531665f259b603a30ef4fd9eafe8421 (patch) | |
tree | fd60213aeb6deb419fbf2aeceeec30a125ee91b7 /src | |
parent | 732a53bcc7d8dff0e068ed8b1ae23aeba8c547a4 (diff) |
Add JS tests for MessagesList.vue
Signed-off-by: Vincent Petry <vincent@nextcloud.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/MessagesList/MessagesList.spec.js | 474 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesList.vue | 7 | ||||
-rw-r--r-- | src/constants.js | 2 | ||||
-rw-r--r-- | src/test-setup.js | 2 |
4 files changed, 483 insertions, 2 deletions
diff --git a/src/components/MessagesList/MessagesList.spec.js b/src/components/MessagesList/MessagesList.spec.js new file mode 100644 index 000000000..d9f0f2fa5 --- /dev/null +++ b/src/components/MessagesList/MessagesList.spec.js @@ -0,0 +1,474 @@ +import Vuex from 'vuex' +import { createLocalVue, shallowMount } from '@vue/test-utils' +import { cloneDeep } from 'lodash' +import storeConfig from '../../store/storeConfig' +import { ATTENDEE } from '../../constants' + +import MessagesList from './MessagesList' + +describe('MessagesList.vue', () => { + const TOKEN = 'XXTOKENXX' + let store + let localVue + let testStoreConfig + const messagesListMock = jest.fn() + const getVisualLastReadMessageIdMock = jest.fn() + + beforeEach(() => { + localVue = createLocalVue() + localVue.use(Vuex) + + testStoreConfig = cloneDeep(storeConfig) + testStoreConfig.modules.messagesStore.getters.messagesList + = jest.fn().mockReturnValue(messagesListMock) + testStoreConfig.modules.messagesStore.getters.getVisualLastReadMessageId + = jest.fn().mockReturnValue(getVisualLastReadMessageIdMock) + store = new Vuex.Store(testStoreConfig) + + // hack to catch date separators + const oldTee = global.t + global.t = jest.fn().mockImplementation(function(pkg, text, data) { + if (data && data.relativeDate) { + return data.relativeDate + ', ' + data.absoluteDate + } + return oldTee.apply(this, arguments) + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('message grouping', () => { + test('groups consecutive messages by author', () => { + getVisualLastReadMessageIdMock.mockReturnValue(110) + const messagesGroup1 = [{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'how are you ?', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 200, + isReplyable: true, + }] + + const messagesGroup2 = [{ + id: 200, + token: TOKEN, + actorId: 'bob', + actorDisplayName: 'Bob', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello!', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 300, + isReplyable: true, + }, { + id: 210, + token: TOKEN, + actorId: 'bob', + actorDisplayName: 'Bob', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'fine... how abouty you ?', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 400, + isReplyable: true, + }] + + const messagesGroup3 = [{ + id: 300, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'fine as well, thanks!', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 0, // temporary + isReplyable: true, + }] + + const allMessages = messagesGroup1.concat(messagesGroup2.concat(messagesGroup3)) + messagesListMock.mockReturnValue(allMessages) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + + expect(groups.exists()).toBe(true) + + let group = groups.at(0) + expect(group.props('messages')).toStrictEqual(messagesGroup1) + expect(group.props('previousMessageId')).toBe(0) + expect(group.props('nextMessageId')).toBe(200) + expect(group.props('lastReadMessageId')).toBe(110) + // using attributes to access v-bind props + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + + group = groups.at(1) + expect(group.props('messages')).toStrictEqual(messagesGroup2) + expect(group.props('previousMessageId')).toBe(110) + expect(group.props('nextMessageId')).toBe(300) + expect(group.props('lastReadMessageId')).toBe(110) + expect(group.attributes('actorid')).toBe('bob') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + + group = groups.at(2) + expect(group.props('messages')).toStrictEqual(messagesGroup3) + expect(group.props('previousMessageId')).toBe(210) + expect(group.props('nextMessageId')).toBe(0) + expect(group.props('lastReadMessageId')).toBe(110) + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + + expect(messagesListMock).toHaveBeenCalledWith(TOKEN) + expect(getVisualLastReadMessageIdMock).toHaveBeenCalledWith(TOKEN) + + const placeholder = wrapper.findAllComponents({ name: 'LoadingPlaceholder' }) + expect(placeholder.exists()).toBe(false) + }) + + test('displays a date separator between days', () => { + const messages = [{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: new Date('2020-05-09 13:00:00').getTime() / 1000, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'no one here ?', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: new Date('2020-05-10 13:00:00').getTime() / 1000, + isReplyable: true, + }, { + id: 'temp-120', + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'seems no one is there...', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 0, // temporary, matches current date + isReplyable: true, + }] + + const mockDate = new Date('2020-05-11 13:00:00') + jest.spyOn(global.Date, 'now').mockImplementation(() => mockDate) + + messagesListMock.mockReturnValue(messages) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + + expect(groups.exists()).toBe(true) + + let group = groups.at(0) + expect(group.props('messages')).toStrictEqual([messages[0]]) + // using attributes to access v-bind props + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + expect(group.attributes('dateseparator')).toBe('2 days ago, 9 May 2020') + + group = groups.at(1) + expect(group.props('messages')).toStrictEqual([messages[1]]) + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + expect(group.attributes('dateseparator')).toBe('Yesterday, 10 May 2020') + + group = groups.at(2) + expect(group.props('messages')).toStrictEqual([messages[2]]) + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + expect(group.attributes('dateseparator')).toBe('Today, 11 May 2020') + + expect(messagesListMock).toHaveBeenCalledWith(TOKEN) + }) + + test('groups system messages with each other', () => { + const messages = [{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Alice has entered the call', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_started', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Alice has exited the call', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_ended', + timestamp: 200, + isReplyable: true, + }] + + messagesListMock.mockReturnValue(messages) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + + expect(groups.exists()).toBe(true) + + const group = groups.at(0) + expect(group.props('messages')).toStrictEqual(messages) + // using attributes to access v-bind props + expect(group.attributes('actorid')).toBe('alice') + expect(group.attributes('actortype')).toBe(ATTENDEE.ACTOR_TYPE.USERS) + + expect(messagesListMock).toHaveBeenCalledWith(TOKEN) + }) + + function testNotGrouped(messages) { + messagesListMock.mockReturnValue(messages) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + + expect(groups.exists()).toBe(true) + + let group = groups.at(0) + expect(group.props('messages')).toStrictEqual([messages[0]]) + group = groups.at(1) + expect(group.props('messages')).toStrictEqual([messages[1]]) + } + + test('does not group system messages with regular messages from the same author', () => { + testNotGrouped([{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Alice has entered the call', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_started', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 200, + isReplyable: true, + }]) + }) + + test('does not group messages of changelog bot', () => { + testNotGrouped([{ + id: 100, + token: TOKEN, + actorId: ATTENDEE.CHANGELOG_BOT_ID, + actorDisplayName: 'Changelog bot', + actorType: ATTENDEE.ACTOR_TYPE.BOTS, + message: 'Alice has entered the call', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_started', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: ATTENDEE.CHANGELOG_BOT_ID, + actorDisplayName: 'Changelog bot', + actorType: ATTENDEE.ACTOR_TYPE.BOTS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 200, + isReplyable: true, + }]) + }) + + test('does not group messages with different actor types', () => { + testNotGrouped([{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Alice has entered the call', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_started', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 200, + isReplyable: true, + }]) + }) + + test('renders a placeholder while loading', () => { + messagesListMock.mockReturnValue([]) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + expect(groups.exists()).toBe(false) + + const placeholder = wrapper.findAllComponents({ name: 'LoadingPlaceholder' }) + expect(placeholder.exists()).toBe(true) + }) + + test('skips deleted messages when grouping', () => { + const messages = [{ + id: 100, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 100, + isReplyable: true, + }, { + id: 110, + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'deleted', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'message_deleted', + timestamp: 101, + isReplyable: true, + }, { + id: '120', + token: TOKEN, + actorId: 'alice', + actorDisplayName: 'Alice', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'hello again', + messageType: 'comment', + messageParameters: {}, + systemMessage: '', + timestamp: 102, + isReplyable: true, + }] + + messagesListMock.mockReturnValue(messages) + + const wrapper = shallowMount(MessagesList, { + localVue, + store, + propsData: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + + const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + + expect(groups.exists()).toBe(true) + + const group = groups.at(0) + expect(group.props('messages')).toStrictEqual([messages[0], messages[2]]) + + expect(messagesListMock).toHaveBeenCalledWith(TOKEN) + }) + }) +}) diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index f0d7842d1..b4fd542b3 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -74,6 +74,7 @@ import MessagesGroup from './MessagesGroup/MessagesGroup' import Axios from '@nextcloud/axios' import { subscribe, unsubscribe } from '@nextcloud/event-bus' import isInLobby from '../../mixins/isInLobby' +import { ATTENDEE } from '../../constants' import debounce from 'debounce' import { EventBus } from '../../services/EventBus' import LoadingPlaceholder from '../LoadingPlaceholder' @@ -172,6 +173,7 @@ export default { * @returns {object} */ messages() { + // FIXME: remove if unused ? return this.$store.getters.messages(this.token) }, /** @@ -311,8 +313,8 @@ export default { return false // No previous message } - if (message1.actorType === 'bots' // Don't group messages of commands and bots - && message1.actorId !== 'changelog') { // Apart from the changelog bot + if (message1.actorType === ATTENDEE.ACTOR_TYPE_BOTS // Don't group messages of commands and bots + && message1.actorId !== ATTENDEE.CHANGELOG_BOT_ID) { // Apart from the changelog bot return false } @@ -573,6 +575,7 @@ export default { * @param {object} event The deleteMessage event emitted by the Message component. */ handleDeleteMessage(event) { + // FIXME: unused ? this.$store.dispatch('deleteMessage', event.message) }, diff --git a/src/constants.js b/src/constants.js index 31025531a..5d6980cfc 100644 --- a/src/constants.js +++ b/src/constants.js @@ -52,8 +52,10 @@ export const ATTENDEE = { GUESTS: 'guests', EMAILS: 'emails', GROUPS: 'groups', + BOTS: 'bots', }, BRIDGE_BOT_ID: 'bridge-bot', + CHANGELOG_BOT_ID: 'changelog', } export const PARTICIPANT = { CALL_FLAG: { diff --git a/src/test-setup.js b/src/test-setup.js index 9854337dd..1370b5644 100644 --- a/src/test-setup.js +++ b/src/test-setup.js @@ -44,6 +44,8 @@ global.OCA = { Talk: { }, } + +// TODO: use nextcloud-l10n lib once https://github.com/nextcloud/nextcloud-l10n/issues/271 is solved global.t = jest.fn().mockImplementation((app, text) => text) global.n = jest.fn().mockImplementation((app, text) => text) |