Spike AXE A11Y testing in Cypress (#9111)

* Spike AXE A11Y testing in Cypress

* Fix NewRoomIntro breaking html/aria list rules

* Fix HeaderButtons breaking aria role semantics rules

* missing type

* Switch left panel from aside to nav and include space panel

* Give the page a main heading of the room name when viewing a room

* Use header landmark on RoomHeader

* Improve aria attributes on composer when autocomplete is closed

* Fix aria-owns on RoomHeader

* Give Spinner an aria role

* Give server picker help button an aria label

* Improve auth aria attributes and semantics

* Improve heading semantics in use case selection screen

* Fix autocomplete attribute to be valid

* Fix heading semantics on login page

* Improve Cypress axe testing

* Add axe tests

* Stop synapse after the timeline tests

* Await spinners to fade before percy snapshotting timeline tests

* Improve naming of plugin

* Update snapshots

* Fix accidental heading change

* Fix double synapse stoppage

* Fix Cypress timeline avatar assertions to be DPI agnostic

* Fix aria attributes on date separators

* delint

* Update snapshots

* Revert style change

* Skip redundant call
This commit is contained in:
Michael Telatynski 2022-08-01 08:31:14 +01:00 committed by GitHub
parent 05cc5f62dd
commit d5db131eef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 244 additions and 83 deletions

View file

@ -382,7 +382,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses}>
<aside className="mx_LeftPanel_roomListContainer">
<div className="mx_LeftPanel_roomListContainer">
{ this.renderSearchDialExplore() }
{ this.renderBreadcrumbs() }
{ !this.props.isMinimized && (
@ -401,7 +401,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{ roomList }
</div>
</div>
</aside>
</div>
</div>
);
}

View file

@ -674,7 +674,7 @@ class LoggedInView extends React.Component<IProps, IState> {
<div className={bodyClasses}>
<div className='mx_LeftPanel_outerWrapper'>
<LeftPanelLiveShareWarning isMinimized={this.props.collapseLhs || false} />
<div className='mx_LeftPanel_wrapper'>
<nav className='mx_LeftPanel_wrapper'>
<BackdropPanel
blurMultiplier={0.5}
backgroundImage={this.state.backgroundImage}
@ -693,7 +693,7 @@ class LoggedInView extends React.Component<IProps, IState> {
resizeNotifier={this.props.resizeNotifier}
/>
</div>
</div>
</nav>
</div>
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
<div className="mx_RoomView_wrapper">

View file

@ -100,11 +100,11 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
return (
<AuthPage>
<CompleteSecurityBody>
<h2 className="mx_CompleteSecurity_header">
<h1 className="mx_CompleteSecurity_header">
{ icon }
{ title }
{ skipButton }
</h2>
</h1>
<div className="mx_CompleteSecurity_body">
<SetupEncryptionBody onFinished={this.props.onFinished} />
</div>

View file

@ -437,7 +437,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
<AuthPage>
<AuthHeader />
<AuthBody>
<h2> { _t('Set a new password') } </h2>
<h1> { _t('Set a new password') } </h1>
{ resetPasswordJsx }
</AuthBody>
</AuthPage>

View file

@ -600,10 +600,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
<AuthPage>
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
<AuthBody>
<h2>
<h1>
{ _t('Sign in') }
{ loader }
</h2>
</h1>
{ errorTextSection }
{ serverDeadSection }
<ServerPicker

View file

@ -507,9 +507,9 @@ export default class Registration extends React.Component<IProps, IState> {
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
if (providers.length > 1) {
// i18n: ssoButtons is a placeholder to help translators understand context
continueWithSection = <h3 className="mx_AuthBody_centered">
continueWithSection = <h2 className="mx_AuthBody_centered">
{ _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() }
</h3>;
</h2>;
}
// i18n: ssoButtons & usernamePassword are placeholders to help translators understand context
@ -521,7 +521,7 @@ export default class Registration extends React.Component<IProps, IState> {
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
<h3 className="mx_AuthBody_centered">
<h2 className="mx_AuthBody_centered">
{ _t(
"%(ssoButtons)s Or %(usernamePassword)s",
{
@ -529,7 +529,7 @@ export default class Registration extends React.Component<IProps, IState> {
usernamePassword: "",
},
).trim() }
</h3>
</h2>
</React.Fragment>;
}
@ -617,7 +617,7 @@ export default class Registration extends React.Component<IProps, IState> {
} else {
// regardless of whether we're the client that started the registration or not, we should
// try our credentials anyway
regDoneText = <h3>{ _t(
regDoneText = <h2>{ _t(
"<a>Log in</a> to your new account.", {},
{
a: (sub) => <AccessibleButton
@ -630,10 +630,10 @@ export default class Registration extends React.Component<IProps, IState> {
}}
>{ sub }</AccessibleButton>,
},
) }</h3>;
) }</h2>;
}
body = <div>
<h2>{ _t("Registration Successful") }</h2>
<h1>{ _t("Registration Successful") }</h1>
{ regDoneText }
</div>;
} else {

View file

@ -298,7 +298,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
return <>
<p>{ introText }</p>
{ this.renderSsoForm(null) }
<h3 className="mx_AuthBody_centered">
<h2 className="mx_AuthBody_centered">
{ _t(
"%(ssoButtons)s Or %(usernamePassword)s",
{
@ -306,7 +306,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
usernamePassword: "",
},
).trim() }
</h3>
</h2>
{ this.renderPasswordForm(null) }
</>;
}
@ -327,16 +327,16 @@ export default class SoftLogout extends React.Component<IProps, IState> {
<AuthPage>
<AuthHeader />
<AuthBody>
<h2>
<h1>
{ _t("You're signed out") }
</h2>
</h1>
<h3>{ _t("Sign in") }</h3>
<h2>{ _t("Sign in") }</h2>
<div>
{ this.renderSignInSection() }
</div>
<h3>{ _t("Clear personal data") }</h3>
<h2>{ _t("Clear personal data") }</h2>
<p>
{ _t(
"Warning: Your personal data (including encryption keys) is still stored " +

View file

@ -33,7 +33,7 @@ export function AuthHeaderDisplay({ title, icon, serverPicker, children }: Props
return (
<Fragment>
{ current?.icon ?? icon }
<h2>{ current?.title ?? title }</h2>
<h1>{ current?.title ?? title }</h1>
{ children }
{ current?.hideServerPicker !== true && serverPicker }
</Fragment>

View file

@ -22,7 +22,7 @@ interface Props {
}
export default function AuthBody({ flex, children }: PropsWithChildren<Props>) {
return <div className={classNames("mx_AuthBody", { "mx_AuthBody_flex": flex })}>
return <main className={classNames("mx_AuthBody", { "mx_AuthBody_flex": flex })}>
{ children }
</div>;
</main>;
}

View file

@ -23,9 +23,9 @@ import { _t } from '../../../languageHandler';
export default class AuthFooter extends React.Component {
public render(): React.ReactNode {
return (
<div className="mx_AuthFooter">
<footer className="mx_AuthFooter" role="contentinfo">
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
</div>
</footer>
);
}
}

View file

@ -18,8 +18,8 @@ import React from 'react';
export default class AuthHeaderLogo extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthHeaderLogo">
return <aside className="mx_AuthHeaderLogo">
Matrix
</div>;
</aside>;
}
}

View file

@ -422,7 +422,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
<Field
id="mx_LoginForm_password"
className={pwFieldClass}
autoComplete="password"
autoComplete="current-password"
type="password"
name="password"
label={_t('Password')}

View file

@ -206,6 +206,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
checked={!this.state.defaultChosen}
onChange={this.onOtherChosen}
childrenInLabel={false}
aria-label={_t("Other homeserver")}
>
<Field
type="text"
@ -230,7 +231,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
{ _t("Continue") }
</AccessibleButton>
<h4>{ _t("Learn more") }</h4>
<h2>{ _t("Learn more") }</h2>
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
{ _t("About homeservers") }
</a>

View file

@ -85,8 +85,13 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
}
return <div className="mx_ServerPicker">
<h3>{ title || _t("Homeserver") }</h3>
{ !disableCustomUrls ? <AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} /> : null }
<h2>{ title || _t("Homeserver") }</h2>
{ !disableCustomUrls ? (
<AccessibleButton
className="mx_ServerPicker_help"
onClick={onHelpClick}
aria-label={_t("Help")}
/>): null }
<span className="mx_ServerPicker_server" title={typeof serverName === "string" ? serverName : undefined}>
{ serverName }
</span>

View file

@ -39,6 +39,7 @@ export default class Spinner extends React.PureComponent<IProps> {
className="mx_Spinner_icon"
style={{ width: w, height: h }}
aria-label={_t("Loading...")}
role="progressbar"
/>
</div>
);

View file

@ -57,7 +57,7 @@ export function UseCaseSelection({ onFinished }: Props) {
</div>
<div className="mx_UseCaseSelection_info mx_UseCaseSelection_slideInDelayed">
<h2>{ _t("Who will you chat to the most?") }</h2>
<h4>{ _t("We'll help you get connected.") }</h4>
<h3>{ _t("We'll help you get connected.") }</h3>
</div>
<div className="mx_UseCaseSelection_options mx_UseCaseSelection_slideInDelayed">
<UseCaseSelectionButton

View file

@ -223,7 +223,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
isExpanded={!!this.state.contextMenuPosition}
title={_t("Jump to date")}
>
<div aria-hidden="true">{ this.getLabel() }</div>
<h2 aria-hidden="true">{ this.getLabel() }</h2>
<div className="mx_DateSeparator_chevron" />
{ contextMenu }
</ContextMenuTooltipButton>
@ -237,15 +237,15 @@ export default class DateSeparator extends React.Component<IProps, IState> {
if (this.state.jumpToDateEnabled) {
dateHeaderContent = this.renderJumpToDateMenu();
} else {
dateHeaderContent = <div aria-hidden="true">{ label }</div>;
dateHeaderContent = <h2 aria-hidden="true">{ label }</h2>;
}
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
return <div className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
<hr role="none" />
{ dateHeaderContent }
<hr role="none" />
</h2>;
</div>;
}
}

View file

@ -95,7 +95,7 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
public abstract renderButtons(): JSX.Element;
public render() {
return <div className="mx_HeaderButtons">
return <div className="mx_HeaderButtons" role="tablist">
{ this.renderButtons() }
</div>;
}

View file

@ -760,7 +760,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const { completionIndex } = this.state;
const hasAutocomplete = Boolean(this.state.autoComplete);
let activeDescendant;
let activeDescendant: string;
if (hasAutocomplete && completionIndex >= 0) {
activeDescendant = generateCompletionDomId(completionIndex);
}
@ -784,8 +784,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded={hasAutocomplete}
aria-owns="mx_Autocomplete"
aria-expanded={hasAutocomplete ? true : undefined}
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}

View file

@ -219,8 +219,7 @@ const NewRoomIntro = () => {
<span> { subText } { subButton } </span>
);
return <div className="mx_NewRoomIntro">
return <li className="mx_NewRoomIntro">
{ !hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
@ -230,7 +229,7 @@ const NewRoomIntro = () => {
) }
{ body }
</div>;
</li>;
};
export default NewRoomIntro;

View file

@ -45,6 +45,8 @@ import { NotificationStateEvents } from '../../../stores/notifications/Notificat
import RoomContext from "../../../contexts/RoomContext";
import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
import { BetaPill } from "../beta/BetaCard";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
export interface ISearchInfo {
searchTerm: string;
@ -71,6 +73,7 @@ interface IProps {
interface IState {
contextMenuPosition?: DOMRect;
rightPanelOpen: boolean;
}
export default class RoomHeader extends React.Component<IProps, IState> {
@ -89,23 +92,29 @@ export default class RoomHeader extends React.Component<IProps, IState> {
super(props, context);
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.state = {};
this.state = {
rightPanelOpen: RightPanelStore.instance.isOpen,
};
}
public componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
}
public componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
}
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
}
private onRightPanelStoreUpdate = () => {
this.setState({ rightPanelOpen: RightPanelStore.instance.isOpen });
};
private onRoomStateEvents = (event: MatrixEvent) => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
@ -230,7 +239,9 @@ export default class RoomHeader extends React.Component<IProps, IState> {
const roomName = <RoomName room={this.props.room}>
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
return <div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
{ roomName }
</div>;
} }
</RoomName>;
@ -311,8 +322,11 @@ export default class RoomHeader extends React.Component<IProps, IState> {
) : null;
return (
<div className="mx_RoomHeader light-panel">
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
<header className="mx_RoomHeader light-panel">
<div
className="mx_RoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name }
@ -322,7 +336,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
{ buttons }
</div>
<RoomLiveShareWarning roomId={this.props.room.roomId} />
</div>
</header>
);
}
}

View file

@ -2408,6 +2408,7 @@
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.",
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
"Homeserver": "Homeserver",
"Help": "Help",
"Choose a locale": "Choose a locale",
"Continue with %(provider)s": "Continue with %(provider)s",
"Sign in with single sign-on": "Sign in with single sign-on",