diff options
author | marco <marcoambrosini@pm.me> | 2022-02-03 15:43:13 +0300 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2022-03-21 14:08:25 +0300 |
commit | b3ea65f6d10cfc0dfc726f30782121689cdb20f1 (patch) | |
tree | 8cbb45b1bee251fada83b746691897e62884fe09 /src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js | |
parent | c983d907cc03378b18f7dc3079e51561ba3f35b8 (diff) |
create MessageButtonsBar component
Signed-off-by: marco <marcoambrosini@pm.me>
Diffstat (limited to 'src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js')
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js | 468 |
1 files changed, 468 insertions, 0 deletions
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js new file mode 100644 index 000000000..eeff13add --- /dev/null +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js @@ -0,0 +1,468 @@ +import Vuex from 'vuex' +import { createLocalVue, shallowMount } from '@vue/test-utils' +import { cloneDeep } from 'lodash' +import storeConfig from '../../../../../store/storeConfig' +import { CONVERSATION, PARTICIPANT, ATTENDEE } from '../../../../../constants' +import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import { findActionButton } from '../../../../../test-helpers' +import MessageButtonsBar from './../MessageButtonsBar/MessageButtonsBar.vue' + +describe('MessageButtonsBar.vue', () => { + const TOKEN = 'XXTOKENXX' + let localVue + let testStoreConfig + let store + let messageProps + let conversationProps + let getActorTypeMock + + beforeEach(() => { + localVue = createLocalVue() + localVue.use(Vuex) + + conversationProps = { + token: TOKEN, + lastCommonReadMessage: 0, + type: CONVERSATION.TYPE.GROUP, + readOnly: CONVERSATION.STATE.READ_WRITE, + } + + testStoreConfig = cloneDeep(storeConfig) + testStoreConfig.modules.tokenStore.getters.getToken + = jest.fn().mockReturnValue(() => TOKEN) + testStoreConfig.modules.conversationsStore.getters.conversation + = jest.fn().mockReturnValue((token) => conversationProps) + testStoreConfig.modules.actorStore.getters.getActorId + = jest.fn().mockReturnValue(() => 'user-id-1') + getActorTypeMock = jest.fn().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.USERS) + testStoreConfig.modules.actorStore.getters.getActorType = getActorTypeMock + + messageProps = { + message: 'test message', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + actorId: 'user-id-1', + actorDisplayName: 'user-display-name-1', + messageParameters: {}, + id: 123, + isTemporary: false, + isFirstMessage: true, + isReplyable: true, + timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000, + token: TOKEN, + systemMessage: '', + messageType: 'comment', + } + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('actions', () => { + + beforeEach(() => { + store = new Vuex.Store(testStoreConfig) + }) + + describe('reply action', () => { + test('replies to message', async () => { + const replyAction = jest.fn() + testStoreConfig.modules.quoteReplyStore.actions.addMessageToBeReplied = replyAction + store = new Vuex.Store(testStoreConfig) + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Reply') + expect(actionButton.exists()).toBe(true) + expect(actionButton.isVisible()).toBe(true) + await actionButton.find('button').trigger('click') + + expect(replyAction).toHaveBeenCalledWith(expect.anything(), { + id: 123, + actorId: 'user-id-1', + actorType: 'users', + actorDisplayName: 'user-display-name-1', + message: 'test message', + messageParameters: {}, + messageType: 'comment', + systemMessage: '', + timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000, + token: TOKEN, + }) + }) + + test('hides reply button when not replyable', async () => { + messageProps.isReplyable = false + store = new Vuex.Store(testStoreConfig) + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Reply') + expect(actionButton.isVisible()).toBe(false) + }) + }) + + describe('private reply action', () => { + test('creates a new conversation when replying to message privately', async () => { + const routerPushMock = jest.fn().mockResolvedValue() + const createOneToOneConversation = jest.fn() + testStoreConfig.modules.conversationsStore.actions.createOneToOneConversation = createOneToOneConversation + store = new Vuex.Store(testStoreConfig) + + messageProps.actorId = 'another-user' + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + mocks: { + $router: { + push: routerPushMock, + }, + }, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Reply privately') + expect(actionButton.exists()).toBe(true) + + createOneToOneConversation.mockResolvedValueOnce({ + token: 'new-token', + }) + + await actionButton.find('button').trigger('click') + + expect(createOneToOneConversation).toHaveBeenCalledWith(expect.anything(), 'another-user') + + expect(routerPushMock).toHaveBeenCalledWith({ + name: 'conversation', + params: { + token: 'new-token', + }, + }) + }) + + /** + * @param {boolean} visible Whether or not the reply-private action is visible + */ + function testPrivateReplyActionVisible(visible) { + store = new Vuex.Store(testStoreConfig) + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Reply privately') + expect(actionButton.exists()).toBe(visible) + } + + test('hides private reply action for own messages', async () => { + // using default message props which have the + // actor id set to the current user + testPrivateReplyActionVisible(false) + }) + + test('hides private reply action for one to one conversation type', async () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.ONE_TO_ONE + testPrivateReplyActionVisible(false) + }) + + test('hides private reply action for guest messages', async () => { + messageProps.actorId = 'guest-user' + messageProps.actorType = ATTENDEE.ACTOR_TYPE.GUESTS + testPrivateReplyActionVisible(false) + }) + + test('hides private reply action when current user is a guest', async () => { + messageProps.actorId = 'another-user' + getActorTypeMock.mockClear().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.GUESTS) + testPrivateReplyActionVisible(false) + }) + }) + + describe('delete action', () => { + test('deletes message', async () => { + let resolveDeleteMessage + const deleteMessage = jest.fn().mockReturnValue(new Promise((resolve, reject) => { resolveDeleteMessage = resolve })) + testStoreConfig.modules.messagesStore.actions.deleteMessage = deleteMessage + store = new Vuex.Store(testStoreConfig) + + // need to mock the date to be within 6h + const mockDate = new Date('2020-05-07 10:00:00') + jest.spyOn(global.Date, 'now') + .mockImplementation(() => mockDate) + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Delete') + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + expect(deleteMessage).toHaveBeenCalledWith(expect.anything(), { + message: { + token: TOKEN, + id: 123, + }, + placeholder: expect.anything(), + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.isDeleting).toBe(true) + expect(wrapper.find('.icon-loading-small').exists()).toBe(true) + + resolveDeleteMessage(200) + // needs two updates... + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isDeleting).toBe(false) + expect(wrapper.find('.icon-loading-small').exists()).toBe(false) + }) + + /** + * @param {boolean} visible Whether or not the delete action is visible + * @param {Date} mockDate The message date (deletion only works within 6h) + * @param {number} participantType The participant type of the user + */ + function testDeleteMessageVisible(visible, mockDate, participantType = PARTICIPANT.TYPE.USER) { + store = new Vuex.Store(testStoreConfig) + + // need to mock the date to be within 6h + if (!mockDate) { + mockDate = new Date('2020-05-07 10:00:00') + } + + jest.spyOn(global.Date, 'now') + .mockImplementation(() => mockDate) + + messageProps.participant = { + actorId: 'user-id-1', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + participantType, + } + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Delete') + expect(actionButton.exists()).toBe(visible) + } + + test('hides delete action when message is older than 6 hours', () => { + testDeleteMessageVisible(false, new Date('2020-05-07 15:24:00')) + }) + + test('hides delete action when the conversation is read-only', () => { + conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY + testDeleteMessageVisible(false) + }) + + test('hides delete action for file messages', () => { + messageProps.message = '{file}' + messageProps.messageParameters.file = {} + testDeleteMessageVisible(false) + }) + + test('hides delete action on other people messages for non-moderators', () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.GROUP + testDeleteMessageVisible(false) + }) + + test('shows delete action on other people messages for moderators', () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.GROUP + testDeleteMessageVisible(true, null, PARTICIPANT.TYPE.MODERATOR) + }) + + test('shows delete action on other people messages for owner', () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.PUBLIC + testDeleteMessageVisible(true, null, PARTICIPANT.TYPE.OWNER) + }) + + test('does not show delete action even for guest moderators', () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.PUBLIC + testDeleteMessageVisible(false, null, PARTICIPANT.TYPE.GUEST_MODERATOR) + }) + + test('does not show delete action on other people messages in one to one conversations', () => { + messageProps.actorId = 'another-user' + conversationProps.type = CONVERSATION.TYPE.ONE_TO_ONE + testDeleteMessageVisible(false) + }) + }) + + test('marks message as unread', async () => { + const updateLastReadMessageAction = jest.fn().mockResolvedValueOnce() + const fetchConversationAction = jest.fn().mockResolvedValueOnce() + testStoreConfig.modules.conversationsStore.actions.updateLastReadMessage = updateLastReadMessageAction + testStoreConfig.modules.conversationsStore.actions.fetchConversation = fetchConversationAction + store = new Vuex.Store(testStoreConfig) + + messageProps.previousMessageId = 100 + + // appears even with more restrictive conditions + conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY + messageProps.actorId = 'another-user' + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + mixins: [{ + computed: { + participant: () => { + return { + actorId: 'guest-id-1', + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + participantType: PARTICIPANT.TYPE.GUEST, + } + }, + }, + }], + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Mark as unread') + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + // needs two updates... + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + expect(updateLastReadMessageAction).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + id: 100, + updateVisually: true, + }) + + expect(fetchConversationAction).toHaveBeenCalledWith(expect.anything(), { + token: TOKEN, + }) + }) + + test('copies message link', async () => { + const copyTextMock = jest.fn() + + // appears even with more restrictive conditions + conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY + messageProps.actorId = 'another-user' + + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + mocks: { + $copyText: copyTextMock, + }, + stubs: { + ActionButton, + }, + mixins: [{ + computed: { + participant: () => { + return { + actorId: 'guest-id-1', + actorType: ATTENDEE.ACTOR_TYPE.GUESTS, + participantType: PARTICIPANT.TYPE.GUEST, + } + }, + }, + }], + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'Copy message link') + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + expect(copyTextMock).toHaveBeenCalledWith('http://localhost/nc-webroot/call/XXTOKENXX#message_123') + }) + + test('renders clickable custom actions', async () => { + const handler = jest.fn() + const handler2 = jest.fn() + const actionsGetterMock = jest.fn().mockReturnValue([{ + label: 'first action', + icon: 'some-icon', + callback: handler, + }, { + label: 'second action', + icon: 'some-icon2', + callback: handler2, + }]) + testStoreConfig.modules.messageActionsStore.getters.messageActions = actionsGetterMock + testStoreConfig.modules.messagesStore.getters.message = jest.fn(() => () => messageProps) + store = new Vuex.Store(testStoreConfig) + const wrapper = shallowMount(MessageButtonsBar, { + localVue, + store, + stubs: { + ActionButton, + }, + propsData: messageProps, + }) + + const actionButton = findActionButton(wrapper, 'first action') + expect(actionButton.exists()).toBe(true) + await actionButton.find('button').trigger('click') + + expect(handler).toHaveBeenCalledWith({ + apiVersion: 'v3', + message: messageProps, + metadata: conversationProps, + }) + + const actionButton2 = findActionButton(wrapper, 'second action') + expect(actionButton2.exists()).toBe(true) + await actionButton2.find('button').trigger('click') + + expect(handler2).toHaveBeenCalledWith({ + apiVersion: 'v3', + message: messageProps, + metadata: conversationProps, + }) + }) + }) +}) |