MM-63699: E2E/Accessibility: Settings modal (#33869)
* a11y tests on notificatins settings * refactor settings and channel settings modal, add focus check on settings modal * more a11y and visual tests
|
|
@ -31,6 +31,7 @@ export {
|
|||
ChannelsPostCreate,
|
||||
ChannelsPostEdit,
|
||||
ChannelsPost,
|
||||
ChannelSettingsModal,
|
||||
DraftPost,
|
||||
FindChannelsModal,
|
||||
DeletePostModal,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, Page} from '@playwright/test';
|
||||
import {Locator, Page, expect} from '@playwright/test';
|
||||
export {waitUntil} from 'async-wait-until';
|
||||
|
||||
const visibilityHidden = 'visibility: hidden !important;';
|
||||
const hideTeamHeader = `#sidebarTeamMenuButton {${visibilityHidden}} `;
|
||||
const hidePostHeaderTime = `.post__time {${visibilityHidden}} `;
|
||||
const hidePostProfileIcon = `.profile-icon {${visibilityHidden}} `;
|
||||
const hideKeywordsAndMentionsDesc = `#keywordsAndMentionsDesc {${visibilityHidden}} `;
|
||||
|
||||
export async function hideDynamicChannelsContent(page: Page) {
|
||||
await page.addStyleTag({content: hideTeamHeader + hidePostHeaderTime + hidePostProfileIcon});
|
||||
await page.addStyleTag({
|
||||
content: hideTeamHeader + hidePostHeaderTime + hidePostProfileIcon + hideKeywordsAndMentionsDesc,
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForAnimationEnd(locator: Locator) {
|
||||
|
|
@ -18,3 +21,8 @@ export async function waitForAnimationEnd(locator: Locator) {
|
|||
Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function toBeFocusedWithFocusVisible(locator: Locator) {
|
||||
await expect(locator).toBeFocused();
|
||||
return locator.evaluate((element) => element.matches(':focus-visible'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
initSetup,
|
||||
isOutsideRemoteUserHour,
|
||||
} from './server';
|
||||
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
|
||||
import {toBeFocusedWithFocusVisible, hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
|
||||
import {pages} from './ui/pages';
|
||||
import {matchSnapshot} from './visual';
|
||||
import {stubNotification, waitForNotification} from './mock_browser_api';
|
||||
|
|
@ -81,6 +81,7 @@ export class PlaywrightExtended {
|
|||
readonly initSetup;
|
||||
|
||||
// ./test_action
|
||||
readonly toBeFocusedWithFocusVisible;
|
||||
readonly hideDynamicChannelsContent;
|
||||
readonly waitForAnimationEnd;
|
||||
readonly waitUntil;
|
||||
|
|
@ -134,7 +135,9 @@ export class PlaywrightExtended {
|
|||
this.initSetup = initSetup;
|
||||
this.getAdminClient = getAdminClient;
|
||||
this.isOutsideRemoteUserHour = isOutsideRemoteUserHour;
|
||||
|
||||
// ./test_action
|
||||
this.toBeFocusedWithFocusVisible = toBeFocusedWithFocusVisible;
|
||||
this.hideDynamicChannelsContent = hideDynamicChannelsContent;
|
||||
this.waitForAnimationEnd = waitForAnimationEnd;
|
||||
this.waitUntil = waitUntil;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {Locator, Page, ViewportSize} from '@playwright/test';
|
|||
|
||||
export type TestArgs = {
|
||||
page: Page;
|
||||
locator?: Locator;
|
||||
browserName: string;
|
||||
viewport?: ViewportSize | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import InfoSettings from './info_settings';
|
||||
import ConfigurationSettings from './configuration_settings';
|
||||
|
||||
export default class ChannelSettingsModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly closeButton;
|
||||
|
||||
readonly infoTab;
|
||||
readonly configurationTab;
|
||||
|
||||
readonly infoSettings;
|
||||
readonly configurationSettings;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||
|
||||
this.infoTab = container.getByRole('tab', {name: 'info'});
|
||||
this.configurationTab = container.getByRole('tab', {name: 'configuration'});
|
||||
|
||||
this.infoSettings = new InfoSettings(container.locator('.ChannelSettingsModal__infoTab'));
|
||||
this.configurationSettings = new ConfigurationSettings(
|
||||
container.locator('.ChannelSettingsModal__configurationTab'),
|
||||
);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
getContainerId() {
|
||||
return this.container.getAttribute('id');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.closeButton.click();
|
||||
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async openInfoTab(): Promise<InfoSettings> {
|
||||
await expect(this.infoTab).toBeVisible();
|
||||
await this.infoTab.click();
|
||||
|
||||
await this.infoSettings.toBeVisible();
|
||||
|
||||
return this.infoSettings;
|
||||
}
|
||||
|
||||
async openConfigurationTab(): Promise<ConfigurationSettings> {
|
||||
await expect(this.configurationTab).toBeVisible();
|
||||
await this.configurationTab.click();
|
||||
|
||||
await this.configurationSettings.toBeVisible();
|
||||
|
||||
return this.configurationSettings;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class InfoSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class AdvancedSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,10 @@ export default class NotificationsSettings {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async getContainerId() {
|
||||
return 'notificationsSettings';
|
||||
}
|
||||
|
||||
async expandSection(section: NotificationSettingsSection) {
|
||||
if (section === 'keysWithHighlight') {
|
||||
await this.container.getByText('Keywords That Get Highlighted (without notifications)').click();
|
||||
|
|
@ -3,45 +3,57 @@
|
|||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import AdvancedSettings from './advanced_settings';
|
||||
import DisplaySettings from './display_settings';
|
||||
import NotificationsSettings from './notification_settings';
|
||||
|
||||
import ConfigurationSettings from '@/ui/components/channels/settings/configuration_settings';
|
||||
import NotificationsSettings from './notifications_settings';
|
||||
import SidebarSettings from './sidebar_settings';
|
||||
|
||||
export default class SettingsModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly notificationsSettingsTab;
|
||||
readonly configurationSettingsTab;
|
||||
readonly content;
|
||||
readonly closeButton;
|
||||
|
||||
readonly notificationsTab;
|
||||
readonly displayTab;
|
||||
readonly sidebarTab;
|
||||
readonly advancedTab;
|
||||
|
||||
readonly notificationsSettings;
|
||||
readonly configurationSettings;
|
||||
|
||||
readonly displaySettingsTab;
|
||||
readonly displaySettings;
|
||||
readonly sidebarSettings;
|
||||
readonly advancedSettings;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.notificationsSettingsTab = container.locator('#notificationsButton');
|
||||
this.configurationSettingsTab = container.locator('#configurationButton');
|
||||
this.content = container.locator('.modal-content');
|
||||
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||
|
||||
this.notificationsSettings = new NotificationsSettings(container.locator('#notificationsSettings'));
|
||||
this.configurationSettings = new ConfigurationSettings(
|
||||
container.locator('.ChannelSettingsModal__configurationTab'),
|
||||
this.notificationsTab = container.getByRole('tab', {name: 'notifications'});
|
||||
this.displayTab = container.getByRole('tab', {name: 'display'});
|
||||
this.sidebarTab = container.getByRole('tab', {name: 'sidebar'});
|
||||
this.advancedTab = container.getByRole('tab', {name: 'advanced'});
|
||||
|
||||
this.notificationsSettings = new NotificationsSettings(
|
||||
container.getByRole('tabpanel', {name: 'notifications'}),
|
||||
);
|
||||
|
||||
this.displaySettingsTab = container.locator('#displayButton');
|
||||
this.displaySettings = new DisplaySettings(container.locator('#displaySettings'));
|
||||
this.displaySettings = new DisplaySettings(container.getByRole('tabpanel', {name: 'display'}));
|
||||
this.sidebarSettings = new SidebarSettings(container.getByRole('tabpanel', {name: 'sidebar'}));
|
||||
this.advancedSettings = new AdvancedSettings(container.getByRole('tabpanel', {name: 'advanced'}));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
getContainerId() {
|
||||
return this.container.getAttribute('id');
|
||||
}
|
||||
|
||||
async openNotificationsTab() {
|
||||
await expect(this.notificationsSettingsTab).toBeVisible();
|
||||
await this.notificationsSettingsTab.click();
|
||||
await expect(this.notificationsTab).toBeVisible();
|
||||
await this.notificationsTab.click();
|
||||
|
||||
await this.notificationsSettings.toBeVisible();
|
||||
|
||||
|
|
@ -49,32 +61,35 @@ export default class SettingsModal {
|
|||
}
|
||||
|
||||
async openDisplayTab() {
|
||||
await expect(this.displaySettingsTab).toBeVisible();
|
||||
await this.displaySettingsTab.click();
|
||||
await expect(this.displayTab).toBeVisible();
|
||||
await this.displayTab.click();
|
||||
|
||||
await this.displaySettings.toBeVisible();
|
||||
|
||||
return this.displaySettings;
|
||||
}
|
||||
|
||||
async closeModal() {
|
||||
async openSidebarTab() {
|
||||
await expect(this.sidebarTab).toBeVisible();
|
||||
await this.sidebarTab.click();
|
||||
|
||||
await this.sidebarSettings.toBeVisible();
|
||||
|
||||
return this.sidebarSettings;
|
||||
}
|
||||
|
||||
async openAdvancedTab() {
|
||||
await expect(this.advancedTab).toBeVisible();
|
||||
await this.advancedTab.click();
|
||||
|
||||
await this.advancedSettings.toBeVisible();
|
||||
|
||||
return this.advancedSettings;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.container.getByLabel('Close').click();
|
||||
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async openConfigurationTab(): Promise<ConfigurationSettings> {
|
||||
await expect(this.configurationSettingsTab).toBeVisible();
|
||||
await this.configurationSettingsTab.click();
|
||||
|
||||
await this.configurationSettings.toBeVisible();
|
||||
|
||||
return this.configurationSettings;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const closeButton = this.container.locator('button.close');
|
||||
await expect(closeButton).toBeVisible();
|
||||
await closeButton.click();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SidebarSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ export default class GlobalHeader {
|
|||
readonly accountMenuButton;
|
||||
readonly productSwitchMenu;
|
||||
readonly recentMentionsButton;
|
||||
readonly savedMessagesButton;
|
||||
readonly settingsButton;
|
||||
readonly searchBox;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ export default class GlobalHeader {
|
|||
this.accountMenuButton = container.getByRole('button', {name: "'s account menu"});
|
||||
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
|
||||
this.recentMentionsButton = container.getByRole('button', {name: 'Recent mentions'});
|
||||
this.savedMessagesButton = container.getByRole('button', {name: 'Saved messages'});
|
||||
this.settingsButton = container.getByRole('button', {name: 'Settings'});
|
||||
this.searchBox = container.locator('#searchFormContainer');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import ChannelsPost from './channels/post';
|
|||
import ChannelsCenterView from './channels/center_view';
|
||||
import ChannelsSidebarLeft from './channels/sidebar_left';
|
||||
import ChannelsSidebarRight from './channels/sidebar_right';
|
||||
import ChannelSettingsModal from './channels/channel_settings/channel_settings_modal';
|
||||
import DeletePostModal from './channels/delete_post_modal';
|
||||
import FindChannelsModal from './channels/find_channels_modal';
|
||||
import SettingsModal from './channels/settings/settings_modal';
|
||||
|
|
@ -56,6 +57,7 @@ const components = {
|
|||
ChannelsPostCreate,
|
||||
ChannelsPostEdit,
|
||||
ChannelsPost,
|
||||
ChannelSettingsModal,
|
||||
DraftPost,
|
||||
FindChannelsModal,
|
||||
DeletePostModal,
|
||||
|
|
@ -103,6 +105,7 @@ export {
|
|||
ChannelsPostCreate,
|
||||
ChannelsPostEdit,
|
||||
ChannelsPost,
|
||||
ChannelSettingsModal,
|
||||
DraftPost,
|
||||
FindChannelsModal,
|
||||
DeletePostModal,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
import {expect, Page} from '@playwright/test';
|
||||
import {waitUntil} from 'async-wait-until';
|
||||
|
||||
import {ChannelsPost, components} from '@/ui/components';
|
||||
import SettingsModal from '@/ui/components/channels/settings/settings_modal';
|
||||
import {ChannelsPost, ChannelSettingsModal, SettingsModal, components} from '@/ui/components';
|
||||
import {duration} from '@/util';
|
||||
export default class ChannelsPage {
|
||||
readonly channels = 'Channels';
|
||||
|
|
@ -23,10 +22,12 @@ export default class ChannelsPage {
|
|||
readonly userProfilePopover;
|
||||
readonly messagePriority;
|
||||
|
||||
readonly findChannelsModal;
|
||||
readonly channelSettingsModal;
|
||||
readonly deletePostModal;
|
||||
readonly settingsModal;
|
||||
readonly findChannelsModal;
|
||||
readonly profileModal;
|
||||
readonly settingsModal;
|
||||
|
||||
readonly postContainer;
|
||||
readonly postDotMenu;
|
||||
readonly postReminderMenu;
|
||||
|
|
@ -49,10 +50,11 @@ export default class ChannelsPage {
|
|||
this.userAccountMenuButton = page.getByRole('button', {name: "User's account menu"});
|
||||
|
||||
// Modals
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
this.channelSettingsModal = new ChannelSettingsModal(page.getByRole('dialog', {name: 'Channel Settings'}));
|
||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
this.profileModal = new components.ProfileModal(page.getByRole('dialog', {name: 'Profile'}));
|
||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||
|
||||
// Menus
|
||||
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
||||
|
|
@ -129,11 +131,17 @@ export default class ChannelsPage {
|
|||
return {rootPost, sidebarRight, lastPost};
|
||||
}
|
||||
|
||||
async openChannelSettings(): Promise<SettingsModal> {
|
||||
async openChannelSettings(): Promise<ChannelSettingsModal> {
|
||||
await this.centerView.header.openChannelMenu();
|
||||
await this.page.locator('#channelSettings[role="menuitem"]').click();
|
||||
await this.settingsModal.toBeVisible();
|
||||
await this.channelSettingsModal.toBeVisible();
|
||||
|
||||
return this.channelSettingsModal;
|
||||
}
|
||||
|
||||
async openSettings(): Promise<SettingsModal> {
|
||||
await this.globalHeader.openSettings();
|
||||
await this.settingsModal.toBeVisible();
|
||||
return this.settingsModal;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,15 @@ export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs, opti
|
|||
}
|
||||
|
||||
if (testConfig.snapshotEnabled) {
|
||||
// Visual test with built-in snapshot
|
||||
const filename = testInfo.title.trim().replace(illegalRe, '').replace(/\s/g, '-').trim().toLowerCase();
|
||||
await expect(testArgs.page).toHaveScreenshot(`${filename}.png`, {fullPage: true, ...options});
|
||||
|
||||
// Visual test with built-in snapshot
|
||||
// If locator is provided, take screenshot of the locator instead of the full page
|
||||
if (testArgs.locator) {
|
||||
await expect(testArgs.locator).toHaveScreenshot(`${filename}.png`, {...options});
|
||||
} else {
|
||||
await expect(testArgs.page).toHaveScreenshot(`${filename}.png`, {fullPage: true, ...options});
|
||||
}
|
||||
}
|
||||
|
||||
if (testConfig.percyEnabled) {
|
||||
|
|
|
|||
6
e2e-tests/playwright/package-lock.json
generated
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
"../../webapp/platform/client": {
|
||||
"name": "@mattermost/client",
|
||||
"version": "10.11.0",
|
||||
"version": "10.12.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "28.1.8",
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mattermost/types": "10.11.0",
|
||||
"@mattermost/types": "10.12.0",
|
||||
"typescript": "^4.3.0 || ^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
},
|
||||
"../../webapp/platform/types": {
|
||||
"name": "@mattermost/types",
|
||||
"version": "10.11.0",
|
||||
"version": "10.12.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"check": "npm run lint && npm run prettier && npm run tsc && npm run lint:test-docs",
|
||||
"test": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
||||
"test:ci": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --grep-invert @visual --project=chrome",
|
||||
"test:visual": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --grep @visual",
|
||||
"test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test specs/visual --grep @visual --update-snapshots",
|
||||
"test:slomo": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
|
||||
"percy:docker": "npm run build && cross-env PW_PERCY_ENABLE=true PERCY_BROWSER_EXECUTABLE='/ms-playwright/chromium-1169/chrome-linux/chrome' percy exec -- playwright test specs/visual --grep @visual --project=chrome --project=ipad",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,301 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that focus is properly managed when opening and closing settings modal using keyboard interactions
|
||||
*/
|
||||
test(
|
||||
'manages focus when opening and closing settings modal with keyboard',
|
||||
{tag: ['@accessibility', '@settings']},
|
||||
async ({pw}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const globalHeader = channelsPage.globalHeader;
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings modal should be visible and focus should be on the modal
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.container);
|
||||
|
||||
// # Press Tab and verify focus is on Close button
|
||||
await page.keyboard.press('Tab');
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.closeButton);
|
||||
|
||||
// # Press Enter and verify Settings modal is closed and focus is back on Settings button
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(settingsModal.container).not.toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(globalHeader.settingsButton);
|
||||
|
||||
// # Open Settings modal again
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
|
||||
// # Press Escape and verify Settings modal is closed and focus is back on Settings button
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(settingsModal.container).not.toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(globalHeader.settingsButton);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that users can navigate between settings tabs using arrow keys
|
||||
*/
|
||||
test('navigates between settings tabs using arrow keys', {tag: ['@accessibility', '@settings']}, async ({pw}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await channelsPage.globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings modal should be visible and focus should be on the modal
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.container);
|
||||
|
||||
// # Press Tab twice and verify focus is on Notifications tab and Notifications Settings panel is visible
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(settingsModal.notificationsTab).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.notificationsTab);
|
||||
await expect(settingsModal.notificationsSettings.container).toBeVisible();
|
||||
|
||||
// # Press arrow down key and verify focus is on Display tab and Display Settings panel is visible
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.displayTab);
|
||||
await expect(settingsModal.displaySettings.container).toBeVisible();
|
||||
|
||||
// # Press arrow down key and verify focus is on Sidebar tab and Sidebar Settings panel is visible
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect(settingsModal.sidebarTab).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.sidebarTab);
|
||||
await expect(settingsModal.sidebarSettings.container).toBeVisible();
|
||||
|
||||
// # Press Tab and verify focus is on Advanced tab and Advanced Settings panel is visible
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.advancedTab);
|
||||
await expect(settingsModal.advancedSettings.container).toBeVisible();
|
||||
|
||||
// # Press arrow down key and verify focus is back on Notifications tab and Notifications Settings panel is visible
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect(settingsModal.notificationsTab).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.notificationsTab);
|
||||
await expect(settingsModal.notificationsSettings.container).toBeVisible();
|
||||
|
||||
// # Press arrow up key and verify focus is on Advanced tab and Advanced Settings panel is visible
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(settingsModal.advancedTab).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.advancedTab);
|
||||
await expect(settingsModal.advancedSettings.container).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that notifications settings panel meets WCAG accessibility standards
|
||||
*/
|
||||
test(
|
||||
'passes accessibility scan on notifications settings panel',
|
||||
{tag: ['@accessibility', '@settings']},
|
||||
async ({pw, axe}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await channelsPage.globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings modal on notifications panel should be visible
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
await expect(settingsModal.notificationsSettings.container).toBeVisible();
|
||||
|
||||
// * Analyze the Settings modal with notifications panel
|
||||
const accessibilityScanResults = await axe
|
||||
.builder(page, {disableColorContrast: true})
|
||||
.disableRules([
|
||||
'color-contrast',
|
||||
|
||||
// Known issue: These fail due to the way setting tabs are grouped.
|
||||
'aria-required-children',
|
||||
'aria-required-parent',
|
||||
])
|
||||
.include(settingsModal.getContainerId())
|
||||
.analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that display settings panel meets WCAG accessibility standards
|
||||
*/
|
||||
test(
|
||||
'passes accessibility scan on display settings panel',
|
||||
{tag: ['@accessibility', '@settings']},
|
||||
async ({pw, axe}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await channelsPage.globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings dialog should be visible
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
|
||||
// # Open Display tab
|
||||
await settingsModal.openDisplayTab();
|
||||
|
||||
// * Display Settings panel should be visible
|
||||
await expect(settingsModal.displaySettings.container).toBeVisible();
|
||||
|
||||
// * Analyze the Settings modal with display panel
|
||||
const accessibilityScanResults = await axe
|
||||
.builder(page, {disableColorContrast: true})
|
||||
.disableRules([
|
||||
'color-contrast',
|
||||
|
||||
// Known issue: These fail due to the way setting tabs are grouped.
|
||||
'aria-required-children',
|
||||
'aria-required-parent',
|
||||
])
|
||||
.include(settingsModal.getContainerId())
|
||||
.analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that sidebar settings panel meets WCAG accessibility standards
|
||||
*/
|
||||
test(
|
||||
'passes accessibility scan on sidebar settings panel',
|
||||
{tag: ['@accessibility', '@settings']},
|
||||
async ({pw, axe}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await channelsPage.globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings dialog should be visible
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
|
||||
// # Open Sidebar tab
|
||||
await settingsModal.openSidebarTab();
|
||||
|
||||
// * Sidebar Settings panel should be visible
|
||||
await expect(settingsModal.sidebarSettings.container).toBeVisible();
|
||||
|
||||
// * Analyze the Settings modal with sidebar panel
|
||||
const accessibilityScanResults = await axe
|
||||
.builder(page, {disableColorContrast: true})
|
||||
.disableRules([
|
||||
'color-contrast',
|
||||
|
||||
// Known issue: These fail due to the way setting tabs are grouped.
|
||||
'aria-required-children',
|
||||
'aria-required-parent',
|
||||
])
|
||||
.include(settingsModal.getContainerId())
|
||||
.analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that advanced settings panel meets WCAG accessibility standards
|
||||
*/
|
||||
test(
|
||||
'passes accessibility scan on advanced settings panel',
|
||||
{tag: ['@accessibility', '@settings']},
|
||||
async ({pw, axe}) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await channelsPage.globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings dialog should be visible
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
|
||||
// # Open Advanced tab
|
||||
await settingsModal.openAdvancedTab();
|
||||
|
||||
// * Advanced Settings panel should be visible
|
||||
await expect(settingsModal.advancedSettings.container).toBeVisible();
|
||||
|
||||
// * Analyze the Settings modal with advanced panel
|
||||
const accessibilityScanResults = await axe
|
||||
.builder(page, {disableColorContrast: true})
|
||||
.disableRules([
|
||||
'color-contrast',
|
||||
|
||||
// Known issue: These fail due to the way setting tabs are grouped.
|
||||
'aria-required-children',
|
||||
'aria-required-parent',
|
||||
])
|
||||
.include(settingsModal.getContainerId())
|
||||
.analyze();
|
||||
|
||||
// * Should have no violation
|
||||
expect(accessibilityScanResults.violations).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
|
|
@ -15,36 +15,36 @@ test('Should show channel banner when configured', async ({pw}) => {
|
|||
|
||||
await channelsPage.newChannel(getRandomId(), 'O');
|
||||
|
||||
let settingsModal = await channelsPage.openChannelSettings();
|
||||
let configurationTab = await settingsModal.openConfigurationTab();
|
||||
let channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
let configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
|
||||
await configurationTab.enableChannelBanner();
|
||||
await configurationTab.setChannelBannerText('Example channel banner text');
|
||||
await configurationTab.setChannelBannerTextColor('#77DD88');
|
||||
|
||||
await configurationTab.save();
|
||||
await settingsModal.closeModal();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBanner('Example channel banner text', '#77DD88');
|
||||
|
||||
// Now we'll disable the channel banner
|
||||
settingsModal = await channelsPage.openChannelSettings();
|
||||
configurationTab = await settingsModal.openConfigurationTab();
|
||||
channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
await configurationTab.disableChannelBanner();
|
||||
|
||||
await configurationTab.save();
|
||||
await settingsModal.closeModal();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBannerNotVisible();
|
||||
|
||||
// re-enabling channel banner should already have
|
||||
// the previously configured text and color
|
||||
settingsModal = await channelsPage.openChannelSettings();
|
||||
configurationTab = await settingsModal.openConfigurationTab();
|
||||
channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
await configurationTab.enableChannelBanner();
|
||||
|
||||
await configurationTab.save();
|
||||
await settingsModal.closeModal();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBanner('Example channel banner text', '#77DD88');
|
||||
});
|
||||
|
|
@ -60,15 +60,15 @@ test('Should render markdown', async ({pw}) => {
|
|||
|
||||
await channelsPage.newChannel(getRandomId(), 'O');
|
||||
|
||||
const settingsModal = await channelsPage.openChannelSettings();
|
||||
const configurationTab = await settingsModal.openConfigurationTab();
|
||||
const channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
|
||||
await configurationTab.enableChannelBanner();
|
||||
await configurationTab.setChannelBannerText('**bold** *italic* ~~strikethrough~~');
|
||||
await configurationTab.setChannelBannerTextColor('#77DD88');
|
||||
|
||||
await configurationTab.save();
|
||||
await settingsModal.closeModal();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBannerHasBoldText('bold');
|
||||
await channelsPage.centerView.assertChannelBannerHasItalicText('italic');
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ test('MM-T5465-2 Should highlight the keywords when a message is sent with the k
|
|||
await notificationsSettings.save();
|
||||
|
||||
// # Close the settings modal
|
||||
await settingsModal.closeModal();
|
||||
await settingsModal.close();
|
||||
|
||||
// # Post a message without the keyword
|
||||
const messageWithoutKeyword = 'This message does not contain the keyword';
|
||||
|
|
@ -147,7 +147,7 @@ test('MM-T5465-3 Should highlight the keywords when a message is sent with the k
|
|||
await notificationsSettings.save();
|
||||
|
||||
// # Close the settings modal
|
||||
await settingsModal.closeModal();
|
||||
await settingsModal.close();
|
||||
|
||||
// # Post a message without the keyword
|
||||
const messageWithoutKeyword = 'This message does not contain the keyword';
|
||||
|
|
@ -203,7 +203,7 @@ test('MM-T5465-4 Highlighted keywords should not appear in the Recent Mentions',
|
|||
await notificationsSettings.save();
|
||||
|
||||
// # Close the settings modal
|
||||
await settingsModal.closeModal();
|
||||
await settingsModal.close();
|
||||
|
||||
// # Open the recent mentions
|
||||
await channelsPage.globalHeader.openRecentMentions();
|
||||
|
|
@ -263,7 +263,7 @@ test('MM-T5465-5 Should highlight keywords in message sent from another user', a
|
|||
await notificationsSettings.save();
|
||||
|
||||
// # Close the settings modal
|
||||
await settingsModal.closeModal();
|
||||
await settingsModal.close();
|
||||
|
||||
// * Verify that the keywords are highlighted in the last message received
|
||||
const lastPostWithHighlight = await channelsPage.getLastPost();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Capture snapshots of settings modal and key focused elements on keyboard navigation
|
||||
*/
|
||||
test('settings modal visual check', {tag: ['@visual', '@settings']}, async ({pw, browserName, viewport}, testInfo) => {
|
||||
// # Create and sign in a new user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in a user in new browser context
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
const globalHeader = channelsPage.globalHeader;
|
||||
const settingsModal = channelsPage.settingsModal;
|
||||
|
||||
// # Visit a default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Set focus to Settings button and press Enter
|
||||
await globalHeader.settingsButton.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// * Settings modal should be visible and focus should be on the modal
|
||||
await expect(settingsModal.container).toBeVisible();
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.container);
|
||||
|
||||
const testArgs = {page, browserName, viewport};
|
||||
await pw.hideDynamicChannelsContent(page);
|
||||
await pw.matchSnapshot({...testInfo, title: `${testInfo.title}`}, {...testArgs, locator: settingsModal.content});
|
||||
|
||||
// # Press Tab to move focus to Close button
|
||||
await page.keyboard.press('Tab');
|
||||
// * Verify focus is on Close button
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.closeButton);
|
||||
await pw.matchSnapshot(
|
||||
{...testInfo, title: `${testInfo.title} close button`},
|
||||
{...testArgs, locator: settingsModal.content},
|
||||
);
|
||||
|
||||
// # Press Tab to move focus to Notifications tab
|
||||
await page.keyboard.press('Tab');
|
||||
// * Verify focus is on Notifications tab and Notifications Settings panel is visible
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.notificationsTab);
|
||||
await pw.matchSnapshot(
|
||||
{...testInfo, title: `${testInfo.title} notifications tab`},
|
||||
{...testArgs, locator: settingsModal.content},
|
||||
);
|
||||
|
||||
// # Press arrow down key to move focus to Display tab
|
||||
await page.keyboard.press('ArrowDown');
|
||||
// * Verify focus is on Display tab and Display Settings panel is visible
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.displayTab);
|
||||
await pw.matchSnapshot(
|
||||
{...testInfo, title: `${testInfo.title} display tab`},
|
||||
{...testArgs, locator: settingsModal.content},
|
||||
);
|
||||
|
||||
// # Press arrow down key to move focus to Sidebar tab
|
||||
await page.keyboard.press('ArrowDown');
|
||||
// * Verify focus is on Sidebar tab and Sidebar Settings panel is visible
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.sidebarTab);
|
||||
await pw.matchSnapshot(
|
||||
{...testInfo, title: `${testInfo.title} sidebar tab`},
|
||||
{...testArgs, locator: settingsModal.content},
|
||||
);
|
||||
|
||||
// # Press arrow down key to move focus to Advanced tab
|
||||
await page.keyboard.press('ArrowDown');
|
||||
// * Verify focus is on Advanced tab and Advanced Settings panel is visible
|
||||
await pw.toBeFocusedWithFocusVisible(settingsModal.advancedTab);
|
||||
await pw.matchSnapshot(
|
||||
{...testInfo, title: `${testInfo.title} advanced tab`},
|
||||
{...testArgs, locator: settingsModal.content},
|
||||
);
|
||||
});
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 22 KiB |