fix(accessibility): tab support at login, reset and signup pages, buttons at ATE and app bar (#24214)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
45a14e23a9
Коммит
ecf7cdbdea
@@ -45,10 +45,12 @@ describe('Signup Email page', () => {
|
||||
|
||||
it('should match elements, back button', () => {
|
||||
// * Check elements in the header with back button
|
||||
cy.get('#back_button').should('be.visible');
|
||||
cy.get('#back_button').should('contain', 'Back');
|
||||
cy.get('#back_button_icon').should('be.visible');
|
||||
cy.get('#back_button_icon').should('have.attr', 'title', 'Back Icon');
|
||||
cy.findByTestId('back_button').
|
||||
should('be.visible').
|
||||
and('have.text', 'Back');
|
||||
cy.get('#back_button_icon').
|
||||
should('be.visible').
|
||||
and('have.attr', 'title', 'Back Icon');
|
||||
});
|
||||
|
||||
it('should match elements, body', () => {
|
||||
@@ -94,20 +96,20 @@ describe('Signup Email page', () => {
|
||||
// * Check elements in the footer
|
||||
cy.get('.hfroute-footer').scrollIntoView().should('be.visible').within(() => {
|
||||
// * Check if about footer link is present
|
||||
cy.findByText('About').should('exist').
|
||||
and('have.attr', 'href', config.SupportSettings.AboutLink || ABOUT_LINK);
|
||||
cy.findByText('About').should('be.visible').
|
||||
should('have.attr', 'href').and('match', new RegExp(`${config.SupportSettings.AboutLink || ABOUT_LINK}/*`));
|
||||
|
||||
// * Check if privacy footer link is present
|
||||
cy.findByText('Privacy Policy').should('exist').
|
||||
and('have.attr', 'href', config.SupportSettings.PrivacyPolicyLink || PRIVACY_POLICY_LINK);
|
||||
cy.findByText('Privacy Policy').should('be.visible').
|
||||
should('have.attr', 'href').and('match', new RegExp(`${config.SupportSettings.PrivacyPolicyLink || PRIVACY_POLICY_LINK}/*`));
|
||||
|
||||
// * Check if terms footer link is present
|
||||
cy.findByText('Terms').should('exist').
|
||||
and('have.attr', 'href', config.SupportSettings.TermsOfServiceLink || TERMS_OF_SERVICE_LINK);
|
||||
cy.findByText('Terms').should('be.visible').
|
||||
should('have.attr', 'href').and('match', new RegExp(`${config.SupportSettings.TermsOfServiceLink || TERMS_OF_SERVICE_LINK}/*`));
|
||||
|
||||
// * Check if help footer link is present
|
||||
cy.findByText('Help').should('exist').
|
||||
and('have.attr', 'href', config.SupportSettings.HelpLink || HELP_LINK);
|
||||
cy.findByText('Help').should('be.visible').
|
||||
should('have.attr', 'href').and('match', new RegExp(`${config.SupportSettings.HelpLink || HELP_LINK}/*`));
|
||||
|
||||
const todaysDate = new Date();
|
||||
const currentYear = todaysDate.getFullYear();
|
||||
|
||||
@@ -18,6 +18,11 @@ type ExtendedFixtures = {
|
||||
pages: typeof pages;
|
||||
};
|
||||
|
||||
type AxeBuilderOptions = {
|
||||
disableColorContrast?: boolean;
|
||||
disableLinkInTextBlock?: boolean;
|
||||
};
|
||||
|
||||
export const test = base.extend<ExtendedFixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
axe: async ({}, use) => {
|
||||
@@ -94,15 +99,29 @@ class PlaywrightExtended {
|
||||
}
|
||||
|
||||
class AxeBuilderExtended {
|
||||
readonly builder: (page: Page, disableRules?: string[]) => AxeBuilder;
|
||||
readonly builder: (page: Page, options?: AxeBuilderOptions) => AxeBuilder;
|
||||
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags
|
||||
readonly tags: string[] = ['wcag2a', 'wcag2aa'];
|
||||
|
||||
constructor() {
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md#wcag-20-level-a--aa-rules
|
||||
this.builder = (page: Page, disableRules?: string[]) => {
|
||||
return new AxeBuilder({page}).withTags(this.tags).disableRules(disableRules || []);
|
||||
this.builder = (page: Page, options: AxeBuilderOptions = {}) => {
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md#wcag-20-level-a--aa-rules
|
||||
const disabledRules: string[] = [];
|
||||
|
||||
if (options.disableColorContrast) {
|
||||
// Disabled in pages due to impact to overall theme of Mattermost.
|
||||
// Option: make use of custom theme to improve color contrast.
|
||||
disabledRules.push('color-contrast');
|
||||
}
|
||||
|
||||
if (options.disableLinkInTextBlock) {
|
||||
// Disabled in pages due to impact to overall theme of Mattermost.
|
||||
// Option: make use of custom theme to improve color contrast.
|
||||
disabledRules.push('link-in-text-block');
|
||||
}
|
||||
|
||||
return new AxeBuilder({page}).withTags(this.tags).disableRules(disabledRules);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
30
e2e-tests/playwright/support/ui/components/footer.ts
Обычный файл
30
e2e-tests/playwright/support/ui/components/footer.ts
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class Footer {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly copyright;
|
||||
readonly aboutLink;
|
||||
readonly privacyPolicyLink;
|
||||
readonly termsLink;
|
||||
readonly helpLink;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.copyright = container.locator('.footer-copyright');
|
||||
this.aboutLink = container.locator('text=About');
|
||||
this.privacyPolicyLink = container.locator('text=Privacy Policy');
|
||||
this.termsLink = container.locator('text=Terms');
|
||||
this.helpLink = container.locator('text=Help');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.copyright).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {Footer};
|
||||
@@ -10,7 +10,9 @@ import {ChannelsPost} from './channels/post';
|
||||
import {ChannelsSidebarLeft} from './channels/sidebar_left';
|
||||
import {ChannelsSidebarRight} from './channels/sidebar_right';
|
||||
import {FindChannelsModal} from './channels/find_channels_modal';
|
||||
import {Footer} from './footer';
|
||||
import {GlobalHeader} from './global_header';
|
||||
import {MainHeader} from './main_header';
|
||||
import {PostDotMenu} from './channels/post_dot_menu';
|
||||
import {DeletePostModal} from './channels/delete_post_modal';
|
||||
import {PostMenu} from './channels/post_menu';
|
||||
@@ -26,7 +28,9 @@ const components = {
|
||||
ChannelsSidebarLeft,
|
||||
ChannelsSidebarRight,
|
||||
FindChannelsModal,
|
||||
Footer,
|
||||
GlobalHeader,
|
||||
MainHeader,
|
||||
PostDotMenu,
|
||||
DeletePostModal,
|
||||
PostMenu,
|
||||
|
||||
24
e2e-tests/playwright/support/ui/components/main_header.ts
Обычный файл
24
e2e-tests/playwright/support/ui/components/main_header.ts
Обычный файл
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class MainHeader {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly logo;
|
||||
readonly backButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.logo = container.locator('.header-logo-link');
|
||||
this.backButton = container.getByTestId('back_button');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {MainHeader};
|
||||
@@ -6,6 +6,7 @@ import {BoardsViewPage} from './boards_view';
|
||||
import {ChannelsPage} from './channels';
|
||||
import {LandingLoginPage} from './landing_login';
|
||||
import {LoginPage} from './login';
|
||||
import {ResetPasswordPage} from './reset_password';
|
||||
import {SignupPage} from './signup';
|
||||
|
||||
const pages = {
|
||||
@@ -14,6 +15,7 @@ const pages = {
|
||||
ChannelsPage,
|
||||
LandingLoginPage,
|
||||
LoginPage,
|
||||
ResetPasswordPage,
|
||||
SignupPage,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import {expect, Page} from '@playwright/test';
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class LoginPage {
|
||||
readonly adminConfig: AdminConfig;
|
||||
|
||||
@@ -17,6 +19,7 @@ export default class LoginPage {
|
||||
readonly loginInput;
|
||||
readonly loginPlaceholder;
|
||||
readonly passwordInput;
|
||||
readonly passwordToggleButton;
|
||||
readonly signInButton;
|
||||
readonly createAccountLink;
|
||||
readonly forgotPasswordLink;
|
||||
@@ -24,6 +27,9 @@ export default class LoginPage {
|
||||
readonly fieldWithError;
|
||||
readonly formContainer;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page, adminConfig: AdminConfig) {
|
||||
this.page = page;
|
||||
this.adminConfig = adminConfig;
|
||||
@@ -34,16 +40,20 @@ export default class LoginPage {
|
||||
|
||||
this.title = page.locator('h1:has-text("Log in to your account")');
|
||||
this.subtitle = page.locator('text=Collaborate with your team in real-time');
|
||||
this.bodyCard = page.locator('.login-body-card');
|
||||
this.bodyCard = page.locator('.login-body-card-content');
|
||||
this.loginInput = page.locator('#input_loginId');
|
||||
this.loginPlaceholder = page.locator(`[placeholder="${loginInputPlaceholder}"]`);
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.getByRole('button', {name: 'Show or hide password'});
|
||||
this.signInButton = page.locator('button:has-text("Log in")');
|
||||
this.createAccountLink = page.locator("text=Don't have an account?");
|
||||
this.forgotPasswordLink = page.locator('text=Forgot your password?');
|
||||
this.userErrorLabel = page.locator('text=Please enter your email or username');
|
||||
this.fieldWithError = page.locator('.with-error');
|
||||
this.formContainer = page.locator('.signup-team__container');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.hfroute-header'));
|
||||
this.footer = new components.Footer(page.locator('.hfroute-footer'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
||||
51
e2e-tests/playwright/support/ui/pages/reset_password.ts
Обычный файл
51
e2e-tests/playwright/support/ui/pages/reset_password.ts
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Page} from '@playwright/test';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class ResetPasswordPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly title;
|
||||
readonly subtitle;
|
||||
readonly emailInput;
|
||||
readonly resetButton;
|
||||
readonly formContainer;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.title = page.locator('h1:has-text("Password Reset")');
|
||||
this.subtitle = page.locator('text=To reset your password, enter the email address you used to sign up');
|
||||
this.emailInput = page.locator(`[placeholder="Email"]`);
|
||||
this.resetButton = page.locator('#passwordResetButton');
|
||||
this.formContainer = page.locator('.signup-team__container');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.signup-header'));
|
||||
this.footer = new components.Footer(page.locator('#footer_section'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.subtitle).toBeVisible();
|
||||
await expect(this.emailInput).toBeVisible();
|
||||
await expect(this.resetButton).toBeVisible();
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/reset_password');
|
||||
}
|
||||
|
||||
async reset(email: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await Promise.all([this.page.waitForNavigation(), this.resetButton.click()]);
|
||||
}
|
||||
}
|
||||
|
||||
export {ResetPasswordPage};
|
||||
@@ -4,6 +4,7 @@
|
||||
import {expect, Page} from '@playwright/test';
|
||||
|
||||
import {duration, wait} from '@e2e-support/util';
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class SignupPage {
|
||||
readonly page: Page;
|
||||
@@ -14,28 +15,50 @@ export default class SignupPage {
|
||||
readonly emailInput;
|
||||
readonly usernameInput;
|
||||
readonly passwordInput;
|
||||
readonly passwordToggleButton;
|
||||
readonly newsLetterCheckBox;
|
||||
readonly newsLetterPrivacyPolicyLink;
|
||||
readonly newsLetterUnsubscribeLink;
|
||||
readonly agreementTermsOfUseLink;
|
||||
readonly agreementPrivacyPolicyLink;
|
||||
readonly createAccountButton;
|
||||
readonly loginLink;
|
||||
readonly emailError;
|
||||
readonly usernameError;
|
||||
readonly passwordError;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.title = page.locator('h1:has-text("Let’s get started")');
|
||||
this.subtitle = page.locator('text=Create your Mattermost account to start collaborating with your team');
|
||||
this.bodyCard = page.locator('.signup-body-card');
|
||||
this.bodyCard = page.locator('.signup-body-card-content');
|
||||
this.loginLink = page.locator('text=Log in');
|
||||
this.emailInput = page.locator('#input_email');
|
||||
this.usernameInput = page.locator('#input_name');
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.getByRole('button', {name: 'Show or hide password'});
|
||||
this.createAccountButton = page.locator('button:has-text("Create Account")');
|
||||
this.loginLink = page.locator('text=Click here to sign in.');
|
||||
this.emailError = page.locator('text=Please enter a valid email address');
|
||||
this.usernameError = page.locator(
|
||||
'text=Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.'
|
||||
);
|
||||
this.passwordError = page.locator('text=Must be 5-64 characters long.');
|
||||
|
||||
const newsletterBlock = page.locator('.check-input');
|
||||
this.newsLetterCheckBox = newsletterBlock.getByRole('checkbox', {name: 'newsletter checkbox'});
|
||||
this.newsLetterPrivacyPolicyLink = newsletterBlock.locator('text=Privacy Policy');
|
||||
this.newsLetterUnsubscribeLink = newsletterBlock.locator('text=unsubscribe');
|
||||
|
||||
const agreementBlock = page.locator('.signup-body-card-agreement');
|
||||
this.agreementTermsOfUseLink = agreementBlock.locator('text=Terms of Use');
|
||||
this.agreementPrivacyPolicyLink = agreementBlock.locator('text=Privacy Policy');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.hfroute-header'));
|
||||
this.footer = new components.Footer(page.locator('.hfroute-footer'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
@@ -49,7 +72,7 @@ export default class SignupPage {
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/signup_email');
|
||||
await this.page.goto('/signup_user_complete');
|
||||
}
|
||||
|
||||
async create(user: {email: string; username: string; password: string}, waitForRedirect = true) {
|
||||
|
||||
@@ -19,7 +19,7 @@ test('Intro to channel', async ({pw, pages, axe}) => {
|
||||
|
||||
// # Analyze the page
|
||||
// Disable 'color-contrast' to be addressed by MM-53814
|
||||
const accessibilityScanResults = await axe.builder(page, ['color-contrast']).analyze();
|
||||
const accessibilityScanResults = await axe.builder(page, {disableColorContrast: true}).analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {expect, test} from '@e2e-support/test_fixture';
|
||||
|
||||
test('/login', async ({pw, pages, page, axe}) => {
|
||||
test('/login accessibility quick check', async ({pw, pages, page, axe}) => {
|
||||
// # Go to login page
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const adminConfig = await adminClient.getConfig();
|
||||
@@ -17,3 +17,63 @@ test('/login', async ({pw, pages, page, axe}) => {
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('/login accessibility tab support', async ({pw, pages, page}) => {
|
||||
// # 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();
|
||||
|
||||
// * Should have focused at login input on page load
|
||||
expect(await loginPage.loginInput).toBeFocused();
|
||||
|
||||
// * Should move focus to password input after tab
|
||||
await loginPage.loginInput.press('Tab');
|
||||
expect(await loginPage.passwordInput).toBeFocused();
|
||||
|
||||
// * Should move focus to password toggle button after tab
|
||||
await loginPage.passwordInput.press('Tab');
|
||||
expect(await loginPage.passwordToggleButton).toBeFocused();
|
||||
|
||||
// * Should move focus to forgot password link after tab
|
||||
await loginPage.passwordToggleButton.press('Tab');
|
||||
expect(await loginPage.forgotPasswordLink).toBeFocused();
|
||||
|
||||
// * Should move focus to forgot password link after tab
|
||||
await loginPage.forgotPasswordLink.press('Tab');
|
||||
expect(await loginPage.signInButton).toBeFocused();
|
||||
|
||||
// * Should move focus to about link after tab
|
||||
await loginPage.signInButton.press('Tab');
|
||||
expect(await loginPage.footer.aboutLink).toBeFocused();
|
||||
|
||||
// * Should move focus to privacy policy link after tab
|
||||
await loginPage.footer.aboutLink.press('Tab');
|
||||
expect(await loginPage.footer.privacyPolicyLink).toBeFocused();
|
||||
|
||||
// * Should move focus to terms link after tab
|
||||
await loginPage.footer.privacyPolicyLink.press('Tab');
|
||||
expect(await loginPage.footer.termsLink).toBeFocused();
|
||||
|
||||
// * Should move focus to help link after tab
|
||||
await loginPage.footer.termsLink.press('Tab');
|
||||
expect(await loginPage.footer.helpLink).toBeFocused();
|
||||
|
||||
// * Should move focus to header logo after tab
|
||||
await loginPage.footer.helpLink.press('Tab');
|
||||
expect(await loginPage.header.logo).toBeFocused();
|
||||
|
||||
// * Should move focus to create account link after tab
|
||||
await loginPage.header.logo.press('Tab');
|
||||
expect(await loginPage.createAccountLink).toBeFocused();
|
||||
|
||||
// * Should move focus to create account link after tab
|
||||
await loginPage.createAccountLink.press('Tab');
|
||||
expect(await loginPage.bodyCard).toBeFocused();
|
||||
|
||||
// * Then, should move focus to login body after tab
|
||||
await loginPage.bodyCard.press('Tab');
|
||||
expect(await loginPage.loginInput).toBeFocused();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@e2e-support/test_fixture';
|
||||
|
||||
test('/reset_password accessibility quick check', async ({pages, page, axe}) => {
|
||||
// # Go to reset password page
|
||||
const resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
await resetPasswordPage.goto();
|
||||
await resetPasswordPage.toBeVisible();
|
||||
|
||||
// # Analyze the page
|
||||
const accessibilityScanResults = await axe.builder(resetPasswordPage.page, {disableColorContrast: true}).analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('/reset_password accessibility tab support', async ({pages, page}) => {
|
||||
// # Go to reset password page
|
||||
const resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
await resetPasswordPage.goto();
|
||||
await resetPasswordPage.toBeVisible();
|
||||
|
||||
// * Should have focused at email input on page load
|
||||
expect(await resetPasswordPage.emailInput).toBeFocused();
|
||||
|
||||
// * Should move focus to reset button after tab
|
||||
await resetPasswordPage.emailInput.press('Tab');
|
||||
expect(await resetPasswordPage.resetButton).toBeFocused();
|
||||
|
||||
// * Should move focus to about link after tab
|
||||
await resetPasswordPage.resetButton.press('Tab');
|
||||
expect(await resetPasswordPage.footer.aboutLink).toBeFocused();
|
||||
|
||||
// * Should move focus to privacy policy link after tab
|
||||
await resetPasswordPage.footer.aboutLink.press('Tab');
|
||||
expect(await resetPasswordPage.footer.privacyPolicyLink).toBeFocused();
|
||||
|
||||
// * Should move focus to terms link after tab
|
||||
await resetPasswordPage.footer.privacyPolicyLink.press('Tab');
|
||||
expect(await resetPasswordPage.footer.termsLink).toBeFocused();
|
||||
|
||||
// * Should move focus to help link after tab
|
||||
await resetPasswordPage.footer.termsLink.press('Tab');
|
||||
expect(await resetPasswordPage.footer.helpLink).toBeFocused();
|
||||
|
||||
// * Should move focus to header logo after tab
|
||||
await resetPasswordPage.footer.helpLink.press('Tab');
|
||||
expect(await resetPasswordPage.header.backButton).toBeFocused();
|
||||
|
||||
// * Then, should move focus to email input after tab
|
||||
await resetPasswordPage.header.backButton.press('Tab');
|
||||
expect(await resetPasswordPage.emailInput).toBeFocused();
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@e2e-support/test_fixture';
|
||||
|
||||
test('/signup_user_complete accessibility quick check', async ({pages, page, axe}) => {
|
||||
// # Go to reset password page
|
||||
const signupPage = new pages.SignupPage(page);
|
||||
await signupPage.goto();
|
||||
await signupPage.toBeVisible();
|
||||
|
||||
// # Analyze the page
|
||||
const accessibilityScanResults = await axe
|
||||
.builder(signupPage.page, {disableColorContrast: true, disableLinkInTextBlock: true})
|
||||
.analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('/signup_user_complete accessibility tab support', async ({pages, page}) => {
|
||||
// # Go to reset password page
|
||||
const signupPage = new pages.SignupPage(page);
|
||||
await signupPage.goto();
|
||||
await signupPage.toBeVisible();
|
||||
|
||||
// * Should have focused at email input on page load
|
||||
expect(await signupPage.emailInput).toBeFocused();
|
||||
|
||||
// * Should move focus to username input after tab
|
||||
await signupPage.emailInput.press('Tab');
|
||||
expect(await signupPage.usernameInput).toBeFocused();
|
||||
|
||||
// * Should move focus to password input after tab
|
||||
await signupPage.usernameInput.press('Tab');
|
||||
expect(await signupPage.passwordInput).toBeFocused();
|
||||
|
||||
// * Should move focus to password toggle button after tab
|
||||
await signupPage.passwordInput.press('Tab');
|
||||
expect(await signupPage.passwordToggleButton).toBeFocused();
|
||||
|
||||
// * Should move focus to newsletter checkbox after tab
|
||||
await signupPage.passwordToggleButton.press('Tab');
|
||||
expect(await signupPage.newsLetterCheckBox).toBeFocused();
|
||||
|
||||
// * Should move focus to newsletter privacy policy link after tab
|
||||
await signupPage.newsLetterCheckBox.press('Tab');
|
||||
expect(await signupPage.newsLetterPrivacyPolicyLink).toBeFocused();
|
||||
|
||||
// * Should move focus to newsletter unsubscribe link after tab
|
||||
await signupPage.newsLetterPrivacyPolicyLink.press('Tab');
|
||||
expect(await signupPage.newsLetterUnsubscribeLink).toBeFocused();
|
||||
|
||||
// * Should move focus to agreement terms of use link after tab
|
||||
await signupPage.newsLetterUnsubscribeLink.press('Tab');
|
||||
expect(await signupPage.agreementTermsOfUseLink).toBeFocused();
|
||||
|
||||
// * Should move focus to agreement privacy policy link after tab
|
||||
await signupPage.agreementTermsOfUseLink.press('Tab');
|
||||
expect(await signupPage.agreementPrivacyPolicyLink).toBeFocused();
|
||||
|
||||
// * Should move focus to privacy policy link after tab
|
||||
await signupPage.footer.aboutLink.press('Tab');
|
||||
expect(await signupPage.footer.privacyPolicyLink).toBeFocused();
|
||||
|
||||
// * Should move focus to terms link after tab
|
||||
await signupPage.footer.privacyPolicyLink.press('Tab');
|
||||
expect(await signupPage.footer.termsLink).toBeFocused();
|
||||
|
||||
// * Should move focus to help link after tab
|
||||
await signupPage.footer.termsLink.press('Tab');
|
||||
expect(await signupPage.footer.helpLink).toBeFocused();
|
||||
|
||||
// * Should move focus to header logo after tab
|
||||
await signupPage.footer.helpLink.press('Tab');
|
||||
expect(await signupPage.header.logo).toBeFocused();
|
||||
|
||||
// * Should move focus to header back button after tab
|
||||
await signupPage.header.logo.press('Tab');
|
||||
expect(await signupPage.header.backButton).toBeFocused();
|
||||
|
||||
// * Should move focus to log in link after tab
|
||||
await signupPage.header.backButton.press('Tab');
|
||||
expect(await signupPage.loginLink).toBeFocused();
|
||||
|
||||
// * Should move focus to sign up body after tab
|
||||
await signupPage.loginLink.press('Tab');
|
||||
expect(await signupPage.bodyCard).toBeFocused();
|
||||
|
||||
// * Then, should move focus to email input after tab
|
||||
await signupPage.bodyCard.press('Tab');
|
||||
expect(await signupPage.emailInput).toBeFocused();
|
||||
});
|
||||
@@ -71,6 +71,7 @@ export const IconContainer = styled.button`
|
||||
`;
|
||||
|
||||
interface FormattingIconProps {
|
||||
id?: string;
|
||||
mode: MarkdownMode;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
@@ -129,7 +130,7 @@ const FormattingIcon = (props: FormattingIconProps): JSX.Element => {
|
||||
const bodyAction = (
|
||||
<IconContainer
|
||||
type='button'
|
||||
id={`FormattingControl_${mode}`}
|
||||
id={props.id || `FormattingControl_${mode}`}
|
||||
onClick={onClick}
|
||||
aria-label={buttonAriaLabel}
|
||||
{...otherProps}
|
||||
|
||||
@@ -50,6 +50,7 @@ const AlertBanner = ({
|
||||
children,
|
||||
}: AlertBannerProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const closeText = formatMessage({id: 'alert_banner.tooltipCloseBtn', defaultMessage: 'Close'});
|
||||
const [tooltipId] = useState(`alert_banner_close_btn_tooltip_${Math.random()}`);
|
||||
|
||||
const bannerIcon = useCallback(() => {
|
||||
@@ -110,12 +111,11 @@ const AlertBanner = ({
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='left'
|
||||
overlay={closeBtnTooltip || (
|
||||
<Tooltip id={tooltipId}>
|
||||
{formatMessage({id: 'alert_banner.tooltipCloseBtn', defaultMessage: 'Close'})}
|
||||
</Tooltip>
|
||||
<Tooltip id={tooltipId}>{closeText}</Tooltip>
|
||||
)}
|
||||
>
|
||||
<button
|
||||
aria-label={closeText}
|
||||
className='AlertBanner__closeButton'
|
||||
onClick={onDismiss}
|
||||
>
|
||||
|
||||
@@ -76,6 +76,8 @@ exports[`components/app_bar/app_bar should match snapshot on mount 1`] = `
|
||||
>
|
||||
<div
|
||||
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
fallback_component
|
||||
</div>
|
||||
@@ -241,6 +243,8 @@ exports[`components/app_bar/app_bar should match snapshot on mount when App Bar
|
||||
>
|
||||
<div
|
||||
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
fallback_component
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,11 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
||||
|
||||
const iconUrl = component.iconUrl;
|
||||
let content: React.ReactNode = (
|
||||
<div className='app-bar__icon-inner'>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='app-bar__icon-inner'
|
||||
>
|
||||
<img
|
||||
src={iconUrl}
|
||||
onLoad={onImageLoadComplete}
|
||||
@@ -77,7 +81,11 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
||||
|
||||
if (!iconUrl) {
|
||||
content = (
|
||||
<div className={classNames('app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered', {'app-bar__old-icon--active': isButtonActive})}>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className={classNames('app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered', {'app-bar__old-icon--active': isButtonActive})}
|
||||
>
|
||||
{component.icon}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,11 +25,9 @@ const BackButton = ({url, className, onClick}: Props): JSX.Element => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
id='back_button'
|
||||
className={classNames('signup-header', className)}
|
||||
>
|
||||
<div className={classNames('signup-header', className)}>
|
||||
<Link
|
||||
data-testid='back_button'
|
||||
onClick={onClick}
|
||||
to={url}
|
||||
>
|
||||
|
||||
@@ -43,10 +43,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with about link 1
|
||||
key="about_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About"
|
||||
id="web.footer.about"
|
||||
/>
|
||||
About
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
@@ -98,10 +95,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with all links 1`
|
||||
key="about_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="About"
|
||||
id="web.footer.about"
|
||||
/>
|
||||
About
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
className="footer-link"
|
||||
@@ -110,10 +104,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with all links 1`
|
||||
key="privacy_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="web.footer.privacy"
|
||||
/>
|
||||
Privacy Policy
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
className="footer-link"
|
||||
@@ -122,10 +113,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with all links 1`
|
||||
key="terms_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms"
|
||||
id="web.footer.terms"
|
||||
/>
|
||||
Terms
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
className="footer-link"
|
||||
@@ -134,10 +122,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with all links 1`
|
||||
key="help_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Help"
|
||||
id="web.footer.help"
|
||||
/>
|
||||
Help
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
@@ -235,10 +220,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with help link 1`
|
||||
key="help_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Help"
|
||||
id="web.footer.help"
|
||||
/>
|
||||
Help
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
@@ -290,10 +272,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with privacy poli
|
||||
key="privacy_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Privacy Policy"
|
||||
id="web.footer.privacy"
|
||||
/>
|
||||
Privacy Policy
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
@@ -345,10 +324,7 @@ exports[`components/HeaderFooterTemplate should match snapshot with term of serv
|
||||
key="terms_link"
|
||||
location="header_footer_template"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Terms"
|
||||
id="web.footer.terms"
|
||||
/>
|
||||
Terms
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {ClientConfig} from '@mattermost/types/config';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
config: Partial<ClientConfig> | undefined;
|
||||
@@ -57,10 +57,7 @@ export default class NotLoggedIn extends React.PureComponent<Props> {
|
||||
location='header_footer_template'
|
||||
href={this.props.config.AboutLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='web.footer.about'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
{localizeMessage('web.footer.about', 'About')}
|
||||
</ExternalLink>,
|
||||
);
|
||||
}
|
||||
@@ -74,10 +71,7 @@ export default class NotLoggedIn extends React.PureComponent<Props> {
|
||||
location='header_footer_template'
|
||||
href={this.props.config.PrivacyPolicyLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='web.footer.privacy'
|
||||
defaultMessage='Privacy Policy'
|
||||
/>
|
||||
{localizeMessage('web.footer.privacy', 'Privacy Policy')}
|
||||
</ExternalLink>,
|
||||
);
|
||||
}
|
||||
@@ -91,10 +85,7 @@ export default class NotLoggedIn extends React.PureComponent<Props> {
|
||||
location='header_footer_template'
|
||||
href={this.props.config.TermsOfServiceLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='web.footer.terms'
|
||||
defaultMessage='Terms'
|
||||
/>
|
||||
{localizeMessage('web.footer.terms', 'Terms')}
|
||||
</ExternalLink>,
|
||||
);
|
||||
}
|
||||
@@ -108,10 +99,7 @@ export default class NotLoggedIn extends React.PureComponent<Props> {
|
||||
location='header_footer_template'
|
||||
href={this.props.config.HelpLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='web.footer.help'
|
||||
defaultMessage='Help'
|
||||
/>
|
||||
{localizeMessage('web.footer.help', 'Help')}
|
||||
</ExternalLink>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,8 +363,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
||||
const onWindowFocus = useCallback(() => {
|
||||
if (extraParam === Constants.SIGNIN_VERIFIED && emailParam) {
|
||||
passwordInput.current?.focus();
|
||||
} else {
|
||||
loginIdInput.current?.focus();
|
||||
}
|
||||
}, [emailParam, extraParam]);
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ function PostPriorityPickerOverlay({
|
||||
{...getTooltipReferenceProps()}
|
||||
>
|
||||
<IconContainer
|
||||
id='messagePriority'
|
||||
ref={pickerRef}
|
||||
className={classNames({control: true, active: pickerOpen})}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -594,6 +594,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
return (
|
||||
<CheckInput
|
||||
id='signup-body-card-form-check-newsletter'
|
||||
ariaLabel={formatMessage({id: 'newsletter_optin.checkmark.box', defaultMessage: 'newsletter checkbox'})}
|
||||
name='newsletter'
|
||||
onChange={() => setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)}
|
||||
text={
|
||||
|
||||
@@ -6,6 +6,7 @@ import './check.scss';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
ariaLabel: string;
|
||||
name: string;
|
||||
text: ReactNode;
|
||||
onChange: () => void;
|
||||
@@ -13,14 +14,17 @@ type Props = {
|
||||
}
|
||||
|
||||
function CheckInput(props: Props) {
|
||||
const {id, ariaLabel, text, ...rest} = props;
|
||||
|
||||
return (
|
||||
<div className='check-input'>
|
||||
<input
|
||||
{...props}
|
||||
data-testid={props.id}
|
||||
{...rest}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={id}
|
||||
type='checkbox'
|
||||
/>
|
||||
<span className='text'>{props.text}</span>
|
||||
<span className='text'>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ const PasswordInput = React.forwardRef((
|
||||
<button
|
||||
id='password_toggle'
|
||||
type='button'
|
||||
aria-label={placeHolder}
|
||||
aria-label={formatMessage({id: 'widget.passwordInput.passwordToggle', defaultMessage: 'Show or hide password'})}
|
||||
className='password-input-toggle'
|
||||
onClick={toggleShowPassword}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -4100,6 +4100,7 @@
|
||||
"navbar.viewPinnedPosts": "View Pinned Posts",
|
||||
"newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.",
|
||||
"newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar",
|
||||
"newsletter_optin.checkmark.box": "newsletter checkbox",
|
||||
"newsletter_optin.checkmark.text": "<span>I would like to receive Mattermost security updates via newsletter.</span> By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the <a>Privacy Policy</a> and understand that I can <aa>unsubscribe</aa> at any time",
|
||||
"newsletter_optin.desc": "Sign up at <a>{link}</a>.",
|
||||
"newsletter_optin.title": "Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?",
|
||||
@@ -5589,6 +5590,7 @@
|
||||
"widget.input.required": "This field is required",
|
||||
"widget.passwordInput.createPassword": "Choose a Password",
|
||||
"widget.passwordInput.password": "Password",
|
||||
"widget.passwordInput.passwordToggle": "Show or hide password",
|
||||
"widgets.channels_input.empty": "No channels found",
|
||||
"widgets.channels_input.loading": "Loading",
|
||||
"widgets.users_emails_input.loading": "Loading",
|
||||
|
||||
Ссылка в новой задаче
Block a user