Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorVincent Petry <vincent@nextcloud.com>2021-05-03 21:30:32 +0300
committerVincent Petry <vincent@nextcloud.com>2021-05-05 09:52:37 +0300
commite1c577b9692bdbbc226f37d860ca82660abd6ef8 (patch)
tree9208d4ee7bf6e04ca9a7b24d47c57d4a06c27ab8 /src
parentd1625a15fe514460f29ac23ff81fbbe3f61e6f71 (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.vue1
-rw-r--r--src/store/messagesStore.js28
-rw-r--r--src/store/messagesStore.spec.js1258
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()
+ })
+
+ })
+})