diff options
author | Vincent Petry <vincent@nextcloud.com> | 2021-05-03 21:30:32 +0300 |
---|---|---|
committer | Vincent Petry <vincent@nextcloud.com> | 2021-05-05 09:52:37 +0300 |
commit | e1c577b9692bdbbc226f37d860ca82660abd6ef8 (patch) | |
tree | 9208d4ee7bf6e04ca9a7b24d47c57d4a06c27ab8 /src | |
parent | d1625a15fe514460f29ac23ff81fbbe3f61e6f71 (diff) |
Added JS tests for messages store
Signed-off-by: Vincent Petry <vincent@nextcloud.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/NewMessageForm/NewMessageForm.vue | 1 | ||||
-rw-r--r-- | src/store/messagesStore.js | 28 | ||||
-rw-r--r-- | src/store/messagesStore.spec.js | 1258 |
3 files changed, 1280 insertions, 7 deletions
diff --git a/src/components/NewMessageForm/NewMessageForm.vue b/src/components/NewMessageForm/NewMessageForm.vue index 91ae7e51d..680d1e6d0 100644 --- a/src/components/NewMessageForm/NewMessageForm.vue +++ b/src/components/NewMessageForm/NewMessageForm.vue @@ -313,6 +313,7 @@ export default { async handleSubmit() { if (this.parsedText !== '') { const temporaryMessage = await this.$store.dispatch('createTemporaryMessage', { text: this.parsedText, token: this.token }) + // FIXME: move "addTemporaryMessage" into "postNewMessage" as it's a pre-requisite anyway ? this.$store.dispatch('addTemporaryMessage', temporaryMessage) this.text = '' this.parsedText = '' diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index bf8fdc159..595ace4c4 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -32,6 +32,9 @@ import SHA1 from 'crypto-js/sha1' import Hex from 'crypto-js/enc-hex' import CancelableRequest from '../utils/cancelableRequest' import { showError } from '@nextcloud/dialogs' +import { + ATTENDEE, +} from '../constants' const state = { /** @@ -444,7 +447,7 @@ const actions = { }, /** - * Deletes the messages of a conversation + * Deletes all messages of a conversation from the store only. * * @param {object} context default store context; * @param {object} token the token of the conversation to be deleted; @@ -491,7 +494,7 @@ const actions = { } // optimistic early commit to avoid indicator flickering - context.commit('updateConversationLastReadMessage', { token, lastReadMessage: id }) + context.dispatch('updateConversationLastReadMessage', { token, lastReadMessage: id }) if (updateVisually) { context.commit('setVisualLastReadMessageId', { token, id }) } @@ -538,7 +541,8 @@ const actions = { // Process each messages and adds it to the store response.data.ocs.data.forEach(message => { - if (message.actorType === 'guests') { + if (message.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) { + // update guest display names cache context.dispatch('setGuestNameIfEmpty', message) } context.dispatch('processMessage', message) @@ -558,7 +562,7 @@ const actions = { if (includeLastKnown && newestKnownMessageId && !context.getters.getLastKnownMessageId(token)) { context.dispatch('setLastKnownMessageId', { - token: token, + token, id: newestKnownMessageId, }) } @@ -611,7 +615,10 @@ const actions = { let lastMessage = null // Process each messages and adds it to the store response.data.ocs.data.forEach(message => { - if (message.actorType === 'guests') { + if (message.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) { + // update guest display names cache, + // force in case the display name has changed since + // the last fetch context.dispatch('forceGuestName', message) } context.dispatch('processMessage', message) @@ -621,7 +628,7 @@ const actions = { }) context.dispatch('setLastKnownMessageId', { - token: token, + token, id: parseInt(response.headers['x-chat-last-given'], 10), }) @@ -651,6 +658,12 @@ const actions = { return false }, + /** + * Sends the given temporary message to the server. + * + * @param {object} context default store context; + * @param {object} temporaryMessage temporary message, must already have been added to messages list. + */ async postNewMessage(context, temporaryMessage) { const { request, cancel } = CancelableRequest(postNewMessage) context.commit('setCancelPostNewMessage', cancel) @@ -694,6 +707,7 @@ const actions = { }) } if (conversation && message.id > conversation.lastReadMessage) { + // no await to make it async context.dispatch('updateLastReadMessage', { token: conversation.token, id: message.id, @@ -708,7 +722,7 @@ const actions = { } let statusCode = null - console.debug(`error while submitting message ${error}`, error) + console.error(`error while submitting message ${error}`, error) if (error.isAxiosError) { statusCode = error?.response?.status } diff --git a/src/store/messagesStore.spec.js b/src/store/messagesStore.spec.js new file mode 100644 index 000000000..0f987a8bb --- /dev/null +++ b/src/store/messagesStore.spec.js @@ -0,0 +1,1258 @@ +import mockConsole from 'jest-mock-console' +import { createLocalVue } from '@vue/test-utils' +import messagesStore from './messagesStore' +import Vuex from 'vuex' +import { cloneDeep } from 'lodash' +import { + ATTENDEE, +} from '../constants' +import { + deleteMessage, + updateLastReadMessage, + fetchMessages, + lookForNewMessages, + postNewMessage, +} from '../services/messagesService' +import CancelableRequest from '../utils/cancelableRequest' +import { showError } from '@nextcloud/dialogs' + +jest.mock('../services/messagesService', () => ({ + deleteMessage: jest.fn(), + updateLastReadMessage: jest.fn(), + fetchMessages: jest.fn(), + lookForNewMessages: jest.fn(), + postNewMessage: jest.fn(), +})) + +jest.mock('../utils/cancelableRequest') +jest.mock('@nextcloud/dialogs', () => ({ + showError: jest.fn(), +})) + +describe('messagesStore', () => { + const TOKEN = 'XXTOKENXX' + let localVue = null + let testStoreConfig + let store = null + let updateConversationLastActiveAction + + beforeEach(() => { + localVue = createLocalVue() + localVue.use(Vuex) + + testStoreConfig = cloneDeep(messagesStore) + + updateConversationLastActiveAction = jest.fn() + testStoreConfig.actions.updateConversationLastActive = updateConversationLastActiveAction + + store = new Vuex.Store(testStoreConfig) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('processMessage', () => { + test('adds message to the list by token', () => { + const message1 = { + id: 1, + token: TOKEN, + } + + store.dispatch('processMessage', message1) + expect(store.getters.messagesList(TOKEN)[0]).toBe(message1) + }) + + test('adds message with its parent to the list', () => { + const parentMessage = { + id: 1, + token: TOKEN, + } + const message1 = { + id: 2, + token: TOKEN, + parent: parentMessage, + } + + store.dispatch('processMessage', message1) + expect(store.getters.messagesList(TOKEN)[0]).toBe(parentMessage) + expect(store.getters.messagesList(TOKEN)[1]).toStrictEqual({ + id: 2, + token: TOKEN, + parent: 1, + }) + }) + + test('deletes matching temporary message when referenced', () => { + const temporaryMessage = { + id: 'temp-1', + referenceId: 'reference-1', + token: TOKEN, + } + store.dispatch('addTemporaryMessage', temporaryMessage) + + const message1 = { + id: 1, + token: TOKEN, + referenceId: 'reference-1', + } + + store.dispatch('processMessage', message1) + expect(store.getters.messagesList(TOKEN)).toStrictEqual([message1]) + }) + + test('replaces existing message', () => { + const message1 = { + id: 1, + token: TOKEN, + message: 'hello', + } + const message2 = Object.assign({}, message1, { message: 'replaced' }) + + store.dispatch('processMessage', message1) + store.dispatch('processMessage', message2) + expect(store.getters.messagesList(TOKEN)).toStrictEqual([message2]) + }) + }) + + test('message list', () => { + const message1 = { + id: 1, + token: TOKEN, + } + const message2 = { + id: 2, + token: 'token-2', + } + const message3 = { + id: 3, + token: TOKEN, + } + + store.dispatch('processMessage', message1) + store.dispatch('processMessage', message2) + store.dispatch('processMessage', message3) + expect(store.getters.messagesList(TOKEN)[0]).toStrictEqual(message1) + expect(store.getters.messagesList(TOKEN)[1]).toStrictEqual(message3) + expect(store.getters.messagesList('token-2')[0]).toStrictEqual(message2) + + // by id + expect(store.getters.messages(TOKEN)[1]).toStrictEqual(message1) + expect(store.getters.messages(TOKEN)[3]).toStrictEqual(message3) + expect(store.getters.messages('token-2')[2]).toStrictEqual(message2) + + // with messages getter + expect(store.getters.messages(TOKEN)).toStrictEqual({ + '1': message1, + '3': message3, + }) + expect(store.getters.messages('token-2')).toStrictEqual({ + '2': message2, + }) + }) + + describe('delete message', () => { + let message + + beforeEach(() => { + message = { + id: 10, + token: TOKEN, + message: 'hello', + } + + store.dispatch('processMessage', message) + }) + + test('deletes from server and replaces with returned system message', async() => { + deleteMessage.mockResolvedValueOnce({ + status: 200, + data: { + ocs: { + data: { + id: 10, + token: TOKEN, + message: '(deleted)', + }, + }, + }, + }) + + const status = await store.dispatch('deleteMessage', { message, placeholder: 'placeholder-text' }) + + expect(deleteMessage).toHaveBeenCalledWith(message) + expect(status).toBe(200) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 10, + token: TOKEN, + message: '(deleted)', + messageType: 'comment_deleted', + }]) + }) + + test('deletes from server and replaces with returned system message including parent', async() => { + deleteMessage.mockResolvedValueOnce({ + status: 200, + data: { + ocs: { + data: { + id: 10, + token: TOKEN, + message: '(deleted)', + parent: { + id: 5, + token: TOKEN, + message: 'parent message', + }, + }, + }, + }, + }) + + const status = await store.dispatch('deleteMessage', { message, placeholder: 'placeholder-text' }) + + expect(deleteMessage).toHaveBeenCalledWith(message) + expect(status).toBe(200) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 5, + token: TOKEN, + message: 'parent message', + }, { + id: 10, + token: TOKEN, + message: '(deleted)', + messageType: 'comment_deleted', + parent: 5, + }]) + }) + + test('shows placeholder while deletion is in progress', () => { + store.dispatch('deleteMessage', { + message, + placeholder: 'placeholder-message', + }).catch(() => {}) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 10, + token: TOKEN, + message: 'placeholder-message', + messageType: 'comment_deleted', + }]) + }) + }) + + test('deletes messages by token from store only', () => { + const message1 = { + id: 1, + token: TOKEN, + } + + store.dispatch('processMessage', message1) + expect(store.getters.messagesList(TOKEN)[0]).toBe(message1) + + store.dispatch('deleteMessages', TOKEN) + expect(store.getters.messagesList(TOKEN)).toStrictEqual([]) + + expect(deleteMessage).not.toHaveBeenCalled() + }) + + describe('temporary messages', () => { + let mockDate + let getMessageToBeRepliedMock + let getActorIdMock + let getActorTypeMock + let getDisplayNameMock + + beforeEach(() => { + mockDate = new Date('2020-01-01 20:00:00') + jest.spyOn(global, 'Date') + .mockImplementation(() => mockDate) + + testStoreConfig = cloneDeep(messagesStore) + + getMessageToBeRepliedMock = jest.fn().mockReturnValue(() => undefined) + getActorIdMock = jest.fn().mockReturnValue(() => 'actor-id-1') + getActorTypeMock = jest.fn().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.USERS) + getDisplayNameMock = jest.fn().mockReturnValue(() => 'actor-display-name-1') + testStoreConfig.getters.getMessageToBeReplied = getMessageToBeRepliedMock + testStoreConfig.getters.getActorId = getActorIdMock + testStoreConfig.getters.getActorType = getActorTypeMock + testStoreConfig.getters.getDisplayName = getDisplayNameMock + testStoreConfig.actions.updateConversationLastActive = updateConversationLastActiveAction + + store = new Vuex.Store(testStoreConfig) + }) + + test('creates temporary message', async() => { + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + expect(getMessageToBeRepliedMock).toHaveBeenCalled() + expect(getActorIdMock).toHaveBeenCalled() + expect(getActorTypeMock).toHaveBeenCalled() + expect(getDisplayNameMock).toHaveBeenCalled() + + expect(temporaryMessage).toStrictEqual({ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }) + }) + + test('creates temporary message with message to be replied', async() => { + getMessageToBeRepliedMock.mockReset() + getMessageToBeRepliedMock.mockReturnValue(() => ({ + id: 123, + })) + + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + expect(temporaryMessage).toStrictEqual({ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + parent: 123, + }) + }) + + test('creates temporary message with file', async() => { + const file = { + type: 'text/plain', + name: 'original-name.txt', + newName: 'new-name.txt', + } + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: 'upload-id-1', + index: 'upload-index-1', + file, + localUrl: 'local-url://original-name.txt', + }) + + expect(temporaryMessage).toStrictEqual({ + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: { + file: { + type: 'file', + file, + mimetype: 'text/plain', + id: expect.stringMatching(/^temp-1577908800000-upload-id-1-0\.[0-9]*$/), + name: 'new-name.txt', + uploadId: 'upload-id-1', + localUrl: 'local-url://original-name.txt', + index: 'upload-index-1', + }, + }, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }) + }) + + test('adds temporary message to the list', async() => { + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + store.dispatch('addTemporaryMessage', temporaryMessage) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }]) + + expect(updateConversationLastActiveAction).toHaveBeenCalledWith(expect.anything(), TOKEN) + + // add again just replaces it + temporaryMessage.message = 'replaced' + store.dispatch('addTemporaryMessage', temporaryMessage) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'replaced', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }]) + }) + + test('marks temporary message as failed', async() => { + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + store.dispatch('addTemporaryMessage', temporaryMessage) + store.dispatch('markTemporaryMessageAsFailed', { + message: temporaryMessage, + reason: 'failure-reason', + }) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([{ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: 'failure-reason', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }]) + }) + + test('removeTemporaryMessageFromStore', async() => { + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + store.dispatch('addTemporaryMessage', temporaryMessage) + store.dispatch('removeTemporaryMessageFromStore', temporaryMessage) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([]) + }) + + test('gets temporary message by reference', async() => { + const temporaryMessage = await store.dispatch('createTemporaryMessage', { + text: 'blah', + token: TOKEN, + uploadId: null, + index: null, + file: null, + localUrl: null, + }) + + store.dispatch('addTemporaryMessage', temporaryMessage) + + expect(store.getters.getTemporaryReferences(TOKEN, temporaryMessage.referenceId)).toStrictEqual([{ + id: 'temp-1577908800000', + actorId: 'actor-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorDisplayName: 'actor-display-name-1', + timestamp: 0, + systemMessage: '', + messageType: '', + message: 'blah', + messageParameters: {}, + token: TOKEN, + isReplyable: false, + sendingFailure: '', + referenceId: expect.stringMatching(/^[a-zA-Z0-9]{40}$/), + }]) + }) + }) + + test('stores first and last known message ids by token', () => { + store.dispatch('setFirstKnownMessageId', { token: TOKEN, id: 1 }) + store.dispatch('setFirstKnownMessageId', { token: 'token-2', id: 2 }) + store.dispatch('setLastKnownMessageId', { token: TOKEN, id: 3 }) + store.dispatch('setLastKnownMessageId', { token: 'token-2', id: 4 }) + + expect(store.getters.getFirstKnownMessageId(TOKEN)).toBe(1) + expect(store.getters.getFirstKnownMessageId('token-2')).toBe(2) + + expect(store.getters.getLastKnownMessageId(TOKEN)).toBe(3) + expect(store.getters.getLastKnownMessageId('token-2')).toBe(4) + }) + + describe('last read message markers', () => { + let conversationsMock + let markConversationReadAction + let getUserIdMock + let updateConversationLastReadMessageMock + + beforeEach(() => { + const conversations = {} + conversations[TOKEN] = { + lastMessage: { + id: 123, + }, + } + + testStoreConfig = cloneDeep(messagesStore) + + getUserIdMock = jest.fn() + conversationsMock = jest.fn().mockReturnValue(conversations) + markConversationReadAction = jest.fn() + updateConversationLastReadMessageMock = jest.fn() + testStoreConfig.getters.conversations = conversationsMock + testStoreConfig.getters.getUserId = getUserIdMock + testStoreConfig.actions.markConversationRead = markConversationReadAction + testStoreConfig.actions.updateConversationLastReadMessage = updateConversationLastReadMessageMock + + updateLastReadMessage.mockResolvedValueOnce() + + store = new Vuex.Store(testStoreConfig) + }) + + test('stores visual last read message id per token', () => { + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 1 }) + store.dispatch('setVisualLastReadMessageId', { token: 'token-2', id: 2 }) + + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(1) + expect(store.getters.getVisualLastReadMessageId('token-2')).toBe(2) + }) + + test('clears last read message', async() => { + getUserIdMock.mockReturnValue(() => 'user-1') + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('clearLastReadMessage', { + token: TOKEN, + updateVisually: false, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).toHaveBeenCalledWith(expect.anything(), TOKEN) + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 123, + }) + + expect(updateLastReadMessage).toHaveBeenCalledWith(TOKEN, 123) + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(100) + }) + + test('clears last read message and update visually', async() => { + getUserIdMock.mockReturnValue(() => 'user-1') + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('clearLastReadMessage', { + token: TOKEN, + updateVisually: true, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).toHaveBeenCalledWith(expect.anything(), TOKEN) + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 123, + }) + + expect(updateLastReadMessage).toHaveBeenCalledWith(TOKEN, 123) + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(123) + }) + + test('clears last read message for guests', async() => { + getUserIdMock.mockReturnValue(() => null) + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('clearLastReadMessage', { + token: TOKEN, + updateVisually: true, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).toHaveBeenCalledWith(expect.anything(), TOKEN) + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 123, + }) + + expect(updateLastReadMessage).not.toHaveBeenCalled() + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(123) + }) + + test('updates last read message', async() => { + getUserIdMock.mockReturnValue(() => 'user-1') + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('updateLastReadMessage', { + token: TOKEN, + id: 200, + updateVisually: false, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).not.toHaveBeenCalled() + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 200, + }) + + expect(updateLastReadMessage).toHaveBeenCalledWith(TOKEN, 200) + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(100) + }) + + test('updates last read message and update visually', async() => { + getUserIdMock.mockReturnValue(() => 'user-1') + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('updateLastReadMessage', { + token: TOKEN, + id: 200, + updateVisually: true, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).not.toHaveBeenCalled() + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 200, + }) + + expect(updateLastReadMessage).toHaveBeenCalledWith(TOKEN, 200) + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(200) + }) + + test('updates last read message for guests', async() => { + getUserIdMock.mockReturnValue(() => null) + + store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) + await store.dispatch('updateLastReadMessage', { + token: TOKEN, + id: 200, + updateVisually: true, + }) + + expect(conversationsMock).toHaveBeenCalled() + expect(markConversationReadAction).not.toHaveBeenCalled() + expect(getUserIdMock).toHaveBeenCalled() + expect(updateConversationLastReadMessageMock).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + lastReadMessage: 200, + }) + + expect(updateLastReadMessage).not.toHaveBeenCalled() + expect(store.getters.getVisualLastReadMessageId(TOKEN)).toBe(200) + }) + }) + + describe('fetchMessages', () => { + let updateLastCommonReadMessageAction + let setGuestNameIfEmptyAction + let cancelFunctionMock + + beforeEach(() => { + testStoreConfig = cloneDeep(messagesStore) + + updateLastCommonReadMessageAction = jest.fn() + setGuestNameIfEmptyAction = jest.fn() + testStoreConfig.actions.updateLastCommonReadMessage = updateLastCommonReadMessageAction + testStoreConfig.actions.setGuestNameIfEmpty = setGuestNameIfEmptyAction + + cancelFunctionMock = jest.fn() + CancelableRequest.mockImplementation((request) => { + return { + request, + cancel: cancelFunctionMock, + } + }) + + store = new Vuex.Store(testStoreConfig) + }) + + test('fetches messages from server including last known', async() => { + const messages = [{ + id: 1, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.USERS, + }, { + id: 2, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + }] + const response = { + headers: { + 'x-chat-last-common-read': '123', + 'x-chat-last-given': '100', + }, + data: { + ocs: { + data: messages, + }, + }, + } + + fetchMessages.mockResolvedValueOnce(response) + + await store.dispatch('fetchMessages', { + token: TOKEN, + lastKnownMessageId: 100, + includeLastKnown: true, + requestOptions: { + dummyOption: true, + }, + }) + + expect(fetchMessages).toHaveBeenCalledWith({ + token: TOKEN, + lastKnownMessageId: 100, + includeLastKnown: true, + }, { + dummyOption: true, + }) + + expect(updateLastCommonReadMessageAction) + .toHaveBeenCalledWith(expect.anything(), { token: TOKEN, lastCommonReadMessage: 123 }) + + expect(setGuestNameIfEmptyAction).toHaveBeenCalledWith(expect.anything(), messages[1]) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual(messages) + expect(store.getters.getFirstKnownMessageId(TOKEN)).toBe(100) + expect(store.getters.getLastKnownMessageId(TOKEN)).toBe(2) + }) + + test('fetches messages from server excluding last known', async() => { + const messages = [{ + id: 1, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.USERS, + }, { + id: 2, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + }] + const response = { + headers: { + 'x-chat-last-common-read': '123', + 'x-chat-last-given': '100', + }, + data: { + ocs: { + data: messages, + }, + }, + } + + fetchMessages.mockResolvedValueOnce(response) + + await store.dispatch('fetchMessages', { + token: TOKEN, + lastKnownMessageId: 100, + includeLastKnown: false, + requestOptions: { + dummyOption: true, + }, + }) + + expect(fetchMessages).toHaveBeenCalledWith({ + token: TOKEN, + lastKnownMessageId: 100, + includeLastKnown: false, + }, { + dummyOption: true, + }) + + expect(updateLastCommonReadMessageAction) + .toHaveBeenCalledWith(expect.anything(), { token: TOKEN, lastCommonReadMessage: 123 }) + + expect(setGuestNameIfEmptyAction).toHaveBeenCalledWith(expect.anything(), messages[1]) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual(messages) + expect(store.getters.getFirstKnownMessageId(TOKEN)).toBe(100) + expect(store.getters.getLastKnownMessageId(TOKEN)).toBe(null) + }) + + test('cancels fetching messages', () => { + store.dispatch('fetchMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(store.state.cancelFetchMessages).toBe(cancelFunctionMock) + + expect(cancelFunctionMock).not.toHaveBeenCalled() + + store.dispatch('cancelFetchMessages') + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + + expect(store.state.cancelFetchMessages).toBe(null) + }) + + test('cancels fetching messages when fetching again', async() => { + store.dispatch('fetchMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(store.state.cancelFetchMessages).toBe(cancelFunctionMock) + + store.dispatch('fetchMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + }) + }) + + describe('look for new messages', () => { + let updateLastCommonReadMessageAction + let updateConversationLastMessageAction + let forceGuestNameAction + let cancelFunctionMock + let conversationMock + + beforeEach(() => { + testStoreConfig = cloneDeep(messagesStore) + + conversationMock = jest.fn() + updateConversationLastMessageAction = jest.fn() + updateLastCommonReadMessageAction = jest.fn() + forceGuestNameAction = jest.fn() + testStoreConfig.getters.conversation = jest.fn().mockReturnValue(conversationMock) + testStoreConfig.actions.updateConversationLastMessage = updateConversationLastMessageAction + testStoreConfig.actions.updateLastCommonReadMessage = updateLastCommonReadMessageAction + testStoreConfig.actions.forceGuestName = forceGuestNameAction + + cancelFunctionMock = jest.fn() + CancelableRequest.mockImplementation((request) => { + return { + request, + cancel: cancelFunctionMock, + } + }) + + store = new Vuex.Store(testStoreConfig) + }) + + test('looks for new messages', async() => { + const messages = [{ + id: 1, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.USERS, + }, { + id: 2, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + }] + const response = { + headers: { + 'x-chat-last-common-read': '123', + 'x-chat-last-given': '100', + }, + data: { + ocs: { + data: messages, + }, + }, + } + + lookForNewMessages.mockResolvedValueOnce(response) + + // smaller number to make it update + conversationMock.mockReturnValue({ lastMessage: { id: 1 } }) + + await store.dispatch('lookForNewMessages', { + token: TOKEN, + lastKnownMessageId: 100, + requestOptions: { + dummyOption: true, + }, + }) + + expect(lookForNewMessages).toHaveBeenCalledWith({ + token: TOKEN, + lastKnownMessageId: 100, + }, { + dummyOption: true, + }) + + expect(conversationMock).toHaveBeenCalledWith(TOKEN) + expect(updateConversationLastMessageAction) + .toHaveBeenCalledWith(expect.anything(), { token: TOKEN, lastMessage: messages[1] }) + expect(updateLastCommonReadMessageAction) + .toHaveBeenCalledWith(expect.anything(), { token: TOKEN, lastCommonReadMessage: 123 }) + + expect(forceGuestNameAction).toHaveBeenCalledWith(expect.anything(), messages[1]) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual(messages) + expect(store.getters.getLastKnownMessageId(TOKEN)).toBe(100) + + // not updated + expect(store.getters.getFirstKnownMessageId(TOKEN)).toBe(null) + }) + + test('looks for new messages does not update last message if lower', async() => { + const messages = [{ + id: 1, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.USERS, + }, { + id: 2, + token: TOKEN, + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + }] + const response = { + headers: {}, + data: { + ocs: { + data: messages, + }, + }, + } + + lookForNewMessages.mockResolvedValueOnce(response) + + // smaller number to make it update + conversationMock.mockReturnValue({ lastMessage: { id: 500 } }) + + await store.dispatch('lookForNewMessages', { + token: TOKEN, + lastKnownMessageId: 100, + requestOptions: { + dummyOption: true, + }, + }) + + expect(updateConversationLastMessageAction) + .not.toHaveBeenCalled() + expect(updateLastCommonReadMessageAction) + .not.toHaveBeenCalled() + + expect(store.getters.getLastKnownMessageId(TOKEN)).toBe(null) + }) + + test('cancels look for new messages', async() => { + store.dispatch('lookForNewMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(store.state.cancelLookForNewMessages).toBe(cancelFunctionMock) + + expect(cancelFunctionMock).not.toHaveBeenCalled() + + store.dispatch('cancelLookForNewMessages') + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + + expect(store.state.cancelLookForNewMessages).toBe(null) + }) + + test('cancels look for new messages when called again', async() => { + store.dispatch('lookForNewMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(store.state.cancelLookForNewMessages).toBe(cancelFunctionMock) + + store.dispatch('lookForNewMessages', { + token: TOKEN, + lastKnownMessageId: 100, + }).catch(() => {}) + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + }) + }) + + describe('posting new message', () => { + let message1 + let conversationMock + let updateLastCommonReadMessageAction + let updateLastReadMessageAction + let updateConversationLastMessageAction + let cancelFunctionMock + let restoreConsole + + beforeEach(() => { + testStoreConfig = cloneDeep(messagesStore) + + jest.useFakeTimers() + + restoreConsole = mockConsole(['error']) + conversationMock = jest.fn() + updateConversationLastMessageAction = jest.fn() + updateLastCommonReadMessageAction = jest.fn() + updateLastReadMessageAction = jest.fn() + testStoreConfig.getters.conversation = jest.fn().mockReturnValue(conversationMock) + testStoreConfig.actions.updateConversationLastMessage = updateConversationLastMessageAction + testStoreConfig.actions.updateLastCommonReadMessage = updateLastCommonReadMessageAction + // mock this complex local action as we already tested it elsewhere + testStoreConfig.actions.updateLastReadMessage = updateLastReadMessageAction + testStoreConfig.actions.updateConversationLastActive = updateConversationLastActiveAction + + cancelFunctionMock = jest.fn() + CancelableRequest.mockImplementation((request) => { + return { + request, + cancel: cancelFunctionMock, + } + }) + + store = new Vuex.Store(testStoreConfig) + message1 = { + id: 1, + token: TOKEN, + message: 'first', + } + + store.dispatch('processMessage', message1) + }) + + afterEach(() => { + restoreConsole() + }) + + test('posts new message', async() => { + const temporaryMessage = { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: '', + } + + const messageResponse = { + id: 200, + token: TOKEN, + message: 'blah', + } + + const response = { + headers: { + 'x-chat-last-common-read': '100', + }, + data: { + ocs: { + data: messageResponse, + }, + }, + } + + postNewMessage.mockResolvedValueOnce(response) + + store.dispatch('addTemporaryMessage', temporaryMessage) + + conversationMock.mockReturnValue({ + token: TOKEN, + lastMessage: { id: 100 }, + lastReadMessage: 50, + }) + + const receivedResponse = await store.dispatch('postNewMessage', temporaryMessage) + + expect(receivedResponse).toBe(response) + + expect(postNewMessage).toHaveBeenCalledWith(temporaryMessage) + + expect(updateLastCommonReadMessageAction).toHaveBeenCalledWith( + expect.anything(), + { token: TOKEN, lastCommonReadMessage: 100 }, + ) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([message1, messageResponse]) + + expect(updateConversationLastMessageAction) + .toHaveBeenCalledWith(expect.anything(), { token: TOKEN, lastMessage: messageResponse }) + + expect(updateLastReadMessageAction).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + id: 200, + updateVisually: true, + }) + }) + + test('cancels posting new message', () => { + const temporaryMessage = { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: '', + } + + store.dispatch('postNewMessage', temporaryMessage).catch(() => {}) + + expect(store.state.cancelPostNewMessage).toBe(cancelFunctionMock) + + expect(cancelFunctionMock).not.toHaveBeenCalled() + + store.dispatch('cancelPostNewMessage') + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + + expect(store.state.cancelPostNewMessage).toBe(null) + }) + + async function testMarkMessageErrors(statusCode, reasonCode) { + const temporaryMessage = { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: '', + } + + const response = { + status: statusCode, + } + + store.dispatch('addTemporaryMessage', temporaryMessage) + + postNewMessage.mockRejectedValueOnce({ isAxiosError: true, response }) + await expect( + store.dispatch('postNewMessage', temporaryMessage) + ).rejects.toMatchObject({ response }) + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([ + message1, + { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: reasonCode, + }, + ]) + + expect(showError).toHaveBeenCalled() + expect(console.error).toHaveBeenCalled() + } + + test('marks message as failed on permission denied', async() => { + await testMarkMessageErrors(403, 'read-only') + }) + + test('marks message as failed when lobby enabled', async() => { + await testMarkMessageErrors(412, 'lobby') + }) + + test('marks message as failed with generic error', async() => { + await testMarkMessageErrors(500, 'other') + }) + + test('cancels after timeout', () => { + const temporaryMessage = { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: '', + } + + store.dispatch('addTemporaryMessage', temporaryMessage) + store.dispatch('postNewMessage', temporaryMessage).catch(() => {}) + + jest.advanceTimersByTime(60000) + + expect(cancelFunctionMock).toHaveBeenCalledWith('canceled') + + expect(store.getters.messagesList(TOKEN)).toStrictEqual([ + message1, + { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: 'timeout', + }, + ]) + }) + + test('does not timeout after request returns', async() => { + const temporaryMessage = { + id: 'temp-123', + message: 'blah', + token: TOKEN, + sendingFailure: '', + } + + const response = { + headers: {}, + data: { + ocs: { + data: { + id: 200, + token: TOKEN, + message: 'blah', + }, + }, + }, + } + + postNewMessage.mockResolvedValueOnce(response) + + store.dispatch('addTemporaryMessage', temporaryMessage) + await store.dispatch('postNewMessage', temporaryMessage) + + jest.advanceTimersByTime(60000) + + expect(cancelFunctionMock).not.toHaveBeenCalled() + }) + + }) +}) |