MM-52641 MM-52645 Playwright/Accessibility: Initial setup and accessibility fix on login page (#23254)
* MM-52641 Playwright/Accessibility: initial setup * upgrade Playwright to 1.36.1 * fix accessibility at login page * fix lint * update snapshot
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
077c16ef61
Коммит
e377d985cd
1
.gitignore
поставляемый
1
.gitignore
поставляемый
@@ -41,6 +41,7 @@ e2e-tests/playwright/storage_state
|
|||||||
e2e-tests/playwright/test-results
|
e2e-tests/playwright/test-results
|
||||||
e2e-tests/playwright/tests/**/*-darwin.png
|
e2e-tests/playwright/tests/**/*-darwin.png
|
||||||
e2e-tests/playwright/tests/**/*-window.png
|
e2e-tests/playwright/tests/**/*-window.png
|
||||||
|
e2e-tests/playwright/tests/accessibility/**/*-snapshots
|
||||||
e2e-tests/playwright/.eslintcache
|
e2e-tests/playwright/.eslintcache
|
||||||
|
|
||||||
# Enterprise & products imports files
|
# Enterprise & products imports files
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ npm run test
|
|||||||
Change to root directory, run docker container
|
Change to root directory, run docker container
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.32.0-focal /bin/bash
|
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.36.0-focal /bin/bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Inside the docker container
|
#### 2. Inside the docker container
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
|
|
||||||
import {UserProfile} from '@mattermost/types/users';
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import { PreferenceType } from '@mattermost/types/preferences';
|
import {PreferenceType} from '@mattermost/types/preferences';
|
||||||
|
|
||||||
import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server';
|
import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server';
|
||||||
import {defaultTeam} from './support/util';
|
import {defaultTeam} from './support/util';
|
||||||
@@ -197,10 +197,15 @@ async function savePreferences(client: Client, userId: UserProfile['id']) {
|
|||||||
|
|
||||||
const preferences: PreferenceType[] = [
|
const preferences: PreferenceType[] = [
|
||||||
{user_id: userId, category: 'tutorial_step', name: userId, value: '999'},
|
{user_id: userId, category: 'tutorial_step', name: userId, value: '999'},
|
||||||
{user_id: userId, category: 'drafts', name: 'drafts_tour_tip_showed', value: JSON.stringify({drafts_tour_tip_showed: true})},
|
{
|
||||||
|
user_id: userId,
|
||||||
|
category: 'drafts',
|
||||||
|
name: 'drafts_tour_tip_showed',
|
||||||
|
value: JSON.stringify({drafts_tour_tip_showed: true}),
|
||||||
|
},
|
||||||
{user_id: userId, category: 'crt_thread_pane_step', name: userId, value: '999'},
|
{user_id: userId, category: 'crt_thread_pane_step', name: userId, value: '999'},
|
||||||
];
|
];
|
||||||
|
|
||||||
await client.savePreferences(userId, preferences);
|
await client.savePreferences(userId, preferences);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
1381
e2e-tests/playwright/package-lock.json
сгенерированный
1381
e2e-tests/playwright/package-lock.json
сгенерированный
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -12,23 +12,25 @@
|
|||||||
"show-report": "npx playwright show-report"
|
"show-report": "npx playwright show-report"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@percy/cli": "1.23.0",
|
"@axe-core/playwright": "4.7.3",
|
||||||
|
"@percy/cli": "1.26.2",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.32.3",
|
"@playwright/test": "1.36.1",
|
||||||
"async-wait-until": "2.0.12",
|
"async-wait-until": "2.0.12",
|
||||||
|
"axe-core": "4.7.2",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"dotenv": "16.0.3",
|
"dotenv": "16.3.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"isomorphic-unfetch": "4.0.2",
|
"isomorphic-unfetch": "4.0.2",
|
||||||
"uuid": "9.0.0"
|
"uuid": "9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/uuid": "9.0.1",
|
"@types/uuid": "9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
"@typescript-eslint/eslint-plugin": "6.1.0",
|
||||||
"@typescript-eslint/parser": "5.59.0",
|
"@typescript-eslint/parser": "6.1.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.38.0",
|
"eslint": "8.45.0",
|
||||||
"prettier": "2.8.7",
|
"prettier": "2.8.7",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,12 @@ export async function initSetup({
|
|||||||
// Update user preference
|
// Update user preference
|
||||||
const preferences: PreferenceType[] = [
|
const preferences: PreferenceType[] = [
|
||||||
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
||||||
{user_id: user.id, category: 'drafts', name: 'drafts_tour_tip_showed', value: JSON.stringify({drafts_tour_tip_showed: true})},
|
{
|
||||||
|
user_id: user.id,
|
||||||
|
category: 'drafts',
|
||||||
|
name: 'drafts_tour_tip_showed',
|
||||||
|
value: JSON.stringify({drafts_tour_tip_showed: true}),
|
||||||
|
},
|
||||||
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
|
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
|
||||||
];
|
];
|
||||||
await userClient.savePreferences(user.id, preferences);
|
await userClient.savePreferences(user.id, preferences);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import {test as base, Browser, ViewportSize} from '@playwright/test';
|
import {test as base, Browser, Page, ViewportSize} from '@playwright/test';
|
||||||
|
import {AxeResults} from 'axe-core';
|
||||||
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
|
|
||||||
import {TestBrowser} from './browser_context';
|
import {TestBrowser} from './browser_context';
|
||||||
import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldSkipInSmallScreen, shouldRunInLinux} from './flag';
|
import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldSkipInSmallScreen, shouldRunInLinux} from './flag';
|
||||||
@@ -11,11 +13,16 @@ import {matchSnapshot} from './visual';
|
|||||||
export {expect} from '@playwright/test';
|
export {expect} from '@playwright/test';
|
||||||
|
|
||||||
type ExtendedFixtures = {
|
type ExtendedFixtures = {
|
||||||
|
axe: AxeBuilderExtended;
|
||||||
pw: PlaywrightExtended;
|
pw: PlaywrightExtended;
|
||||||
pages: typeof pages;
|
pages: typeof pages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const test = base.extend<ExtendedFixtures>({
|
export const test = base.extend<ExtendedFixtures>({
|
||||||
|
axe: async ({page}, use) => {
|
||||||
|
const ab = new AxeBuilderExtended(page);
|
||||||
|
await use(ab);
|
||||||
|
},
|
||||||
pw: async ({browser, viewport}, use) => {
|
pw: async ({browser, viewport}, use) => {
|
||||||
const pw = new PlaywrightExtended(browser, viewport);
|
const pw = new PlaywrightExtended(browser, viewport);
|
||||||
await use(pw);
|
await use(pw);
|
||||||
@@ -84,3 +91,39 @@ class PlaywrightExtended {
|
|||||||
this.matchSnapshot = matchSnapshot;
|
this.matchSnapshot = matchSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AxeBuilderExtended {
|
||||||
|
/**
|
||||||
|
* Each page should have its own Axe Builder to specifically list known issues
|
||||||
|
* which are to be excluded from being scanned until issues are fixed.
|
||||||
|
* Excluded element should have a corresponding ticket.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// '<site_url>/login'
|
||||||
|
readonly loginPage: () => AxeBuilder;
|
||||||
|
|
||||||
|
// See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags
|
||||||
|
readonly tags: string[] = ['wcag2a', 'wcag2aa'];
|
||||||
|
|
||||||
|
// See https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md#wcag-20-level-a--aa-rules
|
||||||
|
readonly disabledRules: string[] = [];
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.loginPage = () => {
|
||||||
|
return new AxeBuilder({page}).withTags(this.tags).disableRules(this.disabledRules);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
violationFingerprints(accessibilityScanResults: AxeResults) {
|
||||||
|
const fingerprints = accessibilityScanResults.violations.map((violation) => ({
|
||||||
|
rule: violation.id,
|
||||||
|
description: violation.description,
|
||||||
|
helpUrl: violation.helpUrl,
|
||||||
|
targets: violation.nodes.map((node) => {
|
||||||
|
return {target: node.target, impact: node.impact, html: node.html};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return JSON.stringify(fingerprints, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default class PostDotMenu {
|
|||||||
constructor(container: Locator) {
|
constructor(container: Locator) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
|
||||||
this.deleteMenuItem = this.container.getByText('Delete', {exact: true});
|
this.deleteMenuItem = this.container.getByText('Delete', {exact: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
async toBeVisible() {
|
async toBeVisible() {
|
||||||
|
|||||||
19
e2e-tests/playwright/tests/accessibility/common/login.spec.ts
Обычный файл
19
e2e-tests/playwright/tests/accessibility/common/login.spec.ts
Обычный файл
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {expect, test} from '@e2e-support/test_fixture';
|
||||||
|
|
||||||
|
test('/login', async ({pw, pages, page, axe}) => {
|
||||||
|
// # Go to login page
|
||||||
|
const {adminClient} = await pw.getAdminClient();
|
||||||
|
const adminConfig = await adminClient.getConfig();
|
||||||
|
const loginPage = new pages.LoginPage(page, adminConfig);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.toBeVisible();
|
||||||
|
|
||||||
|
// # Analyze the page
|
||||||
|
const accessibilityScanResults = await axe.loginPage().analyze();
|
||||||
|
|
||||||
|
// * Should have no violation
|
||||||
|
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||||
|
});
|
||||||
@@ -20,8 +20,8 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use
|
|||||||
// # Create a post in the channel by admin
|
// # Create a post in the channel by admin
|
||||||
const adminPost = await adminClient.createPost(
|
const adminPost = await adminClient.createPost(
|
||||||
createRandomPost({
|
createRandomPost({
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
user_id: adminUser.id,
|
user_id: adminUser.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use
|
|||||||
|
|
||||||
const lastPostByAdmin = await channelPage.getLastPost();
|
const lastPostByAdmin = await channelPage.getLastPost();
|
||||||
await lastPostByAdmin.toBeVisible();
|
await lastPostByAdmin.toBeVisible();
|
||||||
|
|
||||||
// # Open the last post sent by admin in RHS
|
// # Open the last post sent by admin in RHS
|
||||||
await lastPostByAdmin.hover();
|
await lastPostByAdmin.hover();
|
||||||
await lastPostByAdmin.postMenu.toBeVisible();
|
await lastPostByAdmin.postMenu.toBeVisible();
|
||||||
@@ -52,7 +52,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use
|
|||||||
await sidebarRight.postMessage(draftMessageByUser);
|
await sidebarRight.postMessage(draftMessageByUser);
|
||||||
|
|
||||||
// # Close the RHS for draft to be saved
|
// # Close the RHS for draft to be saved
|
||||||
await sidebarRight.close()
|
await sidebarRight.close();
|
||||||
|
|
||||||
// * Verify drafts link in channel sidebar is visible
|
// * Verify drafts link in channel sidebar is visible
|
||||||
await channelPage.sidebarLeft.draftsVisible();
|
await channelPage.sidebarLeft.draftsVisible();
|
||||||
@@ -111,7 +111,7 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete
|
|||||||
await sidebarRight.postMessage('I should be in drafts');
|
await sidebarRight.postMessage('I should be in drafts');
|
||||||
|
|
||||||
// # Close the RHS for draft to be saved
|
// # Close the RHS for draft to be saved
|
||||||
await sidebarRight.close()
|
await sidebarRight.close();
|
||||||
|
|
||||||
// * Verify drafts link in channel sidebar is visible
|
// * Verify drafts link in channel sidebar is visible
|
||||||
await channelPage.sidebarLeft.draftsVisible();
|
await channelPage.sidebarLeft.draftsVisible();
|
||||||
@@ -120,7 +120,7 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete
|
|||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
await post.postMenu.openDotMenu();
|
await post.postMenu.openDotMenu();
|
||||||
await channelPage.postDotMenu.toBeVisible()
|
await channelPage.postDotMenu.toBeVisible();
|
||||||
await channelPage.postDotMenu.delete();
|
await channelPage.postDotMenu.delete();
|
||||||
|
|
||||||
// # Confirm the delete from the modal
|
// # Confirm the delete from the modal
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ export type HeaderProps = {
|
|||||||
const Header = ({alternateLink, backButtonURL, onBackButtonClick}: HeaderProps) => {
|
const Header = ({alternateLink, backButtonURL, onBackButtonClick}: HeaderProps) => {
|
||||||
const {EnableCustomBrand, SiteName} = useSelector(getConfig);
|
const {EnableCustomBrand, SiteName} = useSelector(getConfig);
|
||||||
|
|
||||||
|
const ariaLabel = EnableCustomBrand === 'true' && SiteName ? SiteName : 'Mattermost';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='hfroute-header'>
|
<div className='hfroute-header'>
|
||||||
<div className='header-main'>
|
<div className='header-main'>
|
||||||
<Link
|
<Link
|
||||||
className='header-logo-link'
|
className='header-logo-link'
|
||||||
to='/'
|
to='/'
|
||||||
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
{EnableCustomBrand === 'true' || SiteName !== 'Mattermost' ? SiteName : <Logo/>}
|
{EnableCustomBrand === 'true' || SiteName !== 'Mattermost' ? SiteName : <Logo/>}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ Object {
|
|||||||
class="Input_wrapper"
|
class="Input_wrapper"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
aria-label="Name"
|
||||||
class="Input form-control large"
|
class="Input form-control large"
|
||||||
id="input_name"
|
id="input_name"
|
||||||
name="name"
|
name="name"
|
||||||
@@ -101,6 +102,7 @@ Object {
|
|||||||
class="Input_wrapper"
|
class="Input_wrapper"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
aria-label="Business Email"
|
||||||
class="Input form-control large Input__focus"
|
class="Input form-control large Input__focus"
|
||||||
id="input_email"
|
id="input_email"
|
||||||
name="email"
|
name="email"
|
||||||
@@ -134,6 +136,7 @@ Object {
|
|||||||
class="Input_wrapper"
|
class="Input_wrapper"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
aria-label="Company Name"
|
||||||
class="Input form-control large"
|
class="Input form-control large"
|
||||||
id="input_company_name"
|
id="input_company_name"
|
||||||
name="company_name"
|
name="company_name"
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ const Input = React.forwardRef((
|
|||||||
className={classNames('Input form-control', inputSize, inputClassName, {Input__focus: showLegend})}
|
className={classNames('Input form-control', inputSize, inputClassName, {Input__focus: showLegend})}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
||||||
|
aria-label={label || placeholder}
|
||||||
name={name}
|
name={name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ const PasswordInput = React.forwardRef((
|
|||||||
const customMessageInfo: CustomMessageInputType | null = info ? {type: ItemStatus.INFO, value: info} : null;
|
const customMessageInfo: CustomMessageInputType | null = info ? {type: ItemStatus.INFO, value: info} : null;
|
||||||
const customMessage = error ? customMessageError : customMessageInfo;
|
const customMessage = error ? customMessageError : customMessageInfo;
|
||||||
|
|
||||||
|
const placeHolder = createMode ?
|
||||||
|
formatMessage({id: 'widget.passwordInput.createPassword', defaultMessage: 'Choose a Password'}) :
|
||||||
|
formatMessage({id: 'widget.passwordInput.password', defaultMessage: 'Password'});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
className={classNames('password-input', className)}
|
className={classNames('password-input', className)}
|
||||||
@@ -62,6 +66,7 @@ const PasswordInput = React.forwardRef((
|
|||||||
<button
|
<button
|
||||||
id='password_toggle'
|
id='password_toggle'
|
||||||
type='button'
|
type='button'
|
||||||
|
aria-label={placeHolder}
|
||||||
className='password-input-toggle'
|
className='password-input-toggle'
|
||||||
onClick={toggleShowPassword}
|
onClick={toggleShowPassword}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -73,11 +78,7 @@ const PasswordInput = React.forwardRef((
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
placeholder={createMode ? (
|
placeholder={placeHolder}
|
||||||
formatMessage({id: 'widget.passwordInput.createPassword', defaultMessage: 'Choose a Password'})
|
|
||||||
) : (
|
|
||||||
formatMessage({id: 'widget.passwordInput.password', defaultMessage: 'Password'})
|
|
||||||
)}
|
|
||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
customMessage={error || info ? customMessage : undefined}
|
customMessage={error || info ? customMessage : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0'>
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
<meta name='robots' content='noindex, nofollow'>
|
<meta name='robots' content='noindex, nofollow'>
|
||||||
<meta name='referrer' content='no-referrer'>
|
<meta name='referrer' content='no-referrer'>
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user