ARIA Accessibility improvements (#10675)

* Fix confusing tab indexes in EventTilePreview

* Stop using headings inside buttons

* Prefer labelledby and describedby over duplicated aria-labels

* Improve semantics of tables used in settings

* Fix types

* Update tests

* Fix timestamps
This commit is contained in:
Michael Telatynski 2023-04-21 10:48:48 +01:00 committed by GitHub
parent 259b5fe253
commit 792a39a39b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 197 additions and 137 deletions

View file

@ -23,15 +23,14 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
border: 1px solid $input-border-color; border: 1px solid $input-border-color;
font-size: $font-15px; font-size: $font-17px;
font-weight: $font-semi-bold;
margin: 20px 0; margin: 20px 0;
> h3 { > div {
font-weight: $font-semi-bold; margin-top: 4px;
margin: 0 0 4px; font-weight: normal;
} font-size: $font-15px;
> span {
color: $secondary-content; color: $secondary-content;
} }

View file

@ -17,7 +17,12 @@ limitations under the License.
.mx_CrossSigningPanel_statusList { .mx_CrossSigningPanel_statusList {
border-spacing: 0; border-spacing: 0;
td { th {
text-align: start;
}
td,
th {
padding: 0; padding: 0;
&:first-of-type { &:first-of-type {

View file

@ -1,3 +1,19 @@
/*
Copyright 2023 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.
*/
.mx_CryptographyPanel_sessionInfo { .mx_CryptographyPanel_sessionInfo {
padding: 0em; padding: 0em;
border-spacing: 0px; border-spacing: 0px;
@ -5,13 +21,15 @@
.mx_CryptographyPanel_sessionInfo > tr { .mx_CryptographyPanel_sessionInfo > tr {
vertical-align: baseline; vertical-align: baseline;
padding: 0em; padding: 0em;
}
.mx_CryptographyPanel_sessionInfo > tr > td { th {
padding-bottom: 0em; text-align: start;
padding-left: 0em; }
padding-right: 1em;
padding-top: 0em; td,
th {
padding: 0 1em 0 0;
}
} }
.mx_CryptographyPanel_importExportButtons .mx_AccessibleButton { .mx_CryptographyPanel_importExportButtons .mx_AccessibleButton {

View file

@ -50,7 +50,12 @@ limitations under the License.
.mx_SecureBackupPanel_statusList { .mx_SecureBackupPanel_statusList {
border-spacing: 0; border-spacing: 0;
td { th {
text-align: start;
}
td,
th {
padding: 0; padding: 0;
&:first-of-type { &:first-of-type {

View file

@ -476,7 +476,7 @@ const SpaceSetupPrivateScope: React.FC<{
onFinished(false); onFinished(false);
}} }}
> >
<h3>{_t("Just me")}</h3> {_t("Just me")}
<div>{_t("A private space to organise your rooms")}</div> <div>{_t("A private space to organise your rooms")}</div>
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
@ -485,7 +485,7 @@ const SpaceSetupPrivateScope: React.FC<{
onFinished(true); onFinished(true);
}} }}
> >
<h3>{_t("Me and my teammates")}</h3> {_t("Me and my teammates")}
<div>{_t("A private space for you and your teammates")}</div> <div>{_t("A private space for you and your teammates")}</div>
</AccessibleButton> </AccessibleButton>
</div> </div>

View file

@ -128,8 +128,8 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
const event = this.fakeEvent(this.state); const event = this.fakeEvent(this.state);
return ( return (
<div className={className}> <div className={className} role="presentation">
<EventTile mxEvent={event} layout={this.props.layout} as="div" /> <EventTile mxEvent={event} layout={this.props.layout} as="div" hideTimestamp inhibitInteraction />
</div> </div>
); );
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { randomString } from "matrix-js-sdk/src/randomstring";
import ToggleSwitch from "./ToggleSwitch"; import ToggleSwitch from "./ToggleSwitch";
import { Caption } from "../typography/Caption"; import { Caption } from "../typography/Caption";
@ -43,18 +44,15 @@ interface IProps {
} }
export default class LabelledToggleSwitch extends React.PureComponent<IProps> { export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`;
public render(): React.ReactNode { public render(): React.ReactNode {
// This is a minimal version of a SettingsFlag // This is a minimal version of a SettingsFlag
const { label, caption } = this.props; const { label, caption } = this.props;
let firstPart = ( let firstPart = (
<span className="mx_SettingsFlag_label"> <span className="mx_SettingsFlag_label">
{label} <div id={this.id}>{label}</div>
{caption && ( {caption && <Caption id={`${this.id}_caption`}>{caption}</Caption>}
<>
<br />
<Caption>{caption}</Caption>
</>
)}
</span> </span>
); );
let secondPart = ( let secondPart = (
@ -62,15 +60,14 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
checked={this.props.value} checked={this.props.value}
disabled={this.props.disabled} disabled={this.props.disabled}
onChange={this.props.onChange} onChange={this.props.onChange}
title={this.props.label}
tooltip={this.props.tooltip} tooltip={this.props.tooltip}
aria-labelledby={this.id}
aria-describedby={caption ? `${this.id}_caption` : undefined}
/> />
); );
if (this.props.toggleInFront) { if (this.props.toggleInFront) {
const temp = firstPart; [firstPart, secondPart] = [secondPart, firstPart];
firstPart = secondPart;
secondPart = temp;
} }
const classes = classNames("mx_SettingsFlag", this.props.className, { const classes = classNames("mx_SettingsFlag", this.props.className, {

View file

@ -41,7 +41,7 @@ interface IProps {
} }
// Controlled Toggle Switch element, written with Accessibility in mind // Controlled Toggle Switch element, written with Accessibility in mind
export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps): JSX.Element => { export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => {
const _onClick = (): void => { const _onClick = (): void => {
if (disabled) return; if (disabled) return;
onChange(!checked); onChange(!checked);
@ -61,8 +61,6 @@ export default ({ checked, disabled = false, title, tooltip, onChange, ...props
role="switch" role="switch"
aria-checked={checked} aria-checked={checked}
aria-disabled={disabled} aria-disabled={disabled}
title={title}
tooltip={tooltip}
> >
<div className="mx_ToggleSwitch_ball" /> <div className="mx_ToggleSwitch_ball" />
</AccessibleTooltipButton> </AccessibleTooltipButton>

View file

@ -218,6 +218,10 @@ export interface EventTileProps {
// displayed to the current user either because they're // displayed to the current user either because they're
// the author or they are a moderator // the author or they are a moderator
isSeeingThroughMessageHiddenForModeration?: boolean; isSeeingThroughMessageHiddenForModeration?: boolean;
// The following properties are used by EventTilePreview to disable tab indexes within the event tile
hideTimestamp?: boolean;
inhibitInteraction?: boolean;
} }
interface IState { interface IState {
@ -1006,7 +1010,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
} }
if (this.props.mxEvent.sender && avatarSize) { if (this.props.mxEvent.sender && avatarSize) {
let member; let member: RoomMember | null = null;
// set member to receiver (target) if it is a 3PID invite // set member to receiver (target) if it is a 3PID invite
// so that the correct avatar is shown as the text is // so that the correct avatar is shown as the text is
// `$target accepted the invitation for $email` // `$target accepted the invitation for $email`
@ -1016,9 +1020,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
member = this.props.mxEvent.sender; member = this.props.mxEvent.sender;
} }
// In the ThreadsList view we use the entire EventTile as a click target to open the thread instead // In the ThreadsList view we use the entire EventTile as a click target to open the thread instead
const viewUserOnClick = ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes( const viewUserOnClick =
this.context.timelineRenderingType, !this.props.inhibitInteraction &&
); ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
this.context.timelineRenderingType,
);
avatar = ( avatar = (
<div className="mx_EventTile_avatar"> <div className="mx_EventTile_avatar">
<MemberAvatar <MemberAvatar
@ -1064,6 +1070,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
const showTimestamp = const showTimestamp =
this.props.mxEvent.getTs() && this.props.mxEvent.getTs() &&
!this.props.hideTimestamp &&
(this.props.alwaysShowTimestamps || (this.props.alwaysShowTimestamps ||
this.props.last || this.props.last ||
this.state.hover || this.state.hover ||
@ -1101,7 +1108,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
); );
} }
const linkedTimestamp = ( const linkedTimestamp = !this.props.hideTimestamp ? (
<a <a
href={permalink} href={permalink}
onClick={this.onPermalinkClicked} onClick={this.onPermalinkClicked}
@ -1110,7 +1117,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
> >
{timestamp} {timestamp}
</a> </a>
); ) : null;
const useIRCLayout = this.props.layout === Layout.IRC; const useIRCLayout = this.props.layout === Layout.IRC;
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;

View file

@ -243,36 +243,34 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{_t("Advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList"> <table className="mx_CrossSigningPanel_statusList">
<tbody> <tr>
<tr> <th scope="row">{_t("Cross-signing public keys:")}</th>
<td>{_t("Cross-signing public keys:")}</td> <td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Cross-signing private keys:")}</th>
<td>{_t("Cross-signing private keys:")}</td> <td>
<td> {crossSigningPrivateKeysInStorage
{crossSigningPrivateKeysInStorage ? _t("in secret storage")
? _t("in secret storage") : _t("not found in storage")}
: _t("not found in storage")} </td>
</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Master private key:")}</th>
<td>{_t("Master private key:")}</td> <td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Self signing private key:")}</th>
<td>{_t("Self signing private key:")}</td> <td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("User signing private key:")}</th>
<td>{_t("User signing private key:")}</td> <td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Homeserver feature support:")}</th>
<td>{_t("Homeserver feature support:")}</td> <td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td> </tr>
</tr>
</tbody>
</table> </table>
</details> </details>
{errorSection} {errorSection}

View file

@ -75,22 +75,20 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
<div className="mx_SettingsTab_section mx_CryptographyPanel"> <div className="mx_SettingsTab_section mx_CryptographyPanel">
<span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span> <span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span>
<table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo"> <table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo">
<tbody> <tr>
<tr> <th scope="row">{_t("Session ID:")}</th>
<td>{_t("Session ID:")}</td> <td>
<td> <code>{deviceId}</code>
<code>{deviceId}</code> </td>
</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Session key:")}</th>
<td>{_t("Session key:")}</td> <td>
<td> <code>
<code> <b>{identityKey}</b>
<b>{identityKey}</b> </code>
</code> </td>
</td> </tr>
</tr>
</tbody>
</table> </table>
{importExportButtons} {importExportButtons}
{noSendUnverifiedSetting} {noSendUnverifiedSetting}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactNode } from "react";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { CryptoEvent } from "matrix-js-sdk/src/crypto";
@ -231,9 +231,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
sessionsRemaining, sessionsRemaining,
} = this.state; } = this.state;
let statusDescription; let statusDescription: JSX.Element;
let extraDetailsTableRows; let extraDetailsTableRows: JSX.Element | undefined;
let extraDetails; let extraDetails: JSX.Element | undefined;
const actions: JSX.Element[] = []; const actions: JSX.Element[] = [];
if (error) { if (error) {
statusDescription = <div className="error">{_t("Unable to load key backup status")}</div>; statusDescription = <div className="error">{_t("Unable to load key backup status")}</div>;
@ -267,7 +267,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
restoreButtonCaption = _t("Connect this session to Key Backup"); restoreButtonCaption = _t("Connect this session to Key Backup");
} }
let uploadStatus; let uploadStatus: ReactNode;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) { if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
// No upload status to show when backup disabled. // No upload status to show when backup disabled.
uploadStatus = ""; uploadStatus = "";
@ -391,11 +391,11 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
extraDetailsTableRows = ( extraDetailsTableRows = (
<> <>
<tr> <tr>
<td>{_t("Backup version:")}</td> <th scope="row">{_t("Backup version:")}</th>
<td>{backupInfo.version}</td> <td>{backupInfo.version}</td>
</tr> </tr>
<tr> <tr>
<td>{_t("Algorithm:")}</td> <th scope="row">{_t("Algorithm:")}</th>
<td>{backupInfo.algorithm}</td> <td>{backupInfo.algorithm}</td>
</tr> </tr>
</> </>
@ -460,7 +460,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} }
} }
let actionRow; let actionRow: JSX.Element | undefined;
if (actions.length) { if (actions.length) {
actionRow = <div className="mx_SecureBackupPanel_buttonRow">{actions}</div>; actionRow = <div className="mx_SecureBackupPanel_buttonRow">{actions}</div>;
} }
@ -478,28 +478,26 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{_t("Advanced")}</summary>
<table className="mx_SecureBackupPanel_statusList"> <table className="mx_SecureBackupPanel_statusList">
<tbody> <tr>
<tr> <th scope="row">{_t("Backup key stored:")}</th>
<td>{_t("Backup key stored:")}</td> <td>{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}</td>
<td>{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Backup key cached:")}</th>
<td>{_t("Backup key cached:")}</td> <td>
<td> {backupKeyCached ? _t("cached locally") : _t("not found locally")}
{backupKeyCached ? _t("cached locally") : _t("not found locally")} {backupKeyWellFormedText}
{backupKeyWellFormedText} </td>
</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Secret storage public key:")}</th>
<td>{_t("Secret storage public key:")}</td> <td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td> </tr>
</tr> <tr>
<tr> <th scope="row">{_t("Secret storage:")}</th>
<td>{_t("Secret storage:")}</td> <td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td> </tr>
</tr> {extraDetailsTableRows}
{extraDetailsTableRows}
</tbody>
</table> </table>
{extraDetails} {extraDetails}
</details> </details>

View file

@ -89,8 +89,8 @@ const SpaceCreateMenuType: React.FC<{
}> = ({ title, description, className, onClick }) => { }> = ({ title, description, className, onClick }) => {
return ( return (
<AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}> <AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}>
<h3>{title}</h3> {title}
<span>{description}</span> <div>{description}</div>
</AccessibleButton> </AccessibleButton>
); );
}; };

View file

@ -52,7 +52,7 @@ const SpacePublicShare: React.FC<IProps> = ({ space, onFinished }) => {
} }
}} }}
> >
<h3>{_t("Share invite link")}</h3> {_t("Share invite link")}
<span>{copiedText}</span> <span>{copiedText}</span>
</AccessibleButton> </AccessibleButton>
{space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers) ? ( {space.canInvite(MatrixClientPeg.get()?.getUserId()) && shouldShowComponent(UIComponent.InviteUsers) ? (
@ -63,8 +63,8 @@ const SpacePublicShare: React.FC<IProps> = ({ space, onFinished }) => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
}} }}
> >
<h3>{_t("Invite people")}</h3> {_t("Invite people")}
<span>{_t("Invite with email or username")}</span> <div>{_t("Invite with email or username")}</div>
</AccessibleButton> </AccessibleButton>
) : null} ) : null}
</div> </div>

View file

@ -32,11 +32,9 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
import SettingsStore from "../../../src/settings/SettingsStore"; import SettingsStore from "../../../src/settings/SettingsStore";
// Fake random strings to give a predictable snapshot for checkbox IDs // Fake random strings to give a predictable snapshot for checkbox IDs
jest.mock("matrix-js-sdk/src/randomstring", () => { jest.mock("matrix-js-sdk/src/randomstring", () => ({
return { randomString: () => "abdefghi",
randomString: () => "abdefghi", }));
};
});
describe("SpaceHierarchy", () => { describe("SpaceHierarchy", () => {
describe("showRoom", () => { describe("showRoom", () => {

View file

@ -72,6 +72,11 @@ jest.mock("../../../../src/Modal", () => ({
ModalManagerEvent: { Opened: "opened" }, ModalManagerEvent: { Opened: "opened" },
})); }));
// Fake random strings to give a predictable snapshot for IDs
jest.mock("matrix-js-sdk/src/randomstring", () => ({
randomString: () => "abdefghi",
}));
describe("<LocationShareMenu />", () => { describe("<LocationShareMenu />", () => {
const userId = "@ernie:server.org"; const userId = "@ernie:server.org";
const mockClient = getMockClientWithEventEmitter({ const mockClient = getMockClientWithEventEmitter({

View file

@ -25,12 +25,16 @@ exports[`<LocationShareMenu /> with live location disabled goes to labs flag scr
<span <span
class="mx_SettingsFlag_label" class="mx_SettingsFlag_label"
> >
Enable live location sharing <div
id="mx_LabelledToggleSwitch_abdefghi"
>
Enable live location sharing
</div>
</span> </span>
<div <div
aria-checked="false" aria-checked="false"
aria-disabled="false" aria-disabled="false"
aria-label="Enable live location sharing" aria-labelledby="mx_LabelledToggleSwitch_abdefghi"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
role="switch" role="switch"
tabindex="0" tabindex="0"

View file

@ -27,8 +27,10 @@ import {
ConditionKind, ConditionKind,
IPushRuleCondition, IPushRuleCondition,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import { act, fireEvent, getByTestId, render, screen, waitFor, within } from "@testing-library/react"; import { act, fireEvent, getByTestId, render, screen, waitFor, within } from "@testing-library/react";
import { mocked } from "jest-mock";
import Notifications from "../../../../src/components/views/settings/Notifications"; import Notifications from "../../../../src/components/views/settings/Notifications";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
@ -41,6 +43,11 @@ jest.mock("matrix-js-sdk/src/logger");
// Avoid indirectly importing any eagerly created stores that would require extra setup // Avoid indirectly importing any eagerly created stores that would require extra setup
jest.mock("../../../../src/Notifier"); jest.mock("../../../../src/Notifier");
// Fake random strings to give a predictable snapshot for IDs
jest.mock("matrix-js-sdk/src/randomstring", () => ({
randomString: jest.fn(),
}));
const masterRule: IPushRule = { const masterRule: IPushRule = {
actions: [PushRuleActionName.DontNotify], actions: [PushRuleActionName.DontNotify],
conditions: [], conditions: [],
@ -271,6 +278,11 @@ describe("<Notifications />", () => {
mockClient.getPushRules.mockResolvedValue(pushRules); mockClient.getPushRules.mockResolvedValue(pushRules);
beforeEach(() => { beforeEach(() => {
let i = 0;
mocked(randomString).mockImplementation(() => {
return "testid_" + i++;
});
mockClient.getPushRules.mockClear().mockResolvedValue(pushRules); mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] }); mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] }); mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });

View file

@ -12,18 +12,23 @@ exports[`<Notifications /> main notification switches renders only enable notifi
<span <span
class="mx_SettingsFlag_label" class="mx_SettingsFlag_label"
> >
Enable notifications for this account <div
<br /> id="mx_LabelledToggleSwitch_testid_0"
>
Enable notifications for this account
</div>
<span <span
class="mx_Caption" class="mx_Caption"
id="mx_LabelledToggleSwitch_testid_0_caption"
> >
Turn off to disable notifications on all your devices and sessions Turn off to disable notifications on all your devices and sessions
</span> </span>
</span> </span>
<div <div
aria-checked="false" aria-checked="false"
aria-describedby="mx_LabelledToggleSwitch_testid_0_caption"
aria-disabled="false" aria-disabled="false"
aria-label="Enable notifications for this account" aria-labelledby="mx_LabelledToggleSwitch_testid_0"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
role="switch" role="switch"
tabindex="0" tabindex="0"

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, RenderResult } from "@testing-library/react"; import { act, fireEvent, render, RenderResult } from "@testing-library/react";
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials"; import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
@ -27,6 +28,11 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
const SpaceSettingsVisibilityTab = wrapInMatrixClientContext(_SpaceSettingsVisibilityTab); const SpaceSettingsVisibilityTab = wrapInMatrixClientContext(_SpaceSettingsVisibilityTab);
// Fake random strings to give a predictable snapshot for IDs
jest.mock("matrix-js-sdk/src/randomstring", () => ({
randomString: jest.fn(),
}));
jest.useFakeTimers(); jest.useFakeTimers();
describe("<SpaceSettingsVisibilityTab />", () => { describe("<SpaceSettingsVisibilityTab />", () => {
@ -89,13 +95,16 @@ describe("<SpaceSettingsVisibilityTab />", () => {
const toggleButton = getByTestId("toggle-guest-access-btn")!; const toggleButton = getByTestId("toggle-guest-access-btn")!;
fireEvent.click(toggleButton); fireEvent.click(toggleButton);
}; };
const getGuestAccessToggle = ({ container }: RenderResult) => const getGuestAccessToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Enable guest access");
container.querySelector('[aria-label="Enable guest access"]'); const getHistoryVisibilityToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Preview Space");
const getHistoryVisibilityToggle = ({ container }: RenderResult) =>
container.querySelector('[aria-label="Preview Space"]');
const getErrorMessage = ({ getByTestId }: RenderResult) => getByTestId("space-settings-error")?.textContent; const getErrorMessage = ({ getByTestId }: RenderResult) => getByTestId("space-settings-error")?.textContent;
beforeEach(() => { beforeEach(() => {
let i = 0;
mocked(randomString).mockImplementation(() => {
return "testid_" + i++;
});
(mockMatrixClient.sendStateEvent as jest.Mock).mockClear().mockResolvedValue({}); (mockMatrixClient.sendStateEvent as jest.Mock).mockClear().mockResolvedValue({});
MatrixClientPeg.get = jest.fn().mockReturnValue(mockMatrixClient); MatrixClientPeg.get = jest.fn().mockReturnValue(mockMatrixClient);
}); });

View file

@ -4,7 +4,7 @@ exports[`<SpaceSettingsVisibilityTab /> for a public space Access renders guest
<div <div
aria-checked="true" aria-checked="true"
aria-disabled="false" aria-disabled="false"
aria-label="Enable guest access" aria-labelledby="mx_LabelledToggleSwitch_testid_1"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
role="switch" role="switch"
tabindex="0" tabindex="0"
@ -105,12 +105,16 @@ exports[`<SpaceSettingsVisibilityTab /> renders container 1`] = `
<span <span
class="mx_SettingsFlag_label" class="mx_SettingsFlag_label"
> >
Preview Space <div
id="mx_LabelledToggleSwitch_testid_0"
>
Preview Space
</div>
</span> </span>
<div <div
aria-checked="true" aria-checked="true"
aria-disabled="false" aria-disabled="false"
aria-label="Preview Space" aria-labelledby="mx_LabelledToggleSwitch_testid_0"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
role="switch" role="switch"
tabindex="0" tabindex="0"