diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 68da2414ea4..8e4a9ee5756 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -116,7 +116,7 @@ limitations under the License. } .mx_RoomPreviewBar_actions { - flex-direction: column-reverse; + flex-direction: column; .mx_AccessibleButton { padding: 7px 50px; //extra wide } diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index 7ff7b6416df..4e3c7df0af3 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -593,11 +593,26 @@ export default class RoomPreviewBar extends React.Component { ); } + const isPanel = this.props.canPreview; + const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, { - "mx_RoomPreviewBar_panel": this.props.canPreview, - "mx_RoomPreviewBar_dialog": !this.props.canPreview, + "mx_RoomPreviewBar_panel": isPanel, + "mx_RoomPreviewBar_dialog": !isPanel, }); + // ensure correct tab order for both views + const actions = isPanel + ? <> + { secondaryButton } + { extraComponents } + { primaryButton } + + : <> + { primaryButton } + { extraComponents } + { secondaryButton } + ; + return (
@@ -606,9 +621,7 @@ export default class RoomPreviewBar extends React.Component {
{ reasonElement }
- { secondaryButton } - { extraComponents } - { primaryButton } + { actions }
{ footer } diff --git a/test/components/views/rooms/RoomPreviewBar-test.tsx b/test/components/views/rooms/RoomPreviewBar-test.tsx new file mode 100644 index 00000000000..98cd47102e9 --- /dev/null +++ b/test/components/views/rooms/RoomPreviewBar-test.tsx @@ -0,0 +1,389 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { + renderIntoDocument, + Simulate, + findRenderedDOMComponentWithClass, + act, +} from 'react-dom/test-utils'; + +import "../../../skinned-sdk"; + +import { stubClient } from '../../../test-utils'; + +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; + +import DMRoomMap from '../../../../src/utils/DMRoomMap'; + +import { Room, RoomMember, MatrixError, IContent } from 'matrix-js-sdk'; + +import RoomPreviewBar from '../../../../src/components/views/rooms/RoomPreviewBar'; + +jest.mock('../../../../src/IdentityAuthClient', () => { + return jest.fn().mockImplementation(() => { + return { getAccessToken: jest.fn().mockResolvedValue('mock-token') }; + }); +}); + +jest.useFakeTimers(); + +const createRoom = (roomId: string, userId: string): Room => { + const newRoom = new Room( + roomId, + MatrixClientPeg.get(), + userId, + {}, + ); + DMRoomMap.makeShared().start(); + return newRoom; +}; + +const makeMockRoomMember = ( + { userId, isKicked, membership, content, memberContent }: + {userId?: string; + isKicked?: boolean; + membership?: 'invite' | 'ban'; + content?: Partial; + memberContent?: Partial; + }, +) => ({ + userId, + rawDisplayName: `${userId} name`, + isKicked: jest.fn().mockReturnValue(!!isKicked), + getContent: jest.fn().mockReturnValue(content || {}), + membership, + events: { + member: { + getSender: jest.fn().mockReturnValue('@kicker:test.com'), + getContent: jest.fn().mockReturnValue({ reason: 'test reason', ...memberContent }), + }, + }, +}) as unknown as RoomMember; + +describe('', () => { + const roomId = 'RoomPreviewBar-test-room'; + const userId = '@tester:test.com'; + const inviterUserId = '@inviter:test.com'; + const otherUserId = '@othertester:test.com'; + + const getComponent = (props = {}) => { + const defaultProps = { + room: createRoom(roomId, userId), + }; + const wrapper = renderIntoDocument( + , + ) as React.Component; + return findRenderedDOMComponentWithClass(wrapper, 'mx_RoomPreviewBar') as HTMLDivElement; + }; + + const isSpinnerRendered = (element: Element) => !!element.querySelector('.mx_Spinner'); + const getMessage = (element: Element) => element.querySelector('.mx_RoomPreviewBar_message'); + const getActions = (element: Element) => element.querySelector('.mx_RoomPreviewBar_actions'); + const getPrimaryActionButton = (element: Element) => + getActions(element).querySelector('.mx_AccessibleButton_kind_primary'); + const getSecondaryActionButton = (element: Element) => + getActions(element).querySelector('.mx_AccessibleButton_kind_secondary'); + + beforeEach(() => { + stubClient(); + MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue(userId); + }); + + afterEach(() => { + const container = document.body.firstChild; + container && document.body.removeChild(container); + }); + + it('renders joining message', () => { + const component = getComponent({ joining: true }); + + expect(isSpinnerRendered(component)).toBeTruthy(); + expect(getMessage(component).textContent).toEqual('Joining room …'); + }); + it('renders rejecting message', () => { + const component = getComponent({ rejecting: true }); + expect(isSpinnerRendered(component)).toBeTruthy(); + expect(getMessage(component).textContent).toEqual('Rejecting invite …'); + }); + it('renders loading message', () => { + const component = getComponent({ loading: true }); + expect(isSpinnerRendered(component)).toBeTruthy(); + expect(getMessage(component).textContent).toEqual('Loading …'); + }); + + it('renders not logged in message', () => { + MatrixClientPeg.get().isGuest = jest.fn().mockReturnValue(true); + const component = getComponent({ loading: true }); + + expect(isSpinnerRendered(component)).toBeFalsy(); + expect(getMessage(component).textContent).toEqual('Join the conversation with an account'); + }); + + it('renders kicked message', () => { + const room = createRoom(roomId, otherUserId); + jest.spyOn(room, 'getMember').mockReturnValue(makeMockRoomMember({ isKicked: true })); + const component = getComponent({ loading: true, room }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders banned message', () => { + const room = createRoom(roomId, otherUserId); + jest.spyOn(room, 'getMember').mockReturnValue(makeMockRoomMember({ membership: 'ban' })); + const component = getComponent({ loading: true, room }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + describe('with an error', () => { + it('renders room not found error', () => { + const error = new MatrixError({ + errcode: 'M_NOT_FOUND', + error: "Room not found", + }); + const component = getComponent({ error }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + it('renders other errors', () => { + const error = new MatrixError({ + errcode: 'Something_else', + }); + const component = getComponent({ error }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + }); + + it('renders viewing room message when room an be previewed', () => { + const component = getComponent({ canPreview: true }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders viewing room message when room can not be previewed', () => { + const component = getComponent({ canPreview: false }); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + describe('with an invite', () => { + const inviterName = inviterUserId; + const userMember = makeMockRoomMember({ userId }); + const userMemberWithDmInvite = makeMockRoomMember({ + userId, membership: 'invite', memberContent: { is_direct: true }, + }); + const inviterMember = makeMockRoomMember({ + userId: inviterUserId, + content: { + "reason": 'test', + 'io.element.html_reason': '

hello

', + }, + }); + describe('without an invited email', () => { + describe('for a non-dm room', () => { + const mockGetMember = (id) => { + if (id === userId) return userMember; + return inviterMember; + }; + const onJoinClick = jest.fn(); + const onRejectClick = jest.fn(); + let room; + + beforeEach(() => { + room = createRoom(roomId, userId); + jest.spyOn(room, 'getMember').mockImplementation(mockGetMember); + jest.spyOn(room.currentState, 'getMember').mockImplementation(mockGetMember); + onJoinClick.mockClear(); + onRejectClick.mockClear(); + }); + + it('renders invite message', () => { + const component = getComponent({ inviterName, room }); + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders join and reject action buttons correctly', () => { + const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); + expect(getActions(component)).toMatchSnapshot(); + }); + + it('renders reject and ignore action buttons when handler is provided', () => { + const onRejectAndIgnoreClick = jest.fn(); + const component = getComponent({ + inviterName, room, onJoinClick, onRejectClick, onRejectAndIgnoreClick, + }); + expect(getActions(component)).toMatchSnapshot(); + }); + + it('renders join and reject action buttons in reverse order when room can previewed', () => { + // when room is previewed action buttons are rendered left to right, with primary on the right + const component = getComponent({ inviterName, room, onJoinClick, onRejectClick, canPreview: true }); + expect(getActions(component)).toMatchSnapshot(); + }); + + it('joins room on primary button click', () => { + const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); + act(() => { + Simulate.click(getPrimaryActionButton(component)); + }); + + expect(onJoinClick).toHaveBeenCalled(); + }); + + it('rejects invite on secondary button click', () => { + const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); + act(() => { + Simulate.click(getSecondaryActionButton(component)); + }); + + expect(onRejectClick).toHaveBeenCalled(); + }); + }); + + describe('for a dm room', () => { + const mockGetMember = (id) => { + if (id === userId) return userMemberWithDmInvite; + return inviterMember; + }; + const onJoinClick = jest.fn(); + const onRejectClick = jest.fn(); + let room; + + beforeEach(() => { + room = createRoom(roomId, userId); + jest.spyOn(room, 'getMember').mockImplementation(mockGetMember); + jest.spyOn(room.currentState, 'getMember').mockImplementation(mockGetMember); + onJoinClick.mockClear(); + onRejectClick.mockClear(); + }); + + it('renders invite message to a non-dm room', () => { + const component = getComponent({ inviterName, room }); + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders join and reject action buttons with correct labels', () => { + const onRejectAndIgnoreClick = jest.fn(); + const component = getComponent({ + inviterName, room, onJoinClick, onRejectAndIgnoreClick, onRejectClick, + }); + expect(getActions(component)).toMatchSnapshot(); + }); + }); + }); + + describe('with an invited email', () => { + const invitedEmail = 'test@test.com'; + const mockThreePids = [ + { medium: 'email', address: invitedEmail }, + { medium: 'not-email', address: 'address 2' }, + ]; + + const testJoinButton = (props) => async () => { + const onJoinClick = jest.fn(); + const onRejectClick = jest.fn(); + const component = getComponent({ ...props, onJoinClick, onRejectClick }); + await new Promise(setImmediate); + expect(getPrimaryActionButton(component)).toBeTruthy(); + expect(getSecondaryActionButton(component)).toBeFalsy(); + act(() => { + Simulate.click(getPrimaryActionButton(component)); + }); + expect(onJoinClick).toHaveBeenCalled(); + }; + + describe('when client fails to get 3PIDs', () => { + beforeEach(() => { + MatrixClientPeg.get().getThreePids = jest.fn().mockRejectedValue({ errCode: 'TEST_ERROR' }); + }); + + it('renders error message', async () => { + const component = getComponent({ inviterName, invitedEmail }); + await new Promise(setImmediate); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders join button', testJoinButton({ inviterName, invitedEmail })); + }); + + describe('when invitedEmail is not associated with current account', () => { + beforeEach(() => { + MatrixClientPeg.get().getThreePids = jest.fn().mockResolvedValue( + { threepids: mockThreePids.slice(1) }, + ); + }); + + it('renders invite message with invited email', async () => { + const component = getComponent({ inviterName, invitedEmail }); + await new Promise(setImmediate); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders join button', testJoinButton({ inviterName, invitedEmail })); + }); + + describe('when client has no identity server connected', () => { + beforeEach(() => { + MatrixClientPeg.get().getThreePids = jest.fn().mockResolvedValue({ threepids: mockThreePids }); + MatrixClientPeg.get().getIdentityServerUrl = jest.fn().mockReturnValue(false); + }); + + it('renders invite message with invited email', async () => { + const component = getComponent({ inviterName, invitedEmail }); + await new Promise(setImmediate); + + expect(getMessage(component)).toMatchSnapshot(); + }); + + it('renders join button', testJoinButton({ inviterName, invitedEmail })); + }); + + describe('when client has an identity server connected', () => { + beforeEach(() => { + MatrixClientPeg.get().getThreePids = jest.fn().mockResolvedValue({ threepids: mockThreePids }); + MatrixClientPeg.get().getIdentityServerUrl = jest.fn().mockReturnValue('identity.test'); + MatrixClientPeg.get().lookupThreePid = jest.fn().mockResolvedValue('identity.test'); + }); + + it('renders email mismatch message when invite email mxid doesnt match', async () => { + MatrixClientPeg.get().lookupThreePid = jest.fn().mockReturnValue('not userid'); + const component = getComponent({ inviterName, invitedEmail }); + await new Promise(setImmediate); + + expect(getMessage(component)).toMatchSnapshot(); + expect(MatrixClientPeg.get().lookupThreePid).toHaveBeenCalledWith( + 'email', invitedEmail, undefined, 'mock-token', + ); + await testJoinButton({ inviterName, invitedEmail })(); + }); + + it('renders invite message when invite email mxid match', async () => { + MatrixClientPeg.get().lookupThreePid = jest.fn().mockReturnValue(userId); + const component = getComponent({ inviterName, invitedEmail }); + await new Promise(setImmediate); + + expect(getMessage(component)).toMatchSnapshot(); + await testJoinButton({ inviterName, invitedEmail })(); + }); + }); + }); + }); +}); diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap new file mode 100644 index 00000000000..0c3a335241e --- /dev/null +++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders banned message 1`] = ` +
+

+ You were banned from RoomPreviewBar-test-room by @kicker:test.com +

+

+ Reason: test reason +

+
+`; + +exports[` renders kicked message 1`] = ` +
+

+ You were kicked from RoomPreviewBar-test-room by @kicker:test.com +

+

+ Reason: test reason +

+
+`; + +exports[` renders viewing room message when room an be previewed 1`] = ` +
+

+ You're previewing RoomPreviewBar-test-room. Want to join it? +

+
+`; + +exports[` renders viewing room message when room can not be previewed 1`] = ` +
+

+ RoomPreviewBar-test-room can't be previewed. Do you want to join it? +

+
+`; + +exports[` with an error renders other errors 1`] = ` +
+

+ RoomPreviewBar-test-room is not accessible at this time. +

+

+ Try again later, or ask a room admin to check if you have access. +

+

+ + Something_else was returned while trying to access the room. If you think you're seeing this message in error, please + + submit a bug report + + . + +

+
+`; + +exports[` with an error renders room not found error 1`] = ` +
+

+ RoomPreviewBar-test-room does not exist. +

+

+ This room doesn't exist. Are you sure you're at the right place? +

+
+`; + +exports[` with an invite with an invited email when client fails to get 3PIDs renders error message 1`] = ` +
+

+ Something went wrong with your invite to RoomPreviewBar-test-room +

+

+ An error (unknown error code) was returned while trying to validate your invite. You could try to pass this information on to a room admin. +

+
+`; + +exports[` with an invite with an invited email when client has an identity server connected renders email mismatch message when invite email mxid doesnt match 1`] = ` +
+

+ This invite to RoomPreviewBar-test-room was sent to test@test.com +

+

+ Share this email in Settings to receive invites directly in . +

+
+`; + +exports[` with an invite with an invited email when client has an identity server connected renders invite message when invite email mxid match 1`] = ` +
+

+ This invite to RoomPreviewBar-test-room was sent to test@test.com +

+

+ Share this email in Settings to receive invites directly in . +

+
+`; + +exports[` with an invite with an invited email when client has no identity server connected renders invite message with invited email 1`] = ` +
+

+ This invite to RoomPreviewBar-test-room was sent to test@test.com +

+

+ Use an identity server in Settings to receive invites directly in . +

+
+`; + +exports[` with an invite with an invited email when invitedEmail is not associated with current account renders invite message with invited email 1`] = ` +
+

+ This invite to RoomPreviewBar-test-room was sent to test@test.com which is not associated with your account +

+

+ Link this email with your account in Settings to receive invites directly in . +

+
+`; + +exports[` with an invite without an invited email for a dm room renders invite message to a non-dm room 1`] = ` +
+

+ Do you want to join RoomPreviewBar-test-room? +

+

+ + + + +

+

+ + + + @inviter:test.com name + + ( + @inviter:test.com + ) + + invited you + +

+
+`; + +exports[` with an invite without an invited email for a dm room renders join and reject action buttons with correct labels 1`] = ` +
+
+ Accept +
+
+ Reject & Ignore user +
+
+ Reject +
+
+`; + +exports[` with an invite without an invited email for a non-dm room renders invite message 1`] = ` +
+

+ Do you want to join RoomPreviewBar-test-room? +

+

+ + + + +

+

+ + + + @inviter:test.com name + + ( + @inviter:test.com + ) + + invited you + +

+
+`; + +exports[` with an invite without an invited email for a non-dm room renders join and reject action buttons correctly 1`] = ` +
+
+ Accept +
+
+ Reject +
+
+`; + +exports[` with an invite without an invited email for a non-dm room renders join and reject action buttons in reverse order when room can previewed 1`] = ` +
+
+ Reject +
+
+ Accept +
+
+`; + +exports[` with an invite without an invited email for a non-dm room renders reject and ignore action buttons when handler is provided 1`] = ` +
+
+ Accept +
+
+ Reject & Ignore user +
+
+ Reject +
+
+`; diff --git a/test/test-utils.js b/test/test-utils.js index d43a08ab3a0..9efa8b6098d 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -104,6 +104,10 @@ export function createTestClient() { getCapabilities: jest.fn().mockResolvedValue({}), supportsExperimentalThreads: () => false, getRoomUpgradeHistory: jest.fn().mockReturnValue([]), + getOpenIdToken: jest.fn().mockResolvedValue(), + registerWithIdentityServer: jest.fn().mockResolvedValue({}), + getIdentityAccount: jest.fn().mockResolvedValue({}), + getTerms: jest.fn().mockResolvedValueOnce(), }; }