diff --git a/.changeset/friendly-ravens-teach.md b/.changeset/friendly-ravens-teach.md new file mode 100644 index 000000000000..1c464a8679b6 --- /dev/null +++ b/.changeset/friendly-ravens-teach.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +adds unread badge to sidebar collapser diff --git a/.changeset/metal-avocados-serve.md b/.changeset/metal-avocados-serve.md new file mode 100644 index 000000000000..478407fcb97b --- /dev/null +++ b/.changeset/metal-avocados-serve.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/livechat": patch +--- + +Fixes the 'Finish Chat' option in Livechat appearing before the conversation is started, which caused the action to fail. diff --git a/.kodiak.toml b/.kodiak.toml index 496895b2c85a..378f922d81e4 100644 --- a/.kodiak.toml +++ b/.kodiak.toml @@ -16,7 +16,5 @@ include_coauthors=true [merge.automerge_dependencies] versions = ["minor", "patch"] usernames = ["dependabot"] -[update] -ignored_usernames = ["dependabot"] [approve] auto_approve_usernames = ["dependabot"] diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx index 6afa27992fb2..226a32b2aebc 100644 --- a/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx +++ b/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx @@ -1,4 +1,4 @@ -import { Box, SidebarV2CollapseGroup } from '@rocket.chat/fuselage'; +import { Box } from '@rocket.chat/fuselage'; import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; import { useUserPreference, useUserId } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; @@ -13,6 +13,7 @@ import { usePreventDefault } from '../hooks/usePreventDefault'; import { useRoomList } from '../hooks/useRoomList'; import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu'; import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; +import RoomListCollapser from './RoomListCollapser'; import RoomListRow from './RoomListRow'; import RoomListRowWrapper from './RoomListRowWrapper'; import RoomListWrapper from './RoomListWrapper'; @@ -22,7 +23,7 @@ const RoomList = () => { const isAnonymous = !useUserId(); const { collapsedGroups, handleClick, handleKeyDown } = useCollapsedGroups(); - const { groupsCount, groupsList, roomList } = useRoomList({ collapsedGroups }); + const { groupsCount, groupsList, roomList, groupedUnreadInfo } = useRoomList({ collapsedGroups }); const avatarTemplate = useAvatarTemplate(); const sideBarItemTemplate = useTemplateByViewMode(); const { ref } = useResizeObserver({ debounceDelay: 100 }); @@ -51,11 +52,12 @@ const RoomList = () => { ( - handleClick(groupsList[index])} onKeyDown={(e) => handleKeyDown(e, groupsList[index])} - expanded={!collapsedGroups.includes(groupsList[index])} + groupTitle={groupsList[index]} + unreadCount={groupedUnreadInfo[index]} /> )} {...(roomList.length > 0 && { diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomListCollapser.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomListCollapser.tsx new file mode 100644 index 000000000000..3dfd17cb74dd --- /dev/null +++ b/apps/meteor/client/sidebarv2/RoomList/RoomListCollapser.tsx @@ -0,0 +1,37 @@ +import type { ISubscription } from '@rocket.chat/core-typings'; +import { Badge, SidebarV2CollapseGroup } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, KeyboardEvent, MouseEventHandler } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; + +type RoomListCollapserProps = { + groupTitle: string; + collapsedGroups: string[]; + onClick: MouseEventHandler; + onKeyDown: (e: KeyboardEvent) => void; + unreadCount: Pick; +} & Omit, 'onClick' | 'onKeyDown'>; +const RoomListCollapser = ({ groupTitle, unreadCount: unreadGroupCount, collapsedGroups, ...props }: RoomListCollapserProps) => { + const { t } = useTranslation(); + + const { unreadTitle, unreadVariant, showUnread, unreadCount } = useUnreadDisplay(unreadGroupCount); + + return ( + + {unreadCount.total} + + ) : undefined + } + {...props} + /> + ); +}; + +export default RoomListCollapser; diff --git a/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx b/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx index fc1c10c70cf7..27247d0cd8b4 100644 --- a/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx +++ b/apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx @@ -13,43 +13,53 @@ const user = createFakeUser({ type: 'user', }); -const unreadRooms = [ - { ...createFakeSubscription({ t: 'c', unread: 1 }), ...createFakeRoom({ t: 'c' }) }, - { ...createFakeSubscription({ t: 'c', unread: 1 }), ...createFakeRoom({ t: 'c' }) }, - { ...createFakeSubscription({ t: 'c', unread: 1 }), ...createFakeRoom({ t: 'c' }) }, - { ...createFakeSubscription({ t: 'c', unread: 1 }), ...createFakeRoom({ t: 'c' }) }, +const emptyUnread = { + userMentions: 0, + groupMentions: 0, + unread: 0, + tunread: undefined, + tunreadUser: undefined, + tunreadGroup: undefined, + alert: false, +}; + +const unreadChannels = [ + { ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) }, ]; const favoriteRooms = [ - { ...createFakeSubscription({ t: 'c', f: true, unread: undefined }), ...createFakeRoom({ t: 'c' }) }, - { ...createFakeSubscription({ t: 'c', f: true, unread: undefined }), ...createFakeRoom({ t: 'c' }) }, - { ...createFakeSubscription({ t: 'c', f: true, unread: undefined }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, + { ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) }, ]; const teams = [ - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ teamMain: true }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ teamMain: true }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ teamMain: true }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ teamMain: true }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) }, ]; const discussionRooms = [ - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ prid: '123' }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ prid: '124' }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ prid: '125' }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ prid: '126' }) }, - { ...createFakeSubscription({ unread: undefined }), ...createFakeRoom({ prid: '127' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '123' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '124' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '125' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '126' }) }, + { ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '127' }) }, ]; const directRooms = [ - { ...createFakeSubscription({ t: 'd', unread: undefined }), ...createFakeRoom({ t: 'd' }) }, - { ...createFakeSubscription({ t: 'd', unread: undefined }), ...createFakeRoom({ t: 'd' }) }, - { ...createFakeSubscription({ t: 'd', unread: undefined }), ...createFakeRoom({ t: 'd' }) }, - { ...createFakeSubscription({ t: 'd', unread: undefined }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, + { ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) }, ]; -const fakeRooms = [...unreadRooms, ...favoriteRooms, ...teams, ...discussionRooms, ...directRooms]; +const fakeRooms = [...unreadChannels, ...favoriteRooms, ...teams, ...discussionRooms, ...directRooms]; const emptyArr: any[] = []; @@ -228,7 +238,7 @@ it('should return "Unread" group with the correct items if sidebarShowUnread is }); const unreadIndex = result.current.groupsList.indexOf('Unread'); expect(result.current.groupsList).toContain('Unread'); - expect(result.current.groupsCount[unreadIndex]).toEqual(unreadRooms.length); + expect(result.current.groupsCount[unreadIndex]).toEqual(unreadChannels.length); }); it('should not include unread room in unread group if hideUnreadStatus is enabled', async () => { @@ -246,6 +256,22 @@ it('should not include unread room in unread group if hideUnreadStatus is enable const unreadIndex = result.current.groupsList.indexOf('Unread'); const roomListUnread = result.current.roomList.filter((room) => room.unread); - expect(result.current.groupsCount[unreadIndex]).toEqual(unreadRooms.length); - expect(roomListUnread.length).not.toEqual(unreadRooms.length); + expect(result.current.groupsCount[unreadIndex]).toEqual(unreadChannels.length); + expect(roomListUnread.length).not.toEqual(unreadChannels.length); +}); + +it('should accumulate unread data into `groupedUnreadInfo` when group is collapsed', async () => { + const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), { + legacyRoot: true, + wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(), + }); + + const channelsIndex = result.current.groupsList.indexOf('Channels'); + const { groupMentions, unread, userMentions, tunread, tunreadUser } = result.current.groupedUnreadInfo[channelsIndex]; + + expect(groupMentions).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.groupMentions, 0)); + expect(unread).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.unread, 0)); + expect(userMentions).toEqual(fakeRooms.reduce((acc, cv) => acc + cv.userMentions, 0)); + expect(tunread).toEqual(fakeRooms.reduce((acc, cv) => [...acc, ...(cv.tunread || [])], [] as string[])); + expect(tunreadUser).toEqual(fakeRooms.reduce((acc, cv) => [...acc, ...(cv.tunreadUser || [])], [] as string[])); }); diff --git a/apps/meteor/client/sidebarv2/hooks/useRoomList.ts b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts index 5f9a1234c269..d80d32e1e6cd 100644 --- a/apps/meteor/client/sidebarv2/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts @@ -1,6 +1,6 @@ import type { ILivechatInquiryRecord, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; @@ -27,15 +27,16 @@ const order = [ 'Conversations', ] as const; -export const useRoomList = ({ - collapsedGroups, -}: { - collapsedGroups?: string[]; -}): { +type useRoomListReturnType = { roomList: Array; groupsCount: number[]; groupsList: TranslationKey[]; -} => { + groupedUnreadInfo: Pick< + SubscriptionWithRoom, + 'userMentions' | 'groupMentions' | 'unread' | 'tunread' | 'tunreadUser' | 'tunreadGroup' | 'alert' | 'hideUnreadStatus' + >[]; +}; +export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] }): useRoomListReturnType => { const showOmnichannel = useOmnichannelEnabled(); const sidebarGroupByType = useUserPreference('sidebarGroupByType'); const favoritesEnabled = useUserPreference('sidebarShowFavorites'); @@ -53,7 +54,7 @@ export const useRoomList = ({ const queue = inquiries.enabled ? inquiries.queue : emptyQueue; - const { groupsCount, groupsList, roomList } = useDebouncedValue( + const { groupsCount, groupsList, roomList, groupedUnreadInfo } = useDebouncedValue( useMemo(() => { const isCollapsed = (groupTitle: string) => collapsedGroups?.includes(groupTitle); @@ -133,7 +134,7 @@ export const useRoomList = ({ !sidebarGroupByType && groups.set('Conversations', conversation); - const { groupsCount, groupsList, roomList } = sidebarOrder.reduce( + const { groupsCount, groupsList, roomList, groupedUnreadInfo } = sidebarOrder.reduce( (acc, key) => { const value = groups.get(key); @@ -142,11 +143,39 @@ export const useRoomList = ({ } acc.groupsList.push(key as TranslationKey); + + const groupedUnreadInfoAcc = { + userMentions: 0, + groupMentions: 0, + tunread: [], + tunreadUser: [], + unread: 0, + }; + if (isCollapsed(key)) { + const groupedUnreadInfo = [...value].reduce( + (counter, { userMentions, groupMentions, tunread, tunreadUser, unread, alert, hideUnreadStatus }) => { + if (hideUnreadStatus) { + return counter; + } + + counter.userMentions += userMentions || 0; + counter.groupMentions += groupMentions || 0; + counter.tunread = [...counter.tunread, ...(tunread || [])]; + counter.tunreadUser = [...counter.tunreadUser, ...(tunreadUser || [])]; + counter.unread += unread || 0; + !unread && !tunread?.length && alert && (counter.unread += 1); + return counter; + }, + groupedUnreadInfoAcc, + ); + + acc.groupedUnreadInfo.push(groupedUnreadInfo); acc.groupsCount.push(0); return acc; } + acc.groupedUnreadInfo.push(groupedUnreadInfoAcc); acc.groupsCount.push(value.size); acc.roomList.push(...value); return acc; @@ -155,14 +184,11 @@ export const useRoomList = ({ groupsCount: [], groupsList: [], roomList: [], - } as { - groupsCount: number[]; - groupsList: TranslationKey[]; - roomList: Array; - }, + groupedUnreadInfo: [], + } as useRoomListReturnType, ); - return { groupsCount, groupsList, roomList }; + return { groupsCount, groupsList, roomList, groupedUnreadInfo }; }, [ rooms, showOmnichannel, @@ -183,5 +209,6 @@ export const useRoomList = ({ roomList, groupsCount, groupsList, + groupedUnreadInfo, }; }; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index e4b13829a444..5af5e111b506 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -31,7 +31,7 @@ "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "colorette": "^2.0.20", - "cookie": "^0.5.0", + "cookie": "^0.7.0", "cookie-parser": "^1.4.7", "ejson": "^2.2.3", "eventemitter3": "^5.0.1", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 81a8f4b0a661..1262218c6914 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -317,7 +317,7 @@ "colorette": "^2.0.20", "colors": "^1.4.0", "connect": "^3.7.0", - "cookie": "^0.5.0", + "cookie": "^0.7.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "cron": "~1.8.2", @@ -413,7 +413,7 @@ "react-error-boundary": "^3.1.4", "react-hook-form": "~7.45.4", "react-i18next": "~13.2.2", - "react-keyed-flatten-children": "^1.3.0", + "react-keyed-flatten-children": "^3.0.2", "react-virtuoso": "^4.12.0", "sanitize-html": "^2.13.1", "semver": "^7.6.3", diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 4446422d295a..04d01d5a7c71 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -14,24 +14,21 @@ test.describe.serial('e2e-encryption initial setup', () => { let poAccountProfile: AccountProfile; let poHomeChannel: HomeChannel; let password: string; + const newPassword = 'new password'; test.beforeEach(async ({ page }) => { poAccountProfile = new AccountProfile(page); poHomeChannel = new HomeChannel(page); - - await page.goto('/account/security'); }); test.beforeAll(async ({ api }) => { - const statusCode = (await api.post('/settings/E2E_Enable', { value: true })).status(); - - expect(statusCode).toBe(200); + await api.post('/settings/E2E_Enable', { value: true }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); }); test.afterAll(async ({ api }) => { - const statusCode = (await api.post('/settings/E2E_Enable', { value: false })).status(); - - expect(statusCode).toBe(200); + await api.post('/settings/E2E_Enable', { value: false }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test.afterEach(async ({ api }) => { @@ -39,6 +36,8 @@ test.describe.serial('e2e-encryption initial setup', () => { }); test("expect reset user's e2e encryption key", async ({ page }) => { + await page.goto('/account/security'); + // Reset key to start the flow from the beginning // It will execute a logout await poAccountProfile.securityE2EEncryptionSection.click(); @@ -81,8 +80,7 @@ test.describe.serial('e2e-encryption initial setup', () => { }); test('expect change the e2ee password', async ({ page }) => { - // Change the password to a new one and test it - const newPassword = 'new password'; + await page.goto('/account/security'); await restoreState(page, Users.admin); @@ -117,27 +115,143 @@ test.describe.serial('e2e-encryption initial setup', () => { await expect(page.locator('role=banner >> text="Wasn\'t possible to decode your encryption key to be imported."')).not.toBeVisible(); await expect(page.locator('role=banner >> text="Enter your E2E password"')).not.toBeVisible(); }); + + test('expect placeholder text in place of encrypted message', async ({ page }) => { + await page.goto('/home'); + + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('This is an encrypted message.'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + // Logout and login + await poHomeChannel.sidenav.logout(); + await page.locator('role=button[name="Login"]').waitFor(); + await injectInitialData(); + await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); + + await poHomeChannel.sidenav.openChat(channelName); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await expect(poHomeChannel.content.lastUserMessage).toContainText( + 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', + ); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + await poHomeChannel.content.lastUserMessage.hover(); + await expect(page.locator('[role=toolbar][aria-label="Message actions"]')).not.toBeVisible(); + }); + + test('expect placeholder text in place of encrypted file description, when non-encrypted files upload in disabled e2ee room', async ({ + page, + }) => { + await page.goto('/home'); + + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await poHomeChannel.sidenav.openChat(channelName); + + await poHomeChannel.content.dragAndDropTxtFile(); + await poHomeChannel.content.descriptionInput.fill('any_description'); + await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); + await poHomeChannel.content.btnModalConfirm.click(); + + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); + await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + + await test.step('disable E2EE in the room', async () => { + await poHomeChannel.tabs.kebab.click(); + + await expect(poHomeChannel.tabs.btnDisableE2E).toBeVisible(); + await poHomeChannel.tabs.btnDisableE2E.click(); + await expect(page.getByRole('dialog', { name: 'Disable encryption' })).toBeVisible(); + await page.getByRole('button', { name: 'Disable encryption' }).click(); + await poHomeChannel.dismissToast(); + // will wait till the key icon in header goes away + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(0); + }); + + await page.reload(); + + await test.step('upload the file in disabled E2EE room', async () => { + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).not.toBeVisible(); + + await poHomeChannel.content.dragAndDropTxtFile(); + await poHomeChannel.content.descriptionInput.fill('any_description'); + await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); + await poHomeChannel.content.btnModalConfirm.click(); + + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); + + await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); + await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + }); + + await test.step('Enable E2EE in the room', async () => { + await poHomeChannel.tabs.kebab.click(); + + await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); + await poHomeChannel.tabs.btnEnableE2E.click(); + await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible(); + await page.getByRole('button', { name: 'Enable encryption' }).click(); + await poHomeChannel.dismissToast(); + // will wait till the key icon in header appears + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(1); + }); + + // Logout to remove e2ee keys + await poHomeChannel.sidenav.logout(); + + // Login again + await page.locator('role=button[name="Login"]').waitFor(); + await injectInitialData(); + await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); + + await poHomeChannel.sidenav.openChat(channelName); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await expect(poHomeChannel.content.nthMessage(0)).toContainText( + 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', + ); + await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); + }); }); test.describe.serial('e2e-encryption', () => { let poHomeChannel: HomeChannel; - test.beforeEach(async ({ page, api }) => { - const statusCode = (await api.post('/settings/E2E_Enable', { value: true })).status(); + test.use({ storageState: Users.userE2EE.state }); - expect(statusCode).toBe(200); + test.beforeEach(async ({ page, api }) => { + await api.post('/settings/E2E_Enable', { value: true }); poHomeChannel = new HomeChannel(page); await page.goto('/home'); }); test.beforeAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); }); test.afterAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Enable', { value: false }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => { @@ -368,7 +482,7 @@ test.describe.serial('e2e-encryption', () => { await page.keyboard.press('Enter'); await poHomeChannel.sidenav.btnCreate.click(); - await expect(page).toHaveURL(`/direct/rocketchat.internal.admin.testuser2`); + await expect(page).toHaveURL(`/direct/user2${Users.userE2EE.data.username}`); await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); @@ -388,132 +502,10 @@ test.describe.serial('e2e-encryption', () => { await expect(page.getByText('OTR not available')).toBeVisible(); }); - test('expect placeholder text in place of encrypted message, when E2EE is not setup', async ({ page }) => { - const channelName = faker.string.uuid(); - - await poHomeChannel.sidenav.createEncryptedChannel(channelName); - - await expect(page).toHaveURL(`/group/${channelName}`); - - await poHomeChannel.dismissToast(); - - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); - - await poHomeChannel.content.sendMessage('This is an encrypted message.'); - - await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.'); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - // Logout to remove e2ee keys - await poHomeChannel.sidenav.logout(); - - // Login again - await page.locator('role=button[name="Login"]').waitFor(); - await injectInitialData(); - await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); - - await poHomeChannel.sidenav.openChat(channelName); - - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); - - await expect(poHomeChannel.content.lastUserMessage).toContainText( - 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', - ); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - await poHomeChannel.content.lastUserMessage.hover(); - await expect(page.locator('[role=toolbar][aria-label="Message actions"]')).not.toBeVisible(); - }); - - test('expect placeholder text in place of encrypted file description, when E2EE is not setup and non-encrypted files upload in disabled e2ee room', async ({ - page, - }) => { - const channelName = faker.string.uuid(); - - await poHomeChannel.sidenav.openNewByLabel('Channel'); - await poHomeChannel.sidenav.inputChannelName.fill(channelName); - await poHomeChannel.sidenav.advancedSettingsAccordion.click(); - await poHomeChannel.sidenav.checkboxEncryption.click(); - await poHomeChannel.sidenav.btnCreate.click(); - - await expect(page).toHaveURL(`/group/${channelName}`); - - await poHomeChannel.dismissToast(); - - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); - - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); - - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); - - await test.step('disable E2EE in the room', async () => { - await poHomeChannel.tabs.kebab.click(); - - await expect(poHomeChannel.tabs.btnDisableE2E).toBeVisible(); - await poHomeChannel.tabs.btnDisableE2E.click(); - await expect(page.getByRole('dialog', { name: 'Disable encryption' })).toBeVisible(); - await page.getByRole('button', { name: 'Disable encryption' }).click(); - await poHomeChannel.dismissToast(); - // will wait till the key icon in header goes away - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(0); - }); - - await page.reload(); - - await test.step('upload the file in disabled E2EE room', async () => { - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).not.toBeVisible(); - - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); - - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); - - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); - }); - - await test.step('Enable E2EE in the room', async () => { - await poHomeChannel.tabs.kebab.click(); - - await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); - await poHomeChannel.tabs.btnEnableE2E.click(); - await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible(); - await page.getByRole('button', { name: 'Enable encryption' }).click(); - await poHomeChannel.dismissToast(); - // will wait till the key icon in header appears - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(1); - }); - - // Logout to remove e2ee keys - await poHomeChannel.sidenav.logout(); - - // Login again - await page.locator('role=button[name="Login"]').waitFor(); - await injectInitialData(); - await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); - - await poHomeChannel.sidenav.openChat(channelName); - - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); - - await expect(poHomeChannel.content.nthMessage(0)).toContainText( - 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', - ); - await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); - }); - test.describe('File Encryption', async () => { test.afterAll(async ({ api }) => { - expect((await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' })).status()).toBe(200); - expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' })).status()).toBe(200); + await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' }); + await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' }); }); test('File and description encryption', async ({ page }) => { @@ -574,7 +566,7 @@ test.describe.serial('e2e-encryption', () => { }); await test.step('set whitelisted media type setting', async () => { - expect((await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: 'text/plain' })).status()).toBe(200); + await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: 'text/plain' }); }); await test.step('send text file again with whitelist setting set', async () => { @@ -589,7 +581,7 @@ test.describe.serial('e2e-encryption', () => { }); await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => { - expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' })).status()).toBe(200); + await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' }); }); await test.step('send text file again with blacklisted setting set, file upload should fail', async () => { @@ -606,13 +598,13 @@ test.describe.serial('e2e-encryption', () => { test.describe('File encryption setting disabled', async () => { test.beforeAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable_Encrypt_Files', { value: false })).status()).toBe(200); - expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' })).status()).toBe(200); + await api.post('/settings/E2E_Enable_Encrypt_Files', { value: false }); + await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' }); }); test.afterAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable_Encrypt_Files', { value: true })).status()).toBe(200); - expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' })).status()).toBe(200); + await api.post('/settings/E2E_Enable_Encrypt_Files', { value: true }); + await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' }); }); test('Upload file without encryption in e2ee room', async ({ page }) => { @@ -690,11 +682,11 @@ test.describe.serial('e2e-encryption', () => { await page.goto('/home'); }); test.beforeAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test.afterAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); }); test('expect slash commands to be disabled in an e2ee room', async ({ page }) => { @@ -713,7 +705,7 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await page.locator('[name="msg"]').type('/'); + await page.locator('[name="msg"]').pressSequentially('/'); await expect(page.locator('#popup-item-contextualbar')).toHaveClass(/disabled/); }); }); @@ -821,20 +813,15 @@ test.describe.serial('e2e-encryption', () => { let anotherClientPage: Page; test.beforeEach(async ({ browser }) => { - anotherClientPage = (await createAuxContext(browser, Users.admin)).page; + anotherClientPage = (await createAuxContext(browser, Users.userE2EE)).page; }); test.afterEach(async () => { await anotherClientPage.close(); }); - test.afterAll(async () => { - // inject initial data, so that tokens are restored after forced logout - await injectInitialData(); - }); test('expect force logout on e2e keys reset', async ({ page }) => { const poAccountProfile = new AccountProfile(page); - // creating another logged in client, to check force logout await page.goto('/account/security'); @@ -843,11 +830,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page.locator('role=button[name="Login"]')).toBeVisible(); await expect(anotherClientPage.locator('role=button[name="Login"]')).toBeVisible(); - - // await expect(page.locator('role=banner')).toContainText('Your session was ended on this device, please log in again to continue.'); - // await expect(anotherClientPage.locator('role=banner')).toContainText( - // 'Your session was ended on this device, please log in again to continue.', - // ); }); }); }); @@ -863,13 +845,13 @@ test.describe.serial('e2ee room setup', () => { }); test.beforeAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Enable', { value: true }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test.afterAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Enable', { value: false }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test.afterEach(async ({ api }) => { @@ -1042,7 +1024,7 @@ test.describe.serial('e2ee room setup', () => { }); }); -test.describe.serial('e2ee support legacy formats', () => { +test.describe('e2ee support legacy formats', () => { test.use({ storageState: Users.userE2EE.state }); let poHomeChannel: HomeChannel; @@ -1052,8 +1034,8 @@ test.describe.serial('e2ee support legacy formats', () => { }); test.beforeAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Enable', { value: true }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); test.afterAll(async ({ api }) => { diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 67f8133d8a2a..c19e894e3811 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,6 +1,6 @@ import { Users } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; -import { setSettingValueById } from './utils'; +import { createTargetChannel, setSettingValueById } from './utils'; import { setUserPreferences } from './utils/setUserPreferences'; import { test, expect } from './utils/test'; @@ -9,13 +9,16 @@ test.use({ storageState: Users.admin.state }); test.describe.serial('feature preview', () => { let poHomeChannel: HomeChannel; let poAccountProfile: AccountProfile; + let targetChannel: string; test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); + targetChannel = await createTargetChannel(api); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); + await api.post('/channels.delete', { roomName: targetChannel }); }); test.beforeEach(async ({ page }) => { @@ -73,13 +76,13 @@ test.describe.serial('feature preview', () => { test('should display "Recent" button on sidebar search section, and display recent chats when clicked', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidenav.btnRecent.click(); - await expect(poHomeChannel.sidenav.sidebar.getByRole('heading', { name: 'Recent' })).toBeVisible(); + await poHomeChannel.sidebar.btnRecent.click(); + await expect(poHomeChannel.sidebar.sidebar.getByRole('heading', { name: 'Recent' })).toBeVisible(); }); test('should expand/collapse sidebar groups', async ({ page }) => { await page.goto('/home'); - const collapser = poHomeChannel.sidenav.firstCollapser; + const collapser = poHomeChannel.sidebar.firstCollapser; let isExpanded: boolean; await collapser.click(); @@ -94,7 +97,7 @@ test.describe.serial('feature preview', () => { test('should expand/collapse sidebar groups with keyboard', async ({ page }) => { await page.goto('/home'); - const collapser = poHomeChannel.sidenav.firstCollapser; + const collapser = poHomeChannel.sidebar.firstCollapser; await expect(async () => { await collapser.focus(); @@ -115,7 +118,7 @@ test.describe.serial('feature preview', () => { test('should be able to use keyboard to navigate through sidebar items', async ({ page }) => { await page.goto('/home'); - const collapser = poHomeChannel.sidenav.firstCollapser; + const collapser = poHomeChannel.sidebar.firstCollapser; const dataIndex = await collapser.locator('../..').getAttribute('data-index'); const nextItem = page.locator(`[data-index="${Number(dataIndex) + 1}"]`).getByRole('link'); @@ -129,7 +132,7 @@ test.describe.serial('feature preview', () => { test('should persist collapsed/expanded groups after page reload', async ({ page }) => { await page.goto('/home'); - const collapser = poHomeChannel.sidenav.firstCollapser; + const collapser = poHomeChannel.sidebar.firstCollapser; await collapser.click(); const isExpanded = await collapser.getAttribute('aria-expanded'); @@ -138,5 +141,22 @@ test.describe.serial('feature preview', () => { const isExpandedAfterReload = await collapser.getAttribute('aria-expanded'); expect(isExpanded).toEqual(isExpandedAfterReload); }); + + test('should show unread badge on collapser when group is collapsed and has unread items', async ({ page }) => { + await page.goto('/home'); + + await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello world'); + + await poHomeChannel.sidebar.typeSearch(targetChannel); + const item = poHomeChannel.sidebar.getSearchRoomByName(targetChannel); + await poHomeChannel.sidebar.markItemAsUnread(item); + await poHomeChannel.sidebar.escSearch(); + + const collapser = poHomeChannel.sidebar.firstCollapser; + await collapser.click(); + + await expect(poHomeChannel.sidebar.getItemUnreadBadge(collapser)).toBeVisible(); + }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts index 59d42dfe1e10..5f3a35004a8d 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts @@ -51,12 +51,11 @@ test.describe('OC - Livechat - Cross Tab Communication', () => { await pageLivechat1.onlineAgentMessage.fill('this_a_test_message_from_user'); await pageLivechat1.btnSendMessageToOnlineAgent.click(); - await expect(pageLivechat1.page.locator('div >> text="this_a_test_message_from_user"')).toBeVisible(); - - await expect(pageLivechat2.page.locator('div >> text="this_a_test_message_from_user"')).toBeVisible(); + await expect(pageLivechat1.txtChatMessage('this_a_test_message_from_user')).toBeVisible(); + await expect(pageLivechat2.txtChatMessage('this_a_test_message_from_user')).toBeVisible(); }); - await test.step('expect to restart a livechat conversation and tabs to be synced', async () => { + await test.step('expect to close livechat conversation', async () => { await expect(pageLivechat1.btnOptions).toBeVisible(); await pageLivechat1.btnOptions.click(); @@ -64,21 +63,33 @@ test.describe('OC - Livechat - Cross Tab Communication', () => { await pageLivechat1.btnCloseChat.click(); await pageLivechat1.btnCloseChatConfirm.click(); + }); + await test.step('expect to restart a livechat conversation and tabs to be synced', async () => { await expect(pageLivechat1.btnNewChat).toBeVisible(); await pageLivechat1.startNewChat(); await pageLivechat1.onlineAgentMessage.fill('this_a_test_message_from_user_after_close'); await pageLivechat1.btnSendMessageToOnlineAgent.click(); - await pageLivechat1.page.locator('div >> text="this_a_test_message_from_user"').waitFor({ state: 'hidden' }); - await pageLivechat2.page.locator('div >> text="this_a_test_message_from_user"').waitFor({ state: 'hidden' }); + await pageLivechat1.txtChatMessage('this_a_test_message_from_user').waitFor({ state: 'hidden' }); + await pageLivechat2.txtChatMessage('this_a_test_message_from_user').waitFor({ state: 'hidden' }); - await expect(pageLivechat1.page.locator('div >> text="this_a_test_message_from_user"')).not.toBeVisible(); - await expect(pageLivechat2.page.locator('div >> text="this_a_test_message_from_user"')).not.toBeVisible(); + await expect(pageLivechat1.txtChatMessage('this_a_test_message_from_user')).not.toBeVisible(); + await expect(pageLivechat2.txtChatMessage('this_a_test_message_from_user')).not.toBeVisible(); - await expect(pageLivechat1.page.locator('div >> text="this_a_test_message_from_user_after_close"')).toBeVisible(); - await expect(pageLivechat2.page.locator('div >> text="this_a_test_message_from_user_after_close"')).toBeVisible(); + await expect(pageLivechat1.txtChatMessage('this_a_test_message_from_user_after_close')).toBeVisible(); + await expect(pageLivechat2.txtChatMessage('this_a_test_message_from_user_after_close')).toBeVisible(); + }); + + await test.step('expect to close livechat conversation', async () => { + await expect(pageLivechat1.btnOptions).toBeVisible(); + await pageLivechat1.btnOptions.click(); + + await expect(pageLivechat1.btnCloseChat).toBeVisible(); + await pageLivechat1.btnCloseChat.click(); + + await pageLivechat1.btnCloseChatConfirm.click(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index b1b81aeaef3b..46f806104420 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -175,7 +175,7 @@ export class HomeSidenav { async createEncryptedChannel(name: string) { await this.openNewByLabel('Channel'); - await this.inputChannelName.type(name); + await this.inputChannelName.fill(name); await this.advancedSettingsAccordion.click(); await this.checkboxEncryption.click(); await this.btnCreate.click(); @@ -188,30 +188,4 @@ export class HomeSidenav { getSearchChannelBadge(name: string): Locator { return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`).first().getByRole('status', { exact: true }); } - - // New navigation selectors - - get sidebar(): Locator { - return this.page.getByRole('navigation', { name: 'sidebar' }); - } - - get sidebarSearchSection(): Locator { - return this.sidebar.getByRole('search'); - } - - get btnRecent(): Locator { - return this.sidebarSearchSection.getByRole('button', { name: 'Recent' }); - } - - get channelsList(): Locator { - return this.sidebar.getByRole('list', { name: 'Channels' }); - } - - getCollapseGroupByName(name: string): Locator { - return this.channelsList.getByRole('button', { name, exact: true }); - } - - get firstCollapser(): Locator { - return this.channelsList.getByRole('button').first(); - } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index 02f1769ba7ea..fc5ab5e62385 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -5,3 +5,4 @@ export * from './home-sidenav'; export * from './omnichannel-sidenav'; export * from './omnichannel-close-chat-modal'; export * from './navbar'; +export * from './sidebar'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts new file mode 100644 index 000000000000..b19fccd141a7 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -0,0 +1,85 @@ +import type { Locator, Page } from '@playwright/test'; + +import { expect } from '../../utils/test'; + +export class Sidebar { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + // New navigation locators + get sidebar(): Locator { + return this.page.getByRole('navigation', { name: 'sidebar' }); + } + + get sidebarSearchSection(): Locator { + return this.sidebar.getByRole('search'); + } + + get btnRecent(): Locator { + return this.sidebarSearchSection.getByRole('button', { name: 'Recent' }); + } + + get channelsList(): Locator { + return this.sidebar.getByRole('list', { name: 'Channels' }); + } + + get searchList(): Locator { + return this.sidebar.getByRole('search').getByRole('list', { name: 'Channels' }); + } + + get firstCollapser(): Locator { + return this.channelsList.getByRole('button').first(); + } + + get firstChannelFromList(): Locator { + return this.channelsList.getByRole('listitem').first(); + } + + get searchInput(): Locator { + return this.sidebarSearchSection.getByRole('searchbox'); + } + + async escSearch(): Promise { + await this.page.keyboard.press('Escape'); + } + + async waitForChannel(): Promise { + await this.page.locator('role=main').waitFor(); + await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + await this.page.locator('role=main >> role=list').waitFor(); + + await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); + } + + async typeSearch(name: string): Promise { + return this.searchInput.fill(name); + } + + getSearchRoomByName(name: string): Locator { + return this.searchList.getByRole('link', { name }); + } + + async openChat(name: string): Promise { + await this.typeSearch(name); + await this.getSearchRoomByName(name).click(); + await this.waitForChannel(); + } + + async markItemAsUnread(item: Locator): Promise { + await item.hover(); + await item.focus(); + await item.locator('.rcx-sidebar-item__menu').click(); + await this.page.getByRole('option', { name: 'Mark Unread' }).click(); + } + + getCollapseGroupByName(name: string): Locator { + return this.channelsList.getByRole('button', { name, exact: true }); + } + + getItemUnreadBadge(item: Locator): Locator { + return item.getByRole('status', { name: 'unread' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 15b471e15b4c..76929b6c2dcf 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab, Navbar } from './fragments'; +import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidebar } from './fragments'; export class HomeChannel { public readonly page: Page; @@ -9,6 +9,8 @@ export class HomeChannel { readonly sidenav: HomeSidenav; + readonly sidebar: Sidebar; + readonly navbar: Navbar; readonly tabs: HomeFlextab; @@ -17,6 +19,7 @@ export class HomeChannel { this.page = page; this.content = new HomeContent(page); this.sidenav = new HomeSidenav(page); + this.sidebar = new Sidebar(page); this.navbar = new Navbar(page); this.tabs = new HomeFlextab(page); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts index 0973b39ef89a..b8cf77d63f71 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts @@ -67,7 +67,7 @@ export class OmnichannelLiveChat { } txtChatMessage(message: string): Locator { - return this.page.locator(`text="${message}"`); + return this.page.locator(`[data-qa="message-bubble"] >> text="${message}"`); } async closeChat(): Promise { diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js index 43ff281c6472..e38a53a7db6a 100644 --- a/packages/livechat/src/routes/Chat/container.js +++ b/packages/livechat/src/routes/Chat/container.js @@ -243,9 +243,11 @@ class ChatContainer extends Component { await dispatch({ loading: true }); try { - if (rid) { - await Livechat.closeChat({ rid }); + if (!rid) { + throw new Error('error-room-not-found'); } + + await Livechat.closeChat({ rid }); } catch (error) { console.error(error); const alert = { id: createToken(), children: i18n.t('error_closing_chat'), error: true, timeout: 0 }; @@ -289,7 +291,7 @@ class ChatContainer extends Component { canFinishChat = () => { const { room, connecting, visitorsCanCloseChat } = this.props; - return visitorsCanCloseChat && (room !== undefined || connecting); + return visitorsCanCloseChat && (room?._id !== undefined || connecting); }; canRemoveUserData = () => { diff --git a/yarn.lock b/yarn.lock index eae040c86b08..fbca2be4532a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9399,7 +9399,7 @@ __metadata: colorette: "npm:^2.0.20" colors: "npm:^1.4.0" connect: "npm:^3.7.0" - cookie: "npm:^0.5.0" + cookie: "npm:^0.7.0" cookie-parser: "npm:^1.4.7" cors: "npm:^2.8.5" cron: "npm:~1.8.2" @@ -9529,7 +9529,7 @@ __metadata: react-error-boundary: "npm:^3.1.4" react-hook-form: "npm:~7.45.4" react-i18next: "npm:~13.2.2" - react-keyed-flatten-children: "npm:^1.3.0" + react-keyed-flatten-children: "npm:^3.0.2" react-virtuoso: "npm:^4.12.0" sanitize-html: "npm:^2.13.1" semver: "npm:^7.6.3" @@ -16108,11 +16108,11 @@ __metadata: linkType: hard "braces@npm:^3.0.2, braces@npm:~3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: "npm:^7.0.1" - checksum: 10/966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 + fill-range: "npm:^7.1.1" + checksum: 10/fad11a0d4697a27162840b02b1fad249c1683cbc510cd5bf1a471f2f8085c046d41094308c577a50a03a579dd99d5a6b3724c4b5e8b14df2c4443844cfcda2c6 languageName: node linkType: hard @@ -17819,20 +17819,13 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.7.2": +"cookie@npm:0.7.2, cookie@npm:^0.7.0": version: 0.7.2 resolution: "cookie@npm:0.7.2" checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f languageName: node linkType: hard -"cookie@npm:^0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 10/aae7911ddc5f444a9025fbd979ad1b5d60191011339bce48e555cb83343d0f98b865ff5c4d71fecdfb8555a5cafdc65632f6fce172f32aaf6936830a883a0380 - languageName: node - linkType: hard - "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -21716,12 +21709,12 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: "npm:^5.0.1" - checksum: 10/e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 + checksum: 10/a7095cb39e5bc32fada2aa7c7249d3f6b01bd1ce461a61b0adabacccabd9198500c6fb1f68a7c851a657e273fce2233ba869638897f3d7ed2e87a2d89b4436ea languageName: node linkType: hard @@ -23457,8 +23450,8 @@ __metadata: linkType: hard "http-proxy-middleware@npm:^2.0.3": - version: 2.0.6 - resolution: "http-proxy-middleware@npm:2.0.6" + version: 2.0.7 + resolution: "http-proxy-middleware@npm:2.0.7" dependencies: "@types/http-proxy": "npm:^1.17.8" http-proxy: "npm:^1.18.1" @@ -23470,7 +23463,7 @@ __metadata: peerDependenciesMeta: "@types/express": optional: true - checksum: 10/768e7ae5a422bbf4b866b64105b4c2d1f468916b7b0e9c96750551c7732383069b411aa7753eb7b34eab113e4f77fb770122cb7fb9c8ec87d138d5ddaafda891 + checksum: 10/4a51bf612b752ad945701995c1c029e9501c97e7224c0cf3f8bf6d48d172d6a8f2b57c20fec469534fdcac3aa8a6f332224a33c6b0d7f387aa2cfff9b67216fd languageName: node linkType: hard @@ -31940,6 +31933,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.2.0": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 + languageName: node + linkType: hard + "react-keyed-flatten-children@npm:^1.3.0": version: 1.3.0 resolution: "react-keyed-flatten-children@npm:1.3.0" @@ -31951,6 +31951,17 @@ __metadata: languageName: node linkType: hard +"react-keyed-flatten-children@npm:^3.0.2": + version: 3.0.2 + resolution: "react-keyed-flatten-children@npm:3.0.2" + dependencies: + react-is: "npm:^18.2.0" + peerDependencies: + react: ">=15.0.0" + checksum: 10/58b53ccc707543b3b2f3615a6efe890b1302bf38d68df9239cb4d1ca4e8d8d9c05d670039c08e77befd27b1210c61290d6308a2f8439363e6699fecb94b69ebd + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" @@ -32964,7 +32975,7 @@ __metadata: bcrypt: "npm:^5.1.1" body-parser: "npm:^1.20.3" colorette: "npm:^2.0.20" - cookie: "npm:^0.5.0" + cookie: "npm:^0.7.0" cookie-parser: "npm:^1.4.7" ejson: "npm:^2.2.3" eventemitter3: "npm:^5.0.1"