Correct tab order in room preview dialog (#7302)

* tab order

* lint fixes

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test RoomPreviewBar

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add copyright header

Signed-off-by: Kerry Archibald <kerrya@element.io>

* Update src/components/views/rooms/RoomPreviewBar.tsx

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* fix tabs/spaces lint for reformatted actions

Signed-off-by: Kerry Archibald <kerrya@element.io>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Kerry 2021-12-09 11:48:58 +01:00 committed by GitHub
parent 0c850b2f13
commit d7a6e3ec65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 753 additions and 6 deletions

View file

@ -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
}

View file

@ -593,11 +593,26 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
);
}
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 (
<div className={classes}>
<div className="mx_RoomPreviewBar_message">
@ -606,9 +621,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
</div>
{ reasonElement }
<div className="mx_RoomPreviewBar_actions">
{ secondaryButton }
{ extraComponents }
{ primaryButton }
{ actions }
</div>
<div className="mx_RoomPreviewBar_footer">
{ footer }

View file

@ -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<IContent>;
memberContent?: Partial<IContent>;
},
) => ({
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('<RoomPreviewBar />', () => {
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<React.Component>(
<RoomPreviewBar {...defaultProps} {...props} />,
) as React.Component;
return findRenderedDOMComponentWithClass(wrapper, 'mx_RoomPreviewBar') as HTMLDivElement;
};
const isSpinnerRendered = (element: Element) => !!element.querySelector('.mx_Spinner');
const getMessage = (element: Element) => element.querySelector<HTMLDivElement>('.mx_RoomPreviewBar_message');
const getActions = (element: Element) => element.querySelector<HTMLDivElement>('.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': '<h3>hello</h3>',
},
});
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 })();
});
});
});
});
});

View file

@ -0,0 +1,341 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomPreviewBar /> renders banned message 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
You were banned from RoomPreviewBar-test-room by @kicker:test.com
</h3>
<p>
Reason: test reason
</p>
</div>
`;
exports[`<RoomPreviewBar /> renders kicked message 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
You were kicked from RoomPreviewBar-test-room by @kicker:test.com
</h3>
<p>
Reason: test reason
</p>
</div>
`;
exports[`<RoomPreviewBar /> renders viewing room message when room an be previewed 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
You're previewing RoomPreviewBar-test-room. Want to join it?
</h3>
</div>
`;
exports[`<RoomPreviewBar /> renders viewing room message when room can not be previewed 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
RoomPreviewBar-test-room can't be previewed. Do you want to join it?
</h3>
</div>
`;
exports[`<RoomPreviewBar /> with an error renders other errors 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
RoomPreviewBar-test-room is not accessible at this time.
</h3>
<p>
Try again later, or ask a room admin to check if you have access.
</p>
<p>
<span>
Something_else was returned while trying to access the room. If you think you're seeing this message in error, please
<a
href="https://github.com/vector-im/element-web/issues/new/choose"
rel="noreferrer noopener"
target="_blank"
>
submit a bug report
</a>
.
</span>
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an error renders room not found error 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
RoomPreviewBar-test-room does not exist.
</h3>
<p>
This room doesn't exist. Are you sure you're at the right place?
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite with an invited email when client fails to get 3PIDs renders error message 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
Something went wrong with your invite to RoomPreviewBar-test-room
</h3>
<p>
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.
</p>
</div>
`;
exports[`<RoomPreviewBar /> 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`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
This invite to RoomPreviewBar-test-room was sent to test@test.com
</h3>
<p>
Share this email in Settings to receive invites directly in .
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite with an invited email when client has an identity server connected renders invite message when invite email mxid match 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
This invite to RoomPreviewBar-test-room was sent to test@test.com
</h3>
<p>
Share this email in Settings to receive invites directly in .
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite with an invited email when client has no identity server connected renders invite message with invited email 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
This invite to RoomPreviewBar-test-room was sent to test@test.com
</h3>
<p>
Use an identity server in Settings to receive invites directly in .
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite with an invited email when invitedEmail is not associated with current account renders invite message with invited email 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
This invite to RoomPreviewBar-test-room was sent to test@test.com which is not associated with your account
</h3>
<p>
Link this email with your account in Settings to receive invites directly in .
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a dm room renders invite message to a non-dm room 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
Do you want to join RoomPreviewBar-test-room?
</h3>
<p>
<span
class="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
>
R
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span>
</p>
<p>
<span>
<span>
<span
class="mx_RoomPreviewBar_inviter"
>
@inviter:test.com name
</span>
(
@inviter:test.com
)
</span>
invited you
</span>
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a dm room renders join and reject action buttons with correct labels 1`] = `
<div
class="mx_RoomPreviewBar_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject & Ignore user
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject
</div>
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a non-dm room renders invite message 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
Do you want to join RoomPreviewBar-test-room?
</h3>
<p>
<span
class="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
>
R
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span>
</p>
<p>
<span>
<span>
<span
class="mx_RoomPreviewBar_inviter"
>
@inviter:test.com name
</span>
(
@inviter:test.com
)
</span>
invited you
</span>
</p>
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a non-dm room renders join and reject action buttons correctly 1`] = `
<div
class="mx_RoomPreviewBar_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject
</div>
</div>
`;
exports[`<RoomPreviewBar /> 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`] = `
<div
class="mx_RoomPreviewBar_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a non-dm room renders reject and ignore action buttons when handler is provided 1`] = `
<div
class="mx_RoomPreviewBar_actions"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Accept
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject & Ignore user
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Reject
</div>
</div>
`;

View file

@ -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(),
};
}