From e499decea0a15d63b3e2ff37c0efe0a872d0a1a3 Mon Sep 17 00:00:00 2001 From: sabril <5334504+saturninoabril@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:21:17 +0800 Subject: [PATCH] (test): fix flaky and migrate to playwright (#35156) --- .../accessibility_modals_dialogs_spec.ts | 222 ------------------ .../edit_message_with_attachment_spec.js | 69 ------ .../add_people_to_channel_dialog.spec.ts | 173 ++++++++++++++ .../channels/browse_channels_dialog.spec.ts | 147 ++++++++++++ .../channels/direct_messages_dialog.spec.ts | 136 +++++++++++ .../channels/invite_people_dialog.spec.ts | 115 +++++++++ .../edit_file_attachment.spec.ts | 0 .../edit_message_with_attachment.spec.ts | 59 +++++ 8 files changed, 630 insertions(+), 291 deletions(-) delete mode 100644 e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts delete mode 100644 e2e-tests/cypress/tests/integration/channels/files_and_attachments/edit_message_with_attachment_spec.js create mode 100644 e2e-tests/playwright/specs/accessibility/channels/add_people_to_channel_dialog.spec.ts create mode 100644 e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts create mode 100644 e2e-tests/playwright/specs/accessibility/channels/direct_messages_dialog.spec.ts create mode 100644 e2e-tests/playwright/specs/accessibility/channels/invite_people_dialog.spec.ts rename e2e-tests/playwright/specs/functional/channels/{edit_file_attachments => file_attachments}/edit_file_attachment.spec.ts (100%) create mode 100644 e2e-tests/playwright/specs/functional/channels/file_attachments/edit_message_with_attachment.spec.ts diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts deleted file mode 100644 index 695eb809682..00000000000 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @channels @enterprise @accessibility - -import {Channel} from '@mattermost/types/channels'; -import {Team} from '@mattermost/types/teams'; -import {UserProfile} from '@mattermost/types/users'; - -import * as TIMEOUTS from '../../../../fixtures/timeouts'; - -describe('Verify Accessibility Support in Modals & Dialogs', () => { - let testTeam: Team; - let testChannel: Channel; - let testUser: UserProfile; - let selectedRowText: string; - - before(() => { - // * Check if server has license for Guest Accounts - cy.apiRequireLicenseForFeature('GuestAccounts'); - - cy.apiInitSetup({userPrefix: 'user000a'}).then(({team, channel, user}) => { - testTeam = team; - testChannel = channel; - testUser = user; - - cy.apiCreateUser({prefix: 'user000b'}).then(({user: newUser}) => { - cy.apiAddUserToTeam(testTeam.id, newUser.id).then(() => { - cy.apiAddUserToChannel(testChannel.id, newUser.id); - }); - }); - }); - }); - - beforeEach(() => { - // # Login as sysadmin and visit the town-square - cy.apiAdminLogin(); - cy.visit(`/${testTeam.name}/channels/town-square`); - }); - - it('MM-T1466 Accessibility Support in Direct Messages Dialog screen', () => { - // * Verify the aria-label in create direct message button - cy.uiAddDirectMessage().click(); - - // * Verify the accessibility support in Direct Messages Dialog - cy.findAllByRole('dialog', {name: 'Direct Messages'}).eq(0).within(() => { - cy.findByRole('heading', {name: 'Direct Messages'}); - - // * Verify the accessibility support in search input - cy.findByLabelText('Search for people'). - should('have.attr', 'aria-autocomplete', 'list'); - - // # Search for a text and then check up and down arrow - cy.findByLabelText('Search for people'). - typeWithForce('s'). - wait(TIMEOUTS.HALF_SEC). - typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}'); - cy.get('#multiSelectList').children().eq(2).should('have.class', 'more-modal__row--selected').within(() => { - cy.get('.more-modal__name').invoke('text').then((user) => { - selectedRowText = user.split(' - ')[0].replace('@', ''); - }); - - // * Verify image alt is displayed - cy.get('img.Avatar').should('have.attr', 'alt', 'user profile image'); - }); - - // * Verify if the reader is able to read out the selected row - cy.get('.filtered-user-list div.sr-only:not([role="status"])'). - should('have.attr', 'aria-live', 'polite'). - and('have.attr', 'aria-atomic', 'true'). - invoke('text').then((text) => { - expect(text).equal(selectedRowText); - }); - - // # Search for an invalid text - const additionalSearchTerm = 'somethingwhichdoesnotexist'; - cy.findByLabelText('Search for people').clear(). - typeWithForce(additionalSearchTerm). - wait(TIMEOUTS.HALF_SEC); - - // * Check if reader can read no results - cy.get('.multi-select__wrapper').should('have.attr', 'aria-live', 'polite').and('have.text', `No results found matching ${additionalSearchTerm}`); - }); - }); - - it('MM-T1467 Accessibility Support in Browse Channels Dialog screen', () => { - function getChannelAriaLabel(channel) { - return channel.display_name.toLowerCase() + ', ' + channel.purpose.toLowerCase(); - } - - // # Create atleast 2 channels - let otherChannel; - cy.apiCreateChannel(testTeam.id, 'z_accessibility', 'Z Accessibility', 'O', 'other purpose').then(({channel}) => { - otherChannel = channel; - }); - cy.apiCreateChannel(testTeam.id, 'accessibility', 'Accessibility', 'O', 'some purpose').then(({channel}) => { - cy.apiLogin(testUser).then(() => { - cy.reload(); - - // * Verify the aria-label in more public channels button - cy.uiBrowseOrCreateChannel('Browse channels'); - - // * Verify the accessibility support in More Channels Dialog - cy.findByRole('dialog', {name: 'Browse Channels'}).within(() => { - cy.findByRole('heading', {name: 'Browse Channels'}); - - // * Verify the accessibility support in search input - cy.findByPlaceholderText('Search channels'); - - cy.get('#moreChannelsList').should('be.visible').then((el) => { - return el[0].children.length === 2; - }); - - // # Hide already joined channels - cy.findByText('Hide Joined').click(); - - // # Focus on the Create Channel button and TAB five time - cy.get('#createNewChannelButton').focus().tab().tab().tab().tab().tab(); - - // * Verify channel name is highlighted and reader reads the channel name and channel description - cy.get('#moreChannelsList').within(() => { - const selectedChannel = getChannelAriaLabel(channel); - cy.findByLabelText(selectedChannel).should('be.visible').should('be.focused'); - }); - - // * Press Tab again and verify if focus changes to next row - cy.focused().tab(); - cy.findByLabelText(getChannelAriaLabel(otherChannel)).should('be.focused'); - }); - }); - }); - }); - - it('MM-T1468 Accessibility Support in Add people to Channel Dialog screen', () => { - // # Add atleast 5 users - for (let i = 0; i < 5; i++) { - cy.apiCreateUser().then(({user}) => { - cy.apiAddUserToTeam(testTeam.id, user.id); - }); - } - - // # Visit the test channel, and wait for the page to fully load - cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); - - // # Open Add Members Dialog - cy.uiOpenChannelMenu('Members'); - cy.uiGetButton('Add').click(); - - // * Verify the accessibility support in Add people Dialog - cy.findAllByRole('dialog').eq(0).within(() => { - const modalName = `Add people to ${testChannel.display_name}`; - cy.findByRole('heading', {name: modalName}); - cy.wait(TIMEOUTS.ONE_SEC); - - // * Verify the accessibility support in search input - cy.findByLabelText('Search for people or groups'). - should('have.attr', 'aria-autocomplete', 'list'); - - // # Search for a text and then check up and down arrow - cy.findByLabelText('Search for people or groups'). - wait(TIMEOUTS.HALF_SEC). - typeWithForce('u'). - wait(TIMEOUTS.HALF_SEC). - typeWithForce('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}'); - cy.get('#multiSelectList'). - children().eq(2). - should('have.class', 'more-modal__row--selected'). - within(() => { - cy.get('.more-modal__name').invoke('text').then((user) => { - selectedRowText = user.split(' - ')[0].replace('@', ''); - }); - - // * Verify image alt is displayed - cy.get('img.Avatar').should('have.attr', 'alt', 'user profile image'); - }); - - // * Verify if the reader is able to read out the selected row - cy.get('.filtered-user-list div.sr-only:not([role="status"])'). - should('have.attr', 'aria-live', 'polite'). - and('have.attr', 'aria-atomic', 'true'). - invoke('text').then((text) => { - // Check that the readout starts with the selected user since it may be followed by - // "Already in Channel" depending on which user was selected - expect(text).to.match(new RegExp(`^${selectedRowText}\\b`)); - }); - - // # Search for an invalid text and check if reader can read no results - cy.findByLabelText('Search for people or groups'). - typeWithForce('somethingwhichdoesnotexist'). - wait(TIMEOUTS.HALF_SEC); - - // * Check if reader can read no results - cy.get('.custom-no-options-message'). - should('be.visible'). - and('contain', 'No matches found - Invite them to the team'); - }); - }); - - it('MM-T1515 Verify Accessibility Support in Invite People Flow', () => { - // # Open Invite People - cy.uiGetLHSHeader().click(); - cy.get("#sidebarTeamMenu li:contains('Invite people')").should('be.visible').click(); - - // * Verify accessibility support in Invite People Dialog - cy.findByTestId('invitationModal').should('have.attr', 'aria-modal', 'true').and('have.attr', 'aria-labelledby', 'invitation_modal_title').and('have.attr', 'role', 'dialog'); - cy.get('#invitation_modal_title').should('be.visible').and('contain.text', 'Invite people to'); - - // # Press tab - cy.get('button.icon-close').focus().tab({shift: true}).tab(); - - // * Verify tab focuses on close button - cy.get('button.icon-close').should('have.attr', 'aria-label', 'Close').and('be.focused'); - }); -}); - diff --git a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/edit_message_with_attachment_spec.js b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/edit_message_with_attachment_spec.js deleted file mode 100644 index c0cebbdb12f..00000000000 --- a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/edit_message_with_attachment_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @channels @files_and_attachments - -import * as TIMEOUTS from '../../../fixtures/timeouts'; - -describe('Edit Message with Attachment', () => { - before(() => { - // # Enable Link Previews - cy.apiUpdateConfig({ - ServiceSettings: { - EnableLinkPreviews: true, - }, - }); - - // # Create new team and new user and visit off-topic channel - cy.apiInitSetup({loginAfter: true}).then(({offTopicUrl}) => { - cy.visit(offTopicUrl); - }); - }); - - it('MM-T2268 - Edit Message with Attachment', () => { - // # Upload file - cy.get('#fileUploadInput').attachFile('mattermost-icon.png'); - - // # Wait for file to upload - cy.wait(TIMEOUTS.TWO_SEC); - - // # Post message - cy.postMessage('Test'); - - cy.getLastPost().within(() => { - // * Posted message should be correct - cy.get('.post-message__text').should('contain.text', 'Test'); - - // * Attachment should exist - cy.get('.file-view--single').should('exist'); - - // * Edited indicator should not exist - cy.get('.post-edited__indicator').should('not.exist'); - }); - - // # Open the edit dialog - cy.uiGetPostTextBox().type('{uparrow}'); - - // # Add some more text and save - cy.get('#edit_textbox').type(' with some edit'); - cy.get('#edit_textbox').type('{enter}').wait(TIMEOUTS.HALF_SEC); - - cy.getLastPost().within(() => { - // * New text should show - cy.get('.post-message__text').should('contain.text', 'Test with some edit'); - - // * Attachment should still exist - cy.get('.file-view--single').should('exist'); - - // * Edited indicator should exist - cy.get('.post-edited__indicator').should('exist'); - }); - }); -}); diff --git a/e2e-tests/playwright/specs/accessibility/channels/add_people_to_channel_dialog.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/add_people_to_channel_dialog.spec.ts new file mode 100644 index 00000000000..39880b82781 --- /dev/null +++ b/e2e-tests/playwright/specs/accessibility/channels/add_people_to_channel_dialog.spec.ts @@ -0,0 +1,173 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify accessibility support in Add people to Channel Dialog screen + */ +test( + 'MM-T1468 Accessibility Support in Add people to Channel Dialog screen', + {tag: ['@accessibility', '@add_people_channel']}, + async ({pw}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, adminUser, adminClient} = await pw.initSetup(); + + // # Create a channel in the team + const channel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + displayName: 'Test Channel', + name: 'test-channel', + }), + ); + + // # Create additional users and add to team + for (let i = 0; i < 5; i++) { + const newUser = await adminClient.createUser(await pw.random.user(), '', ''); + await adminClient.addToTeam(team.id, newUser.id); + } + + // # Log in as admin + const {page, channelsPage} = await pw.testBrowser.login(adminUser); + + // # Visit the test channel + await channelsPage.goto(team.name, channel.name); + await channelsPage.toBeVisible(); + + // # Open channel menu and click Members + await channelsPage.centerView.header.openChannelMenu(); + const membersMenuItem = page.locator('#channelMembers'); + await membersMenuItem.click(); + + // # Click the Add people button + const addButton = page.getByRole('button', {name: 'Add people'}); + await addButton.click(); + + // * Verify the Add people dialog is visible + const dialog = page.getByRole('dialog').first(); + await expect(dialog).toBeVisible(); + + // * Verify the heading with channel name + const modalName = `Add people to ${channel.display_name}`; + await expect(dialog.getByRole('heading', {name: modalName})).toBeVisible(); + await pw.wait(pw.duration.one_sec); + + // * Verify the search input has proper accessibility attributes + const searchInput = dialog.getByLabel('Search for people or groups'); + await expect(searchInput).toBeVisible(); + await expect(searchInput).toHaveAttribute('aria-autocomplete', 'list'); + + // # Search for a text and navigate with arrow keys + await pw.wait(pw.duration.half_sec); + await searchInput.fill('u'); + await pw.wait(pw.duration.half_sec); + + // # Navigate down through the list + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + + // * Verify the selected row has the correct class + const selectedRow = dialog.locator('#multiSelectList').locator('.more-modal__row--selected'); + await expect(selectedRow).toBeVisible(); + + // * Verify image alt is displayed for user profile + const avatar = selectedRow.locator('img.Avatar'); + await expect(avatar).toHaveAttribute('alt', 'user profile image'); + + // * Verify screen reader live region exists and has proper attributes + const srOnlyRegion = dialog.locator('.filtered-user-list div.sr-only:not([role="status"])'); + await expect(srOnlyRegion).toHaveAttribute('aria-live', 'polite'); + await expect(srOnlyRegion).toHaveAttribute('aria-atomic', 'true'); + + // # Search for an invalid text + await searchInput.fill('somethingwhichdoesnotexist'); + await pw.wait(pw.duration.half_sec); + + // * Check if the no results message is displayed with proper accessibility + const noResultsWrapper = dialog.locator('.multi-select__wrapper'); + await expect(noResultsWrapper).toHaveAttribute('aria-live', 'polite'); + const noResultsMessage = dialog.locator('.no-channel-message .primary-message'); + await expect(noResultsMessage).toBeVisible(); + await expect(noResultsMessage).toContainText('No results found matching'); + }, +); + +/** + * @objective Verify Add people to Channel dialog passes accessibility scan and matches aria-snapshot + */ +test( + 'accessibility scan and aria-snapshot of Add people to Channel dialog', + {tag: ['@accessibility', '@add_people_channel', '@snapshots']}, + async ({pw, axe}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, adminUser, adminClient} = await pw.initSetup(); + + // # Create a channel in the team + const channel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + displayName: 'Test Channel', + name: 'test-channel', + }), + ); + + // # Create additional users and add to team + for (let i = 0; i < 3; i++) { + const newUser = await adminClient.createUser(await pw.random.user(), '', ''); + await adminClient.addToTeam(team.id, newUser.id); + } + + // # Log in as admin + const {page, channelsPage} = await pw.testBrowser.login(adminUser); + + // # Visit the test channel + await channelsPage.goto(team.name, channel.name); + await channelsPage.toBeVisible(); + + // # Open channel menu and click Members + await channelsPage.centerView.header.openChannelMenu(); + const membersMenuItem = page.locator('#channelMembers'); + await membersMenuItem.click(); + + // # Click the Add people button + const addButton = page.getByRole('button', {name: 'Add people'}); + await addButton.click(); + + // * Verify the Add people dialog is visible + const dialog = page.getByRole('dialog').first(); + await expect(dialog).toBeVisible(); + await pw.wait(pw.duration.one_sec); + + // * Verify aria snapshot of Add people to Channel dialog + await expect(dialog).toMatchAriaSnapshot(` + - dialog "Add people to Test Channel": + - document: + - heading "Add people to Test Channel" [level=1] + - button "Close" + - log + - text: Search for people or groups + - combobox "Search for people or groups" + - button "Cancel" + - button "Add" + `); + + // * Analyze the Add people dialog for accessibility issues + const accessibilityScanResults = await axe + .builder(page, {disableColorContrast: true}) + .include('[role="dialog"]') + .analyze(); + + // * Should have no violations + expect(accessibilityScanResults.violations).toHaveLength(0); + }, +); diff --git a/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts new file mode 100644 index 00000000000..2c3c890f185 --- /dev/null +++ b/e2e-tests/playwright/specs/accessibility/channels/browse_channels_dialog.spec.ts @@ -0,0 +1,147 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify accessibility support in Browse Channels Dialog screen + */ +test( + 'MM-T1467 Accessibility Support in Browse Channels Dialog screen', + {tag: ['@accessibility', '@browse_channels']}, + async ({pw}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, user, adminClient} = await pw.initSetup(); + + // # Create two channels with purposes for testing aria-label + const channel1 = await adminClient.createChannel({ + team_id: team.id, + name: 'accessibility-' + Date.now(), + display_name: 'Accessibility', + type: 'O', + purpose: 'some purpose', + }); + + const channel2 = await adminClient.createChannel({ + team_id: team.id, + name: 'z-accessibility-' + Date.now(), + display_name: 'Z Accessibility', + type: 'O', + purpose: 'other purpose', + }); + + // # Log in as regular user + const {page, channelsPage} = await pw.testBrowser.login(user); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Click on Browse or Create Channel button and then Browse Channels + await channelsPage.sidebarLeft.browseOrCreateChannelButton.click(); + const browseChannelsMenuItem = page.locator('#browseChannelsMenuItem'); + await browseChannelsMenuItem.click(); + + // * Verify the Browse Channels dialog is visible + const dialog = page.getByRole('dialog', {name: 'Browse Channels'}); + await expect(dialog).toBeVisible(); + + // * Verify the heading + await expect(dialog.getByRole('heading', {name: 'Browse Channels'})).toBeVisible(); + + // * Verify the search input exists + const searchInput = dialog.getByPlaceholder('Search channels'); + await expect(searchInput).toBeVisible(); + + // # Wait for channel list to load + const channelList = dialog.locator('#moreChannelsList'); + await expect(channelList).toBeVisible(); + + // # Hide already joined channels + const hideJoinedCheckbox = dialog.getByText('Hide Joined'); + await hideJoinedCheckbox.click(); + + // # Focus on Create Channel button and tab through elements + const createChannelButton = dialog.locator('#createNewChannelButton'); + await createChannelButton.focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + // * Verify channel name is highlighted and has proper aria-label + const channel1AriaLabel = `${channel1.display_name.toLowerCase()}, ${channel1.purpose.toLowerCase()}`; + const channel1Item = dialog.getByLabel(channel1AriaLabel); + await expect(channel1Item).toBeVisible(); + await expect(channel1Item).toBeFocused(); + + // # Press Tab again to move to next channel + await page.keyboard.press('Tab'); + + // * Verify focus moved to next channel + const channel2AriaLabel = `${channel2.display_name.toLowerCase()}, ${channel2.purpose.toLowerCase()}`; + const channel2Item = dialog.getByLabel(channel2AriaLabel); + await expect(channel2Item).toBeFocused(); + }, +); + +/** + * @objective Verify Browse Channels dialog passes accessibility scan and matches aria-snapshot + */ +test( + 'accessibility scan and aria-snapshot of Browse Channels dialog', + {tag: ['@accessibility', '@browse_channels', '@snapshots']}, + async ({pw, axe}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, user} = await pw.initSetup(); + + // # Log in as regular user + const {page, channelsPage} = await pw.testBrowser.login(user); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Click on Browse or Create Channel button and then Browse Channels + await channelsPage.sidebarLeft.browseOrCreateChannelButton.click(); + const browseChannelsMenuItem = page.locator('#browseChannelsMenuItem'); + await browseChannelsMenuItem.click(); + + // * Verify the Browse Channels dialog is visible + const dialog = page.getByRole('dialog', {name: 'Browse Channels'}); + await expect(dialog).toBeVisible(); + await pw.wait(pw.duration.one_sec); + + // * Verify aria snapshot of Browse Channels dialog + await expect(dialog).toMatchAriaSnapshot(` + - dialog "Browse Channels": + - document: + - heading "Browse Channels" [level=1] + - button "Create New Channel" + - button "Close" + - textbox "Search Channels" + - /text: \\d+ Results/ + - /status: \\d+ Results/ + - status: Channel type filter set to All + - button "Channel type filter" + - checkbox "Hide joined channels": Hide Joined + - search + `); + + // * Analyze the Browse Channels dialog for accessibility issues + const accessibilityScanResults = await axe + .builder(page, {disableColorContrast: true}) + .include('[role="dialog"]') + .analyze(); + + // * Should have no violations + expect(accessibilityScanResults.violations).toHaveLength(0); + }, +); diff --git a/e2e-tests/playwright/specs/accessibility/channels/direct_messages_dialog.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/direct_messages_dialog.spec.ts new file mode 100644 index 00000000000..63e208caa50 --- /dev/null +++ b/e2e-tests/playwright/specs/accessibility/channels/direct_messages_dialog.spec.ts @@ -0,0 +1,136 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify accessibility support in Direct Messages Dialog screen + */ +test( + 'MM-T1466 Accessibility Support in Direct Messages Dialog screen', + {tag: ['@accessibility', '@direct_messages']}, + async ({pw}) => { + // # Skip test if no license for Guest Accounts + await pw.skipIfNoLicense(); + + // # Initialize setup with admin and another user + const {team, adminUser, adminClient} = await pw.initSetup(); + + // # Create additional user and add to team + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); + await adminClient.addToTeam(team.id, user2.id); + + // # Log in as admin + const {page, channelsPage} = await pw.testBrowser.login(adminUser); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Click on the Write a direct message button to open the Direct Messages dialog + const writeDirectMessageButton = page.getByRole('button', {name: 'Write a direct message'}); + await writeDirectMessageButton.click(); + + // * Verify the Direct Messages dialog is visible + const dialog = page.getByRole('dialog', {name: 'Direct Messages'}); + await expect(dialog).toBeVisible(); + + // * Verify the heading + await expect(dialog.getByRole('heading', {name: 'Direct Messages'})).toBeVisible(); + + // * Verify the search input has proper accessibility attributes + const searchInput = dialog.getByLabel('Search for people'); + await expect(searchInput).toBeVisible(); + await expect(searchInput).toHaveAttribute('aria-autocomplete', 'list'); + + // # Search for a text and navigate with arrow keys + await searchInput.fill('s'); + await pw.wait(pw.duration.half_sec); + + // # Navigate down through the list + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + + // * Verify the selected row has the correct class + const selectedRow = dialog.locator('#multiSelectList').locator('.more-modal__row--selected'); + await expect(selectedRow).toBeVisible(); + + // * Verify image alt is displayed for user profile + const avatar = selectedRow.locator('img.Avatar'); + await expect(avatar).toHaveAttribute('alt', 'user profile image'); + + // * Verify screen reader live region exists and has proper attributes + const srOnlyRegion = dialog.locator('.filtered-user-list div.sr-only:not([role="status"])'); + await expect(srOnlyRegion).toHaveAttribute('aria-live', 'polite'); + await expect(srOnlyRegion).toHaveAttribute('aria-atomic', 'true'); + + // # Search for an invalid text + const invalidSearchTerm = 'somethingwhichdoesnotexist'; + await searchInput.clear(); + await searchInput.fill(invalidSearchTerm); + await pw.wait(pw.duration.half_sec); + + // * Check if the no results message is displayed with proper accessibility + const noResultsWrapper = dialog.locator('.multi-select__wrapper'); + await expect(noResultsWrapper).toHaveAttribute('aria-live', 'polite'); + await expect(noResultsWrapper).toContainText(`No results found matching ${invalidSearchTerm}`); + }, +); + +/** + * @objective Verify Direct Messages dialog passes accessibility scan and matches aria-snapshot + */ +test( + 'accessibility scan and aria-snapshot of Direct Messages dialog', + {tag: ['@accessibility', '@direct_messages', '@snapshots']}, + async ({pw, axe}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, user} = await pw.initSetup(); + + // # Log in as admin + const {page, channelsPage} = await pw.testBrowser.login(user); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Click on the Write a direct message button to open the Direct Messages dialog + const writeDirectMessageButton = page.getByRole('button', {name: 'Write a direct message'}); + await writeDirectMessageButton.click(); + + // * Verify the Direct Messages dialog is visible + const dialog = page.getByRole('dialog', {name: 'Direct Messages'}); + await expect(dialog).toBeVisible(); + await pw.wait(pw.duration.one_sec); + + // * Verify aria snapshot of Direct Messages dialog (key structural elements only) + await expect(dialog).toMatchAriaSnapshot(` + - dialog "Direct Messages": + - document: + - heading "Direct Messages" [level=1] + - button "Close" + - application: + - log + - text: Search for people + - combobox "Search for people" + - button "Go" + `); + + // * Analyze the Direct Messages dialog for accessibility issues + const accessibilityScanResults = await axe + .builder(page, {disableColorContrast: true}) + .include('[role="dialog"]') + // TODO: Address scrollable-region-focusable violation in the Direct Messages dialog + // The multiSelectList and sr-only status elements need to be keyboard accessible + .disableRules(['scrollable-region-focusable']) + .analyze(); + + // * Should have no violations + expect(accessibilityScanResults.violations).toHaveLength(0); + }, +); diff --git a/e2e-tests/playwright/specs/accessibility/channels/invite_people_dialog.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/invite_people_dialog.spec.ts new file mode 100644 index 00000000000..5c2d74613c4 --- /dev/null +++ b/e2e-tests/playwright/specs/accessibility/channels/invite_people_dialog.spec.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify accessibility support in Invite People Flow + */ +test( + 'MM-T1515 Verify Accessibility Support in Invite People Flow', + {tag: ['@accessibility', '@invite_people']}, + async ({pw}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, adminUser} = await pw.initSetup(); + + // # Log in as admin + const {page, channelsPage} = await pw.testBrowser.login(adminUser); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Open team menu and click Invite people + await channelsPage.sidebarLeft.teamMenuButton.click(); + await channelsPage.teamMenu.toBeVisible(); + + const invitePeopleMenuItem = page.locator("#sidebarTeamMenu li:has-text('Invite people')"); + await invitePeopleMenuItem.click(); + + // * Verify the Invite People modal has proper accessibility attributes + const inviteModal = page.getByTestId('invitationModal'); + await expect(inviteModal).toBeVisible(); + await expect(inviteModal).toHaveAttribute('aria-modal', 'true'); + await expect(inviteModal).toHaveAttribute('aria-labelledby', 'invitation_modal_title'); + await expect(inviteModal).toHaveAttribute('role', 'dialog'); + + // * Verify the modal title is visible and contains correct text + const modalTitle = page.locator('#invitation_modal_title'); + await expect(modalTitle).toBeVisible(); + await expect(modalTitle).toContainText('Invite people to'); + + // # Get the close button and verify accessibility + const closeButton = inviteModal.locator('button.icon-close'); + await expect(closeButton).toHaveAttribute('aria-label', 'Close'); + + // # Focus on close button and verify tab navigation works + await closeButton.focus(); + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Tab'); + + // * Verify focus returns to close button + await expect(closeButton).toBeFocused(); + }, +); + +/** + * @objective Verify Invite People dialog passes accessibility scan and matches aria-snapshot + */ +test( + 'accessibility scan and aria-snapshot of Invite People dialog', + {tag: ['@accessibility', '@invite_people', '@snapshots']}, + async ({pw, axe}) => { + // # Skip test if no license + await pw.skipIfNoLicense(); + + // # Initialize setup + const {team, user} = await pw.initSetup(); + + // # Log in as user + const {page, channelsPage} = await pw.testBrowser.login(user); + + // # Visit town-square channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Open team menu and click Invite people + await channelsPage.sidebarLeft.teamMenuButton.click(); + await channelsPage.teamMenu.toBeVisible(); + + const invitePeopleMenuItem = page.locator("#sidebarTeamMenu li:has-text('Invite people')"); + await invitePeopleMenuItem.click(); + + // * Verify the Invite People modal is visible + const inviteModal = page.getByTestId('invitationModal'); + await expect(inviteModal).toBeVisible(); + await pw.wait(pw.duration.one_sec); + + // * Verify aria snapshot of Invite People dialog (key structural elements only) + await expect(inviteModal).toMatchAriaSnapshot(` + - dialog: + - document: + - heading [level=1] + - button + - text: "To:" + - log + - text: Add members + - combobox "Invite People" + - listbox + - button + - button "Invite" [disabled] + `); + + // * Analyze the Invite People dialog for accessibility issues + const accessibilityScanResults = await axe + .builder(page, {disableColorContrast: true}) + .include('[data-testid="invitationModal"]') + .analyze(); + + // * Should have no violations + expect(accessibilityScanResults.violations).toHaveLength(0); + }, +); diff --git a/e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts b/e2e-tests/playwright/specs/functional/channels/file_attachments/edit_file_attachment.spec.ts similarity index 100% rename from e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts rename to e2e-tests/playwright/specs/functional/channels/file_attachments/edit_file_attachment.spec.ts diff --git a/e2e-tests/playwright/specs/functional/channels/file_attachments/edit_message_with_attachment.spec.ts b/e2e-tests/playwright/specs/functional/channels/file_attachments/edit_message_with_attachment.spec.ts new file mode 100644 index 00000000000..0a157a73dfa --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/file_attachments/edit_message_with_attachment.spec.ts @@ -0,0 +1,59 @@ +// 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 users can edit a message that has an attachment, + * the attachment is preserved after edit, and the edited indicator appears. + */ +test('MM-T2268 Edit Message with Attachment', async ({pw}) => { + // # Initialize user and login + const {user} = await pw.initSetup(); + const {channelsPage, page} = await pw.testBrowser.login(user); + + // # Navigate to channels page + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // # Post a message with an attachment + await channelsPage.postMessage('Test', ['mattermost.png']); + + // # Get the last post and verify content + const post = await channelsPage.getLastPost(); + await post.toBeVisible(); + + // * Verify the posted message is correct + await post.toContainText('Test'); + + // * Verify attachment exists (image thumbnail) + const attachment = post.container.getByLabel(/file thumbnail/i); + await expect(attachment).toBeVisible(); + + // * Verify edited indicator does not exist initially + const postId = await post.getId(); + await expect(channelsPage.centerView.editedPostIcon(postId)).not.toBeVisible(); + + // # Focus on the post textbox and press Up arrow to open edit dialog + await channelsPage.centerView.postCreate.input.focus(); + await page.keyboard.press('ArrowUp'); + + // # Verify edit mode is active and add more text + await channelsPage.centerView.postEdit.toBeVisible(); + await channelsPage.centerView.postEdit.input.fill('Test with some edit'); + await channelsPage.centerView.postEdit.sendMessage(); + + // # Get the updated post + const updatedPost = await channelsPage.getLastPost(); + await updatedPost.toBeVisible(); + + // * Verify the new text shows + await updatedPost.toContainText('Test with some edit'); + + // * Verify attachment still exists (image thumbnail) + const updatedAttachment = updatedPost.container.getByLabel(/file thumbnail/i); + await expect(updatedAttachment).toBeVisible(); + + // * Verify edited indicator now exists + await expect(channelsPage.centerView.editedPostIcon(postId)).toBeVisible(); +});