diff --git a/e2e-tests/playwright/lib/src/index.ts b/e2e-tests/playwright/lib/src/index.ts index acafc87bc85..514df2bb21d 100644 --- a/e2e-tests/playwright/lib/src/index.ts +++ b/e2e-tests/playwright/lib/src/index.ts @@ -51,14 +51,7 @@ export { ScheduledDraftModal, ScheduledPost, SendMessageNowModal, - SystemConsoleSidebar, - SystemConsoleNavbar, - SystemUsers, - SystemUsersFilterPopover, - SystemUsersFilterMenu, - SystemUsersColumnToggleMenu, SystemConsoleFeatureDiscovery, - SystemConsoleMobileSecurity, MessagePriority, UserProfilePopover, UserAccountMenu, diff --git a/e2e-tests/playwright/lib/src/ui/components/index.ts b/e2e-tests/playwright/lib/src/ui/components/index.ts index b3e3ca2c69b..84c6f0bfac6 100644 --- a/e2e-tests/playwright/lib/src/ui/components/index.ts +++ b/e2e-tests/playwright/lib/src/ui/components/index.ts @@ -1,157 +1,189 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import ChannelsHeader from './channels/header'; -import ChannelsAppBar from './channels/app_bar'; -import ChannelsPostCreate from './channels/post_create'; -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 InvitePeopleModal from './channels/invite_people_modal'; -import SettingsModal from './channels/settings/settings_modal'; +// Shared / Global Components import Footer from './footer'; import GlobalHeader from './global_header'; -import SearchBox from './channels/search_box'; import MainHeader from './main_header'; -import PostDotMenu from './channels/post_dot_menu'; -import PostReminderMenu from './channels/post_reminder_menu'; -import PostMenu from './channels/post_menu'; -import ThreadFooter from './channels/thread_footer'; -import EmojiGifPicker from './channels/emoji_gif_picker'; -import GenericConfirmModal from './channels/generic_confirm_modal'; -import MessagePriority from './channels/message_priority'; -import ScheduleMessageMenu from './channels/schedule_message_menu'; -import ScheduleMessageModal from './channels/schedule_message_modal'; -import ScheduledPostIndicator from './channels/scheduled_post_indicator'; -import ScheduledDraftModal from './channels/scheduled_draft_modal'; import UserAccountMenu from './user_account_menu'; -import TeamMenu from './channels/team_menu'; -import TeamSettingsModal from './channels/team_settings/team_settings_modal'; -import ProfileModal from './channels/profile_modal'; -import UserProfilePopover from './channels/user_profile_popover'; -import SystemConsoleSidebar from './system_console/sidebar'; -import SystemConsoleNavbar from './system_console/navbar'; -import SystemUsers from './system_console/sections/system_users/system_users'; -import SystemUsersFilterPopover from './system_console/sections/system_users/filter_popover'; -import SystemUsersFilterMenu from './system_console/sections/system_users/filter_menu'; -import SystemUsersColumnToggleMenu from './system_console/sections/system_users/column_toggle_menu'; +// Channels Components +import ChannelsAppBar from './channels/app_bar'; +import ChannelsCenterView from './channels/center_view'; +import ChannelsHeader from './channels/header'; +import ChannelsPost from './channels/post'; +import ChannelsPostCreate from './channels/post_create'; import ChannelsPostEdit from './channels/post_edit'; +import ChannelSettingsModal from './channels/channel_settings/channel_settings_modal'; +import ChannelsSidebarLeft from './channels/sidebar_left'; +import ChannelsSidebarRight from './channels/sidebar_right'; import DeletePostConfirmationDialog from './channels/delete_post_confirmation_dialog'; -import RestorePostConfirmationDialog from './channels/restore_post_confirmation_dialog'; -import SystemConsoleFeatureDiscovery from './system_console/sections/system_users/feature_discovery'; -import SystemConsoleMobileSecurity from './system_console/sections/system_users/mobile_security'; -import SystemConsoleNotifications from './system_console/sections/site_configuration/notifications'; -import ScheduledPost from './channels/scheduled_post'; -import SendMessageNowModal from './channels/send_message_now_modal'; +import DeletePostModal from './channels/delete_post_modal'; import DeleteScheduledPostModal from './channels/delete_scheduled_post_modal'; import DraftPost from './channels/draft_post'; +import EmojiGifPicker from './channels/emoji_gif_picker'; +import FindChannelsModal from './channels/find_channels_modal'; import FlagPostConfirmationDialog from './channels/flag_post_confirmation_dialog'; +import GenericConfirmModal from './channels/generic_confirm_modal'; +import InvitePeopleModal from './channels/invite_people_modal'; +import MessagePriority from './channels/message_priority'; +import PostDotMenu from './channels/post_dot_menu'; +import PostMenu from './channels/post_menu'; +import PostReminderMenu from './channels/post_reminder_menu'; +import ProfileModal from './channels/profile_modal'; +import RestorePostConfirmationDialog from './channels/restore_post_confirmation_dialog'; +import ScheduledDraftModal from './channels/scheduled_draft_modal'; +import ScheduledPost from './channels/scheduled_post'; +import ScheduledPostIndicator from './channels/scheduled_post_indicator'; +import ScheduleMessageMenu from './channels/schedule_message_menu'; +import ScheduleMessageModal from './channels/schedule_message_modal'; +import SearchBox from './channels/search_box'; +import SendMessageNowModal from './channels/send_message_now_modal'; +import SettingsModal from './channels/settings/settings_modal'; +import TeamMenu from './channels/team_menu'; +import TeamSettingsModal from './channels/team_settings/team_settings_modal'; +import ThreadFooter from './channels/thread_footer'; +import UserProfilePopover from './channels/user_profile_popover'; +// System Console Components +import {AdminSectionPanel, DropdownSetting, RadioSetting, TextInputSetting} from './system_console/base_components'; +import DelegatedGranularAdministration from './system_console/sections/user_management/delegated_granular_administration'; +import UserDetail from './system_console/sections/user_management/user_detail'; +import EditionAndLicense from './system_console/sections/about/edition_and_license'; +import MobileSecurity from './system_console/sections/environment/mobile_security'; +import Notifications from './system_console/sections/site_configuration/notifications'; +import SystemConsoleFeatureDiscovery from './system_console/sections/system_users/feature_discovery'; +import SystemConsoleHeader from './system_console/header'; +import SystemConsoleNavbar from './system_console/navbar'; +import SystemConsoleSidebar from './system_console/sidebar'; +import SystemConsoleSidebarHeader from './system_console/sidebar_header'; +import TeamStatistics from './system_console/sections/reporting/team_statistics'; +import Users from './system_console/sections/user_management/users'; const components = { + // Shared / Global + Footer, GlobalHeader, - SearchBox, - ChannelsCenterView, - ChannelsSidebarLeft, - ChannelsSidebarRight, + MainHeader, + UserAccountMenu, + + // Channels ChannelsAppBar, + ChannelsCenterView, ChannelsHeader, + ChannelsPost, ChannelsPostCreate, ChannelsPostEdit, - ChannelsPost, ChannelSettingsModal, - DraftPost, - FindChannelsModal, - FlagPostConfirmationDialog, + ChannelsSidebarLeft, + ChannelsSidebarRight, + DeletePostConfirmationDialog, DeletePostModal, DeleteScheduledPostModal, + DraftPost, + EmojiGifPicker, + FindChannelsModal, + FlagPostConfirmationDialog, + GenericConfirmModal, InvitePeopleModal, - SettingsModal, + MessagePriority, PostDotMenu, PostMenu, - ThreadFooter, - Footer, - MainHeader, PostReminderMenu, - EmojiGifPicker, - GenericConfirmModal, - ScheduleMessageMenu, - ScheduleMessageModal, - ScheduledPostIndicator, + ProfileModal, + RestorePostConfirmationDialog, ScheduledDraftModal, ScheduledPost, + ScheduledPostIndicator, + ScheduleMessageMenu, + ScheduleMessageModal, + SearchBox, SendMessageNowModal, - SystemConsoleSidebar, - SystemConsoleNavbar, - SystemUsers, - SystemUsersFilterPopover, - SystemUsersFilterMenu, - SystemUsersColumnToggleMenu, - SystemConsoleFeatureDiscovery, - SystemConsoleMobileSecurity, - SystemConsoleNotifications, - MessagePriority, - UserProfilePopover, - UserAccountMenu, + SettingsModal, TeamMenu, TeamSettingsModal, - DeletePostConfirmationDialog, - RestorePostConfirmationDialog, - ProfileModal, + ThreadFooter, + UserProfilePopover, + + // System Console + AdminSectionPanel, + DelegatedGranularAdministration, + DropdownSetting, + EditionAndLicense, + MobileSecurity, + Notifications, + RadioSetting, + SystemConsoleFeatureDiscovery, + SystemConsoleHeader, + SystemConsoleNavbar, + SystemConsoleSidebar, + SystemConsoleSidebarHeader, + TeamStatistics, + TextInputSetting, + UserDetail, + Users, }; export { components, + + // Shared / Global + Footer, GlobalHeader, - SearchBox, - ChannelsCenterView, - ChannelsSidebarLeft, - ChannelsSidebarRight, + MainHeader, + UserAccountMenu, + + // Channels Page ChannelsAppBar, + ChannelsCenterView, ChannelsHeader, + ChannelsPost, ChannelsPostCreate, ChannelsPostEdit, - ChannelsPost, ChannelSettingsModal, - DraftPost, - FindChannelsModal, - FlagPostConfirmationDialog, + ChannelsSidebarLeft, + ChannelsSidebarRight, + DeletePostConfirmationDialog, DeletePostModal, DeleteScheduledPostModal, + DraftPost, + EmojiGifPicker, + FindChannelsModal, + FlagPostConfirmationDialog, + GenericConfirmModal, InvitePeopleModal, - SettingsModal, + MessagePriority, PostDotMenu, PostMenu, - ThreadFooter, - Footer, - MainHeader, PostReminderMenu, - EmojiGifPicker, - GenericConfirmModal, - ScheduleMessageMenu, - ScheduleMessageModal, - ScheduledPostIndicator, + ProfileModal, + RestorePostConfirmationDialog, ScheduledDraftModal, ScheduledPost, + ScheduledPostIndicator, + ScheduleMessageMenu, + ScheduleMessageModal, + SearchBox, SendMessageNowModal, - SystemConsoleSidebar, - SystemConsoleNavbar, - SystemUsers, - SystemUsersFilterPopover, - SystemUsersFilterMenu, - SystemUsersColumnToggleMenu, - SystemConsoleFeatureDiscovery, - SystemConsoleMobileSecurity, - SystemConsoleNotifications, - MessagePriority, - UserProfilePopover, - UserAccountMenu, + SettingsModal, TeamMenu, TeamSettingsModal, - DeletePostConfirmationDialog, - RestorePostConfirmationDialog, - ProfileModal, + ThreadFooter, + UserProfilePopover, + + // System Console + AdminSectionPanel, + DelegatedGranularAdministration, + DropdownSetting, + EditionAndLicense, + MobileSecurity, + Notifications, + RadioSetting, + SystemConsoleFeatureDiscovery, + SystemConsoleHeader, + SystemConsoleNavbar, + SystemConsoleSidebar, + SystemConsoleSidebarHeader, + TeamStatistics, + TextInputSetting, + UserDetail, + Users, }; diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/base_components.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/base_components.ts new file mode 100644 index 00000000000..473854faf4b --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/base_components.ts @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * Radio Setting - represents a true/false radio button group + * Uses getByRole for radio buttons, getByText for help text + * + * Usage: + * await setting.selectTrue(); + * await setting.toBeTrue(); + * await setting.toBeFalse(); + */ +export class RadioSetting { + readonly container: Locator; + readonly trueOption: Locator; + readonly falseOption: Locator; + readonly helpText: Locator; + + constructor(container: Locator) { + this.container = container; + this.trueOption = container.getByRole('radio', {name: 'True'}); + this.falseOption = container.getByRole('radio', {name: 'False'}); + this.helpText = container.locator('.help-text'); + } + + /** + * Select the True option + */ + async selectTrue() { + await this.trueOption.check(); + } + + /** + * Select the False option + */ + async selectFalse() { + await this.falseOption.check(); + } + + /** + * Assert that the setting is visible + */ + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Assert that the True option is selected + */ + async toBeTrue() { + await expect(this.trueOption).toBeChecked(); + } + + /** + * Assert that the False option is selected (True is not checked) + */ + async toBeFalse() { + await expect(this.falseOption).toBeChecked(); + } +} + +/** + * Text Input Setting - represents a text input field + */ +export class TextInputSetting { + readonly container: Locator; + readonly label: Locator; + readonly input: Locator; + readonly helpText: Locator; + + constructor(container: Locator, labelText: string) { + this.container = container; + this.label = container.getByText(labelText); + this.input = container.getByRole('textbox'); + this.helpText = container.locator('.help-text'); + } + + async fill(value: string) { + await this.input.fill(value); + } + + async getValue(): Promise { + return (await this.input.inputValue()) ?? ''; + } + + async clear() { + await this.input.clear(); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +/** + * Dropdown Setting - represents a select dropdown + */ +export class DropdownSetting { + readonly container: Locator; + readonly label: Locator; + readonly dropdown: Locator; + readonly helpText: Locator; + + constructor(container: Locator, labelText: string) { + this.container = container; + this.label = container.getByText(labelText); + this.dropdown = container.getByRole('combobox'); + this.helpText = container.locator('.help-text'); + } + + async select(option: string) { + await this.dropdown.selectOption(option); + } + + async getSelectedValue(): Promise { + return (await this.dropdown.inputValue()) ?? ''; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +/** + * Admin Section Panel - represents a collapsible section panel + */ +export class AdminSectionPanel { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + readonly body: Locator; + + constructor(container: Locator, titleText: string) { + this.container = container; + this.title = container.getByRole('heading', {name: titleText}); + this.description = container.locator('.AdminSectionPanel__description'); + this.body = container.locator('.AdminSectionPanel__body'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/base_modal.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/base_modal.ts new file mode 100644 index 00000000000..c862b32545e --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/base_modal.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * Base modal component for System Console modals. + * Can be extended or used directly for various modal dialogs. + */ +export default class BaseModal { + readonly container: Locator; + readonly title: Locator; + readonly closeButton: Locator; + readonly cancelButton: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.modal-title'); + this.closeButton = container.getByRole('button', {name: 'Close'}); + this.cancelButton = container.getByRole('button', {name: 'Cancel'}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async close() { + await this.closeButton.click(); + await expect(this.container).not.toBeVisible(); + } + + async cancel() { + await this.cancelButton.click(); + await expect(this.container).not.toBeVisible(); + } + + async clickButton(name: string) { + await this.container.getByRole('button', {name}).click(); + } +} + +/** + * Confirm modal with specific confirm button ID (#confirmModalButton) + */ +export class ConfirmModal extends BaseModal { + readonly confirmButton: Locator; + + constructor(container: Locator) { + super(container); + this.confirmButton = container.locator('#confirmModalButton'); + } + + async confirm() { + await this.confirmButton.click(); + await expect(this.container).not.toBeVisible(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/header.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/header.ts new file mode 100644 index 00000000000..b4435099793 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/header.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console section header component + * Represents the header area that displays the current section title + */ +export default class SystemConsoleHeader { + readonly container: Locator; + readonly title: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.admin-console__header'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async getTitle(): Promise { + return (await this.title.textContent()) ?? ''; + } + + async toHaveTitle(expectedTitle: string) { + await expect(this.title).toContainText(expectedTitle); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/navbar.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/navbar.ts index 8ac2a4c9993..f7377b99c4d 100644 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/navbar.ts +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/navbar.ts @@ -3,14 +3,27 @@ import {Locator, expect} from '@playwright/test'; +/** + * System Console Navbar component + */ export default class SystemConsoleNavbar { readonly container: Locator; + readonly backLink: Locator; constructor(container: Locator) { this.container = container; + this.backLink = container.getByRole('link', {name: /Back/}); } async toBeVisible() { await expect(this.container).toBeVisible(); + await expect(this.backLink).toBeVisible(); + } + + /** + * Click the back link to return to the team + */ + async clickBack() { + await this.backLink.click(); } } diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/about/edition_and_license.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/about/edition_and_license.ts new file mode 100644 index 00000000000..d85e24e9a1d --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/about/edition_and_license.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console -> About -> Edition and License + */ +export default class EditionAndLicense { + readonly container: Locator; + readonly header: Locator; + + constructor(container: Locator) { + this.container = container; + this.header = container.getByText('Edition and License', {exact: true}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/environment/mobile_security.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/environment/mobile_security.ts new file mode 100644 index 00000000000..f171dc11cc8 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/environment/mobile_security.ts @@ -0,0 +1,132 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +import {RadioSetting, TextInputSetting, DropdownSetting, AdminSectionPanel} from '../../base_components'; + +/** + * System Console -> Environment -> Mobile Security + */ +export default class MobileSecurity { + readonly container: Locator; + + // Header + readonly header: Locator; + + // Panels + readonly generalMobileSecurity: GeneralMobileSecurityPanel; + readonly microsoftIntune: MicrosoftIntunePanel; + + // Save section + readonly saveButton: Locator; + readonly errorMessage: Locator; + + constructor(container: Locator) { + this.container = container; + + this.header = container.getByText('Mobile Security', {exact: true}); + + this.generalMobileSecurity = new GeneralMobileSecurityPanel( + container.locator('.AdminSectionPanel').filter({hasText: 'General Mobile Security'}), + ); + this.microsoftIntune = new MicrosoftIntunePanel( + container.locator('.AdminSectionPanel').filter({hasText: 'Microsoft Intune'}), + ); + + this.saveButton = container.getByRole('button', {name: 'Save'}); + this.errorMessage = container.locator('.error-message'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } + + async save() { + await this.saveButton.click(); + } + + // Convenience shortcuts for General Mobile Security settings + get enableBiometricAuthentication() { + return this.generalMobileSecurity.enableBiometricAuthentication; + } + get preventScreenCapture() { + return this.generalMobileSecurity.preventScreenCapture; + } + get enableJailbreakProtection() { + return this.generalMobileSecurity.enableJailbreakProtection; + } + get enableSecureFilePreviewMode() { + return this.generalMobileSecurity.enableSecureFilePreviewMode; + } + get allowPdfLinkNavigation() { + return this.generalMobileSecurity.allowPdfLinkNavigation; + } + + // Convenience shortcuts for Microsoft Intune settings + get enableIntuneMAM() { + return this.microsoftIntune.enableIntuneMAM; + } + get authProvider() { + return this.microsoftIntune.authProvider; + } + get tenantId() { + return this.microsoftIntune.tenantId; + } + get clientId() { + return this.microsoftIntune.clientId; + } +} + +class GeneralMobileSecurityPanel extends AdminSectionPanel { + readonly enableBiometricAuthentication: RadioSetting; + readonly preventScreenCapture: RadioSetting; + readonly enableJailbreakProtection: RadioSetting; + readonly enableSecureFilePreviewMode: RadioSetting; + readonly allowPdfLinkNavigation: RadioSetting; + + constructor(container: Locator) { + super(container, 'General Mobile Security'); + + this.enableBiometricAuthentication = new RadioSetting( + this.body.getByRole('group', {name: /Enable Biometric Authentication/}), + ); + this.preventScreenCapture = new RadioSetting(this.body.getByRole('group', {name: /Prevent Screen Capture/})); + this.enableJailbreakProtection = new RadioSetting( + this.body.getByRole('group', {name: /Enable Jailbreak\/Root Protection/}), + ); + this.enableSecureFilePreviewMode = new RadioSetting( + this.body.getByRole('group', {name: /Enable Secure File Preview Mode/}), + ); + this.allowPdfLinkNavigation = new RadioSetting( + this.body.getByRole('group', {name: /Allow Link Navigation in Secure PDFs/}), + ); + } +} + +class MicrosoftIntunePanel extends AdminSectionPanel { + readonly enableIntuneMAM: RadioSetting; + readonly authProvider: DropdownSetting; + readonly tenantId: TextInputSetting; + readonly clientId: TextInputSetting; + + constructor(container: Locator) { + super(container, 'Microsoft Intune'); + + this.enableIntuneMAM = new RadioSetting(this.body.getByRole('group', {name: /Enable Microsoft Intune MAM/})); + + this.authProvider = new DropdownSetting( + this.body.locator('.form-group').filter({hasText: 'Auth Provider:'}), + 'Auth Provider:', + ); + this.tenantId = new TextInputSetting( + this.body.locator('.form-group').filter({hasText: 'Tenant ID:'}), + 'Tenant ID:', + ); + this.clientId = new TextInputSetting( + this.body.locator('.form-group').filter({hasText: 'Application (Client) ID:'}), + 'Application (Client) ID:', + ); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/reporting/team_statistics.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/reporting/team_statistics.ts new file mode 100644 index 00000000000..b61f4952cc5 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/reporting/team_statistics.ts @@ -0,0 +1,155 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console -> Reporting -> Team Statistics + */ +export default class TeamStatistics { + readonly container: Locator; + readonly header: Locator; + + // Team filter + readonly teamFilterDropdown: Locator; + + // Banner + readonly banner: Locator; + + // Statistics cards + readonly totalActivatedUsers: StatCard; + readonly publicChannels: StatCard; + readonly privateChannels: StatCard; + readonly totalPosts: StatCard; + + // Charts + readonly totalPostsChart: ChartSection; + readonly activeUsersWithPostsChart: ChartSection; + + // Tables + readonly recentActiveUsers: TableSection; + readonly newlyCreatedUsers: TableSection; + + constructor(container: Locator) { + this.container = container; + this.header = container.locator('.team-statistics__header'); + + this.teamFilterDropdown = container.getByTestId('teamFilter'); + + this.banner = container.locator('.banner'); + + const gridStatistics = container.locator('.grid-statistics'); + this.totalActivatedUsers = new StatCard( + gridStatistics.locator('.grid-statistics__card').filter({hasText: 'Total Activated Users'}), + ); + this.publicChannels = new StatCard( + gridStatistics.locator('.grid-statistics__card').filter({hasText: 'Public Channels'}), + ); + this.privateChannels = new StatCard( + gridStatistics.locator('.grid-statistics__card').filter({hasText: 'Private Channels'}), + ); + this.totalPosts = new StatCard( + gridStatistics.locator('.grid-statistics__card').filter({hasText: 'Total Posts'}), + ); + + this.totalPostsChart = new ChartSection( + container.locator('.total-count.by-day').filter({hasText: 'Total Posts'}), + ); + this.activeUsersWithPostsChart = new ChartSection( + container.locator('.total-count.by-day').filter({hasText: 'Active Users With Posts'}), + ); + + this.recentActiveUsers = new TableSection( + container.locator('.recent-active-users').filter({hasText: 'Recent Active Users'}), + ); + this.newlyCreatedUsers = new TableSection( + container.locator('.recent-active-users').filter({hasText: 'Newly Created Users'}), + ); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } + + async selectTeam(teamName: string) { + // Wait for the dropdown to be enabled (it may be disabled while loading) + await expect(this.teamFilterDropdown).toBeEnabled(); + await this.teamFilterDropdown.selectOption({label: teamName}); + } + + async selectTeamById(teamId: string) { + // Wait for the dropdown to be enabled (it may be disabled while loading) + await expect(this.teamFilterDropdown).toBeEnabled(); + await this.teamFilterDropdown.selectOption({value: teamId}); + } + + async getSelectedTeam(): Promise { + return (await this.teamFilterDropdown.inputValue()) ?? ''; + } + + /** + * Verify the team statistics header shows the expected team name + */ + async toHaveTeamHeader(teamDisplayName: string) { + const heading = this.container.getByText(`Team Statistics for ${teamDisplayName}`, {exact: true}); + await expect(heading).toBeVisible(); + } +} + +class StatCard { + readonly container: Locator; + readonly title: Locator; + readonly value: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.title'); + this.value = container.locator('.content'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async getValue(): Promise { + return (await this.value.textContent()) ?? ''; + } +} + +class ChartSection { + readonly container: Locator; + readonly title: Locator; + readonly content: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.title'); + this.content = container.locator('.content'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async hasNoData(): Promise { + const text = await this.content.textContent(); + return text?.includes('Not enough data') ?? false; + } +} + +class TableSection { + readonly container: Locator; + readonly title: Locator; + readonly table: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.title'); + this.table = container.locator('table'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/site_configuration/notifications.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/site_configuration/notifications.ts index d0cab8cea29..1a4d32ee8ce 100644 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/site_configuration/notifications.ts +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/site_configuration/notifications.ts @@ -3,75 +3,98 @@ import {Locator, expect} from '@playwright/test'; +import {RadioSetting, TextInputSetting, DropdownSetting} from '../../base_components'; + /** * System Console -> Site Configuration -> Notifications */ -export default class SystemConsoleNotifications { +export default class Notifications { readonly container: Locator; - // header + // Header readonly header: Locator; - // Notification Display Name - readonly notificationDisplayName: Locator; - readonly notificationDisplayNameInput: Locator; - readonly notificationDisplayNameHelpText: Locator; + // Radio Settings + readonly showMentionConfirmDialog: RadioSetting; + readonly enableEmailNotifications: RadioSetting; + readonly enablePreviewModeBanner: RadioSetting; + readonly enableEmailBatching: RadioSetting; + readonly enableNotificationMonitoring: RadioSetting; - // Notification From Address - readonly notificationFromAddress: Locator; - readonly notificationFromAddressInput: Locator; - readonly notificationFromAddressHelpText: Locator; + // Dropdown Settings + readonly emailNotificationContents: DropdownSetting; + readonly pushNotificationContents: DropdownSetting; - // Support Email Address - readonly supportEmailAddress: Locator; - readonly supportEmailAddressInput: Locator; - readonly supportEmailHelpText: Locator; + // Text Input Settings + readonly notificationDisplayName: TextInputSetting; + readonly notificationFromAddress: TextInputSetting; + readonly supportEmailAddress: TextInputSetting; + readonly notificationReplyToAddress: TextInputSetting; + readonly notificationFooterMailingAddress: TextInputSetting; - // Push Notification Contents - readonly pushNotificationContents: Locator; - readonly pushNotificationContentsDropdown: Locator; - readonly pushNotificationContentsHelpText: Locator; - - // Save button + // Save section readonly saveButton: Locator; readonly errorMessage: Locator; constructor(container: Locator) { this.container = container; - // header - this.header = this.container.locator('.admin-console__header').getByText('Notifications'); + this.header = container.getByText('Notifications', {exact: true}); - // Notification Display Name - this.notificationDisplayName = this.container.getByTestId('EmailSettings.FeedbackNameinput'); - this.notificationDisplayNameInput = this.container.getByTestId('EmailSettings.FeedbackNameinput'); - this.notificationDisplayNameHelpText = this.container.getByTestId('EmailSettings.FeedbackNamehelp-text'); - - // Notification From Address - this.notificationFromAddress = this.container.getByLabel('Notification From Address:'); - this.notificationFromAddressInput = this.container.getByTestId('EmailSettings.FeedbackEmailinput'); - this.notificationFromAddressHelpText = this.container.getByTestId('EmailSettings.FeedbackEmailhelp-text'); - - // Support Email Address - this.supportEmailAddress = this.container.getByLabel('Support Email Address:'); - this.supportEmailAddressInput = this.container.getByTestId('SupportSettings.SupportEmailinput'); - this.supportEmailHelpText = this.container.getByTestId('SupportSettings.SupportEmailhelp-text'); - - // Push Notification Contents - this.pushNotificationContents = this.container.getByTestId('EmailSettings.PushNotificationContents'); - this.pushNotificationContentsDropdown = this.container.getByTestId( - 'EmailSettings.PushNotificationContentsdropdown', + this.showMentionConfirmDialog = new RadioSetting( + container.getByRole('group', {name: /Show @channel, @all, @here and group mention confirmation dialog/}), ); - this.pushNotificationContentsHelpText = this.container.getByTestId( - 'EmailSettings.PushNotificationContentshelp-text', + this.enableEmailNotifications = new RadioSetting( + container.getByRole('group', {name: /Enable Email Notifications/}), + ); + this.enablePreviewModeBanner = new RadioSetting( + container.getByRole('group', {name: /Enable Preview Mode Banner/}), + ); + this.enableEmailBatching = new RadioSetting(container.getByRole('group', {name: /Enable Email Batching/})); + this.enableNotificationMonitoring = new RadioSetting( + container.getByRole('group', {name: /Enable Notification Monitoring/}), ); - // Save button and error message - this.saveButton = this.container.getByTestId('saveSetting'); - this.errorMessage = this.container.locator('.has-error'); + this.emailNotificationContents = new DropdownSetting( + container.locator('.form-group').filter({hasText: 'Email Notification Contents:'}), + 'Email Notification Contents:', + ); + this.pushNotificationContents = new DropdownSetting( + container.locator('.form-group').filter({hasText: 'Push Notification Contents:'}), + 'Push Notification Contents:', + ); + + this.notificationDisplayName = new TextInputSetting( + container.locator('.form-group').filter({hasText: 'Notification Display Name:'}), + 'Notification Display Name:', + ); + this.notificationFromAddress = new TextInputSetting( + container.locator('.form-group').filter({hasText: 'Notification From Address:'}), + 'Notification From Address:', + ); + this.supportEmailAddress = new TextInputSetting( + container.locator('.form-group').filter({hasText: 'Support Email Address:'}), + 'Support Email Address:', + ); + this.notificationReplyToAddress = new TextInputSetting( + container.locator('.form-group').filter({hasText: 'Notification Reply-To Address:'}), + 'Notification Reply-To Address:', + ); + this.notificationFooterMailingAddress = new TextInputSetting( + container.locator('.form-group').filter({hasText: 'Notification Footer Mailing Address:'}), + 'Notification Footer Mailing Address:', + ); + + this.saveButton = container.getByRole('button', {name: 'Save'}); + this.errorMessage = container.locator('.has-error'); } async toBeVisible() { await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } + + async save() { + await this.saveButton.click(); } } diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/column_toggle_menu.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/column_toggle_menu.ts deleted file mode 100644 index 11145a62d17..00000000000 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/column_toggle_menu.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Locator, expect} from '@playwright/test'; - -export default class SystemUsersColumnToggleMenu { - readonly container: Locator; - - constructor(container: Locator) { - this.container = container; - } - - async toBeVisible() { - await expect(this.container).toBeVisible(); - } - - /** - * Return the locator for the menu item with the given name. - */ - async getMenuItem(menuItem: string) { - const menuItemLocator = this.container.getByRole('menuitemcheckbox').filter({hasText: menuItem}); - await menuItemLocator.waitFor(); - - return menuItemLocator; - } - - /** - * Returns the list of locators for all the menu items. - */ - async getAllMenuItems() { - const menuItemLocators = this.container.getByRole('menuitemcheckbox'); - return menuItemLocators; - } - - /** - * Pass in the item name to check/uncheck the menu item. - */ - async clickMenuItem(menuItem: string) { - const menuItemLocator = await this.getMenuItem(menuItem); - await menuItemLocator.click(); - } - - /** - * Close column toggle menu. - */ - async close() { - await this.container.press('Escape'); - await expect(this.container).not.toBeVisible(); - } -} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_menu.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_menu.ts deleted file mode 100644 index a39fff19c59..00000000000 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_menu.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Locator, expect} from '@playwright/test'; - -/** - * The dropdown menu which appears for both Role and Status filter. - */ -export default class SystemUsersFilterMenu { - readonly container: Locator; - - constructor(container: Locator) { - this.container = container; - } - - async toBeVisible() { - await expect(this.container).toBeVisible(); - } - - /** - * Return the locator for the menu item with the given name. - */ - async getMenuItem(menuItem: string) { - const menuItemLocator = this.container.getByText(menuItem); - await menuItemLocator.waitFor(); - - return menuItemLocator; - } - - /** - * Clicks on the menu item with the given name. - */ - async clickMenuItem(menuItem: string) { - const menuItemLocator = await this.getMenuItem(menuItem); - await menuItemLocator.click(); - } - - /** - * Close the menu. - */ - async close() { - await this.container.press('Escape'); - } -} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_popover.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_popover.ts deleted file mode 100644 index e88256c5ff2..00000000000 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/filter_popover.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Locator, expect} from '@playwright/test'; - -export default class SystemUsersFilterPopover { - readonly container: Locator; - - readonly teamMenuInput: Locator; - readonly roleMenuButton: Locator; - readonly statusMenuButton: Locator; - - readonly applyButton: Locator; - - constructor(container: Locator) { - this.container = container; - - this.teamMenuInput = this.container.locator('#asyncTeamSelectInput'); - this.roleMenuButton = this.container.locator('#DropdownInput_filterRole'); - this.statusMenuButton = this.container.locator('#DropdownInput_filterStatus'); - - this.applyButton = this.container.getByText('Apply'); - } - - async toBeVisible() { - await expect(this.container).toBeVisible(); - await expect(this.applyButton).toBeVisible(); - } - - /** - * Save the filter settings. - */ - async save() { - await this.applyButton.click(); - } - - /** - * Allows to type in the team filter for searching. - */ - async searchInTeamMenu(teamDisplayName: string) { - expect(this.teamMenuInput).toBeVisible(); - await this.teamMenuInput.fill(teamDisplayName); - } - - /** - * Opens the role filter menu. - */ - async openRoleMenu() { - expect(this.roleMenuButton).toBeVisible(); - await this.roleMenuButton.click(); - } - - /** - * Opens the status filter menu. - */ - async openStatusMenu() { - expect(this.statusMenuButton).toBeVisible(); - await this.statusMenuButton.click(); - } - - /** - * Closes the filter popover. - */ - async close() { - await this.container.press('Escape'); - await expect(this.container).not.toBeVisible(); - } -} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts deleted file mode 100644 index a3acd91116b..00000000000 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {expect, Locator, Page} from '@playwright/test'; - -/** - * System Console -> Environment -> Mobile Security - */ -export default class MobileSecurity { - readonly page: Page; - readonly container: Locator; - - readonly enableBiometricAuthenticationToggleTrue: Locator; - readonly enableBiometricAuthenticationToggleFalse: Locator; - readonly preventScreenCaptureToggleTrue: Locator; - readonly preventScreenCaptureToggleFalse: Locator; - readonly jailbreakProtectionToggleTrue: Locator; - readonly jailbreakProtectionToggleFalse: Locator; - readonly enableSecureFilePreviewToggleTrue: Locator; - readonly enableSecureFilePreviewToggleFalse: Locator; - readonly allowPdfLinkNavigationToggleTrue: Locator; - readonly allowPdfLinkNavigationToggleFalse: Locator; - readonly enableIntuneMAMToggleTrue: Locator; - readonly enableIntuneMAMToggleFalse: Locator; - - // New IntuneSettings fields - readonly enableIntuneToggleTrue: Locator; - readonly enableIntuneToggleFalse: Locator; - readonly intuneAuthServiceDropdown: Locator; - readonly intuneTenantIdInput: Locator; - readonly intuneClientIdInput: Locator; - readonly intuneTenantIdRequiredError: Locator; - - readonly saveButton: Locator; - - constructor(container: Locator, page: Page) { - this.container = container; - this.page = page; - - this.enableBiometricAuthenticationToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobileEnableBiometricstrue', - ); - this.enableBiometricAuthenticationToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobileEnableBiometricsfalse', - ); - - this.preventScreenCaptureToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobilePreventScreenCapturetrue', - ); - this.preventScreenCaptureToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobilePreventScreenCapturefalse', - ); - - this.jailbreakProtectionToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobileJailbreakProtectiontrue', - ); - this.jailbreakProtectionToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobileJailbreakProtectionfalse', - ); - - this.jailbreakProtectionToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobileJailbreakProtectiontrue', - ); - this.jailbreakProtectionToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobileJailbreakProtectionfalse', - ); - - this.enableSecureFilePreviewToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobileEnableSecureFilePreviewtrue', - ); - this.enableSecureFilePreviewToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobileEnableSecureFilePreviewfalse', - ); - - this.allowPdfLinkNavigationToggleTrue = this.container.getByTestId( - 'NativeAppSettings.MobileAllowPdfLinkNavigationtrue', - ); - this.allowPdfLinkNavigationToggleFalse = this.container.getByTestId( - 'NativeAppSettings.MobileAllowPdfLinkNavigationfalse', - ); - - // Legacy Intune toggle (will be removed in Phase 6) - this.enableIntuneMAMToggleTrue = this.container.getByTestId('IntuneSettings.Enabletrue'); - this.enableIntuneMAMToggleFalse = this.container.getByTestId('IntuneSettings.Enablefalse'); - - // New IntuneSettings fields - this.enableIntuneToggleTrue = this.container.getByTestId('IntuneSettings.Enabletrue'); - this.enableIntuneToggleFalse = this.container.getByTestId('IntuneSettings.Enablefalse'); - this.intuneAuthServiceDropdown = this.container.getByTestId('IntuneSettings.AuthServicedropdown'); - this.intuneTenantIdInput = this.container.getByTestId('IntuneSettings.TenantIdinput'); - this.intuneClientIdInput = this.container.getByTestId('IntuneSettings.ClientIdinput'); - this.intuneTenantIdRequiredError = this.container.getByTestId('errorMessage'); - - this.saveButton = this.container.getByRole('button', {name: 'Save'}); - } - - async discardChanges() { - this.page.getByRole('button', {name: 'Yes, Discard'}).click(); - } - - async intuneTenantIdRequiredErrorToBeVisible() { - await expect(this.intuneTenantIdRequiredError).toBeVisible(); - } - - async toBeVisible() { - await expect(this.container).toBeVisible(); - } - - async clickEnableBiometricAuthenticationToggleTrue() { - await this.enableBiometricAuthenticationToggleTrue.click(); - } - - async clickEnableBiometricAuthenticationToggleFalse() { - await this.enableBiometricAuthenticationToggleFalse.click(); - } - - async clickPreventScreenCaptureToggleTrue() { - await this.preventScreenCaptureToggleTrue.click(); - } - - async clickPreventScreenCaptureToggleFalse() { - await this.preventScreenCaptureToggleFalse.click(); - } - - async clickJailbreakProtectionToggleTrue() { - await this.jailbreakProtectionToggleTrue.click(); - } - - async clickJailbreakProtectionToggleFalse() { - await this.jailbreakProtectionToggleFalse.click(); - } - - async clickEnableSecureFilePreviewToggleTrue() { - await this.enableSecureFilePreviewToggleTrue.click(); - } - - async clickEnableSecureFilePreviewToggleFalse() { - await this.enableSecureFilePreviewToggleFalse.click(); - } - - async clickAllowPdfLinkNavigationToggleTrue() { - await this.allowPdfLinkNavigationToggleTrue.click(); - } - - async clickAllowPdfLinkNavigationToggleFalse() { - await this.allowPdfLinkNavigationToggleFalse.click(); - } - - async clickEnableIntuneMAMToggleTrue() { - await this.enableIntuneMAMToggleTrue.click(); - } - - async selectAuthProvider(value: 'office365' | 'saml') { - await this.intuneAuthServiceDropdown.selectOption(value); - } - - async clickEnableIntuneMAMToggleFalse() { - await this.enableIntuneMAMToggleFalse.click(); - } - - // New IntuneSettings methods - async clickEnableIntuneToggleTrue() { - await this.enableIntuneToggleTrue.click(); - } - - async clickEnableIntuneToggleFalse() { - await this.enableIntuneToggleFalse.click(); - } - - async selectIntuneAuthService(value: 'office365' | 'saml') { - await this.intuneAuthServiceDropdown.selectOption(value); - } - - async fillIntuneTenantId(value: string) { - await this.intuneTenantIdInput.fill(value); - } - - async fillIntuneClientId(value: string) { - await this.intuneClientIdInput.fill(value); - } - - async clickSaveButton() { - await this.saveButton.click(); - } -} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/system_users.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/system_users.ts deleted file mode 100644 index a916d402d77..00000000000 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/system_users.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Locator, expect} from '@playwright/test'; - -/** - * System Console -> User Management -> Users - */ -export default class SystemUsers { - readonly container: Locator; - - readonly searchInput: Locator; - readonly saveRoleChange: Locator; - readonly columnToggleMenuButton: Locator; - readonly dateRangeSelectorMenuButton: Locator; - readonly exportButton: Locator; - readonly filterPopoverButton: Locator; - readonly actionMenuButtons: Locator[]; - - readonly loadingSpinner: Locator; - - constructor(container: Locator) { - this.container = container; - - this.searchInput = this.container.getByLabel('Search users'); - this.saveRoleChange = this.container.locator('button.btn-primary:has-text("Save")'); - this.columnToggleMenuButton = this.container.locator('#systemUsersColumnTogglerMenuButton'); - this.dateRangeSelectorMenuButton = this.container.locator('#systemUsersDateRangeSelectorMenuButton'); - this.exportButton = this.container.getByText('Export'); - this.filterPopoverButton = this.container.getByText(/Filters \(\d+\)/); - this.actionMenuButtons = Array.from(Array(10).keys()).map((index) => - this.container.locator(`#actionMenuButton-systemUsersTable-${index}`), - ); - - this.loadingSpinner = this.container.getByText('Loading'); - } - - async toBeVisible() { - await expect(this.container).toBeVisible(); - } - - async isLoadingComplete() { - await expect(this.loadingSpinner).toHaveCount(0); - } - - /** - * Returns the locator for the header of the given column. - */ - async getColumnHeader(columnName: string) { - const columnHeader = this.container.getByRole('columnheader').filter({hasText: columnName}); - return columnHeader; - } - - /** - * Checks if given column exists in the table. By searching for the column header. - */ - async doesColumnExist(columnName: string) { - const columnHeader = await this.getColumnHeader(columnName); - return await columnHeader.isVisible(); - } - - /** - * Clicks on the column header of the given column for sorting. - */ - async clickSortOnColumn(columnName: string) { - const columnHeader = await this.getColumnHeader(columnName); - await columnHeader.waitFor(); - await columnHeader.click(); - } - - /** - * Return the locator for the given row number. If '0' is passed, it will return the header row. - */ - async getNthRow(rowNumber: number) { - const row = this.container.getByRole('row').nth(rowNumber); - await row.waitFor(); - - return row; - } - - /** - * Opens the Filter popover - */ - async openFilterPopover() { - expect(this.filterPopoverButton).toBeVisible(); - await this.filterPopoverButton.click(); - } - - /** - * Open the column toggle menu - */ - async openColumnToggleMenu() { - expect(this.columnToggleMenuButton).toBeVisible(); - await this.columnToggleMenuButton.click(); - } - - /** - * Open the date range selector menu - */ - async openDateRangeSelectorMenu() { - expect(this.dateRangeSelectorMenuButton).toBeVisible(); - await this.dateRangeSelectorMenuButton.click(); - } - - /** - * Enter the given search term in the search input - */ - async enterSearchText(searchText: string) { - expect(this.searchInput).toBeVisible(); - await this.searchInput.fill(`${searchText}`); - - await this.isLoadingComplete(); - } - - /** - * Searches and verifies that the row with given text is found - */ - async verifyRowWithTextIsFound(text: string) { - const foundUser = this.container.getByText(text); - await foundUser.waitFor(); - - await expect(foundUser).toBeVisible(); - } - - /** - * Searches and verifies that the row with given text is not found - */ - async verifyRowWithTextIsNotFound(text: string) { - const foundUser = this.container.getByText(text); - await expect(foundUser).not.toBeVisible(); - } -} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/delegated_granular_administration.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/delegated_granular_administration.ts new file mode 100644 index 00000000000..153f2d3ab64 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/delegated_granular_administration.ts @@ -0,0 +1,154 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +import SystemRoles from './system_roles'; + +/** + * System Console -> User Management -> Delegated Granular Administration + */ +export default class DelegatedGranularAdministration { + readonly container: Locator; + readonly header: Locator; + + // Admin Roles Panel + readonly adminRolesPanel: AdminRolesPanel; + + // System Roles page (accessed by clicking Edit on a role row) + readonly systemRoles: SystemRoles; + + constructor(container: Locator) { + this.container = container; + this.header = container.getByText('Delegated Granular Administration', {exact: true}); + + this.adminRolesPanel = new AdminRolesPanel(container.locator('#SystemRoles')); + + // System Roles page (click Edit on a role to navigate here) + this.systemRoles = new SystemRoles(container); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } +} + +class AdminRolesPanel { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + private readonly dataGrid: DataGrid; + + constructor(container: Locator) { + this.container = container; + this.title = container.getByRole('heading', {name: 'Admin Roles'}); + this.description = container.getByText('Manage different levels of access to the system console.'); + this.dataGrid = new DataGrid(container.locator('.DataGrid')); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.title).toBeVisible(); + } + + // Shortcuts to role rows + get systemAdmin() { + return this.dataGrid.systemAdmin; + } + get systemManager() { + return this.dataGrid.systemManager; + } + get userManager() { + return this.dataGrid.userManager; + } + get customGroupManager() { + return this.dataGrid.customGroupManager; + } + get viewer() { + return this.dataGrid.viewer; + } +} + +class DataGrid { + readonly container: Locator; + readonly header: Locator; + readonly rows: Locator; + + // Role rows + readonly systemAdmin: RoleRow; + readonly systemManager: RoleRow; + readonly userManager: RoleRow; + readonly customGroupManager: RoleRow; + readonly viewer: RoleRow; + + constructor(container: Locator) { + this.container = container; + this.header = container.locator('.DataGrid_header'); + this.rows = container.locator('.DataGrid_rows'); + + // Individual role rows + this.systemAdmin = new RoleRow( + this.rows.locator('.DataGrid_row').filter({hasText: 'System Admin'}), + 'system_admin_edit', + ); + this.systemManager = new RoleRow( + this.rows.locator('.DataGrid_row').filter({hasText: 'System Manager'}), + 'system_manager_edit', + ); + this.userManager = new RoleRow( + this.rows.locator('.DataGrid_row').filter({hasText: 'User Manager'}), + 'system_user_manager_edit', + ); + this.customGroupManager = new RoleRow( + this.rows.locator('.DataGrid_row').filter({hasText: 'Custom Group Manager'}), + 'system_custom_group_admin_edit', + ); + this.viewer = new RoleRow( + this.rows.locator('.DataGrid_row').filter({hasText: 'Viewer'}), + 'system_read_only_admin_edit', + ); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +class RoleRow { + readonly container: Locator; + readonly roleName: Locator; + readonly description: Locator; + readonly type: Locator; + readonly editLink: Locator; + + constructor(container: Locator, editTestId: string) { + this.container = container; + + const cells = container.locator('.DataGrid_cell'); + this.roleName = cells.nth(0); + this.description = cells.nth(1); + this.type = cells.nth(2); + this.editLink = container.getByTestId(editTestId).getByRole('link', {name: 'Edit'}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async clickEdit() { + await this.editLink.click(); + } + + async getRoleName(): Promise { + return (await this.roleName.textContent()) ?? ''; + } + + async getDescription(): Promise { + return (await this.description.textContent()) ?? ''; + } + + async getType(): Promise { + return (await this.type.textContent()) ?? ''; + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/system_roles.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/system_roles.ts new file mode 100644 index 00000000000..c3e8e27164a --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/system_roles.ts @@ -0,0 +1,392 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console -> User Management -> Delegated Granular Administration -> [Role] Edit + * This page is shown when editing a specific role (e.g., System Manager, User Manager, etc.) + */ +export default class SystemRoles { + readonly container: Locator; + + // Header + readonly backLink: Locator; + readonly roleName: Locator; + + // Privileges Panel + readonly privilegesPanel: PrivilegesPanel; + + // Assigned People Panel + readonly assignedPeoplePanel: AssignedPeoplePanel; + + // Save section + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly errorMessage: Locator; + + constructor(container: Locator) { + this.container = container; + + this.backLink = container.locator('.admin-console__header .back'); + this.roleName = container.locator('.admin-console__header span').last(); + + this.privilegesPanel = new PrivilegesPanel(container.locator('#SystemRolePermissions')); + this.assignedPeoplePanel = new AssignedPeoplePanel(container.locator('#SystemRoleUsers')); + + this.saveButton = container.getByTestId('saveSetting'); + this.cancelButton = container.getByRole('link', {name: 'Cancel'}); + this.errorMessage = container.locator('.error-message'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.roleName).toBeVisible(); + } + + async goBack() { + await this.backLink.click(); + } + + async save() { + await this.saveButton.click(); + } + + async cancel() { + await this.cancelButton.click(); + } + + async getRoleName(): Promise { + return (await this.roleName.textContent()) ?? ''; + } +} + +class PrivilegesPanel { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + + // Permission sections + readonly about: PermissionSection; + readonly reporting: PermissionSection; + readonly userManagement: PermissionSection; + readonly environment: PermissionSection; + readonly siteConfiguration: PermissionSection; + readonly authentication: PermissionSection; + readonly plugins: PermissionSection; + readonly integrations: PermissionSection; + readonly compliance: PermissionSection; + readonly experimental: PermissionSection; + + constructor(container: Locator) { + this.container = container; + this.title = container.getByRole('heading', {name: 'Privileges'}); + this.description = container.getByText('Level of access to the system console.'); + + // Permission sections + this.about = new PermissionSection(container, 'permission_section_about'); + this.reporting = new PermissionSection(container, 'permission_section_reporting'); + this.userManagement = new PermissionSection(container, 'permission_section_user_management'); + this.environment = new PermissionSection(container, 'permission_section_environment'); + this.siteConfiguration = new PermissionSection(container, 'permission_section_site'); + this.authentication = new PermissionSection(container, 'permission_section_authentication'); + this.plugins = new PermissionSection(container, 'permission_section_plugins'); + this.integrations = new PermissionSection(container, 'permission_section_integrations'); + this.compliance = new PermissionSection(container, 'permission_section_compliance'); + this.experimental = new PermissionSection(container, 'permission_section_experimental'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.title).toBeVisible(); + } +} + +class PermissionSection { + readonly container: Locator; + readonly row: Locator; + readonly title: Locator; + readonly description: Locator; + readonly subsectionsToggle: Locator; + readonly dropdownButton: Locator; + readonly subsectionsContainer: Locator; + + private readonly panelContainer: Locator; + private readonly testId: string; + private readonly sectionName: string; + + constructor(panelContainer: Locator, testId: string) { + this.panelContainer = panelContainer; + this.testId = testId; + // Extract section name from testId (e.g., 'permission_section_user_management' -> 'user_management') + this.sectionName = testId.replace('permission_section_', ''); + + this.container = panelContainer.getByTestId(testId); + // Use CSS :has() selector to find the row containing this section + this.row = panelContainer.locator(`.PermissionRow:has([data-testid="${testId}"])`); + this.title = this.container.locator('.PermissionSectionText_title'); + this.description = this.container.locator('.PermissionSection_description'); + this.subsectionsToggle = this.container.locator('.PermissionSubsectionsToggle button'); + // Use the dropdown button ID which is more reliable + this.dropdownButton = panelContainer.page().locator(`#systemRolePermissionDropdown${this.sectionName}`); + this.subsectionsContainer = this.row.locator('.PermissionSubsections'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get the current permission value (e.g., "Can edit", "Read only", "No access", "Mixed access") + */ + async getPermissionValue(): Promise { + return (await this.dropdownButton.locator('.PermissionSectionDropdownButton_text').textContent()) ?? ''; + } + + /** + * Set permission for this section + * @param permission - "Can edit", "Read only", or "No access" + */ + async setPermission(permission: 'Can edit' | 'Read only' | 'No access') { + await expect(this.dropdownButton).toBeVisible(); + await this.dropdownButton.click(); + + // Wait for the MenuWrapper to have --open class which indicates menu is open + const menuWrapper = this.dropdownButton.locator('xpath=ancestor::div[contains(@class, "MenuWrapper")]'); + await expect(menuWrapper).toHaveClass(/MenuWrapper--open/); + + // Find the menu items and click the one matching the permission + const menuItem = menuWrapper.locator('.Menu__content li').filter({hasText: permission}); + await expect(menuItem).toBeVisible(); + await menuItem.click(); + + // Wait for menu to close + await expect(menuWrapper).not.toHaveClass(/MenuWrapper--open/); + } + + /** + * Expand subsections if they are collapsed + */ + async expandSubsections() { + const hasToggle = await this.subsectionsToggle.isVisible(); + if (!hasToggle) { + return; + } + + const buttonText = await this.subsectionsToggle.textContent(); + if (buttonText?.includes('Show')) { + await this.subsectionsToggle.click(); + // Wait for subsections to be visible + await expect(this.subsectionsContainer).toBeVisible(); + } + } + + /** + * Collapse subsections if they are expanded + */ + async collapseSubsections() { + const hasToggle = await this.subsectionsToggle.isVisible(); + if (!hasToggle) { + return; + } + + const buttonText = await this.subsectionsToggle.textContent(); + if (buttonText?.includes('Hide')) { + await this.subsectionsToggle.click(); + } + } + + /** + * Check if subsections are visible + */ + async hasSubsections(): Promise { + return this.subsectionsToggle.isVisible(); + } + + /** + * Get a subsection by its testId suffix + * @param subsectionName - The suffix of the subsection testId (e.g., "team_statistics" for "permission_section_reporting_team_statistics") + */ + getSubsection(subsectionName: string): PermissionSubsection { + const subsectionTestId = `${this.testId}_${subsectionName}`; + return new PermissionSubsection(this.panelContainer, subsectionTestId); + } + + // Reporting subsections shortcuts + get siteStatistics() { + return this.getSubsection('site_statistics'); + } + get teamStatistics() { + return this.getSubsection('team_statistics'); + } + get serverLogs() { + return this.getSubsection('server_logs'); + } + + // User Management subsections shortcuts + get users() { + return this.getSubsection('users'); + } + get groups() { + return this.getSubsection('groups'); + } + get teams() { + return this.getSubsection('teams'); + } + get channels() { + return this.getSubsection('channels'); + } + get permissions() { + return this.getSubsection('permissions'); + } + get systemRoles() { + return this.getSubsection('system_roles'); + } +} + +class PermissionSubsection { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + readonly dropdownButton: Locator; + + private readonly sectionName: string; + + constructor(panelContainer: Locator, testId: string) { + this.container = panelContainer.getByTestId(testId); + this.title = this.container.locator('.PermissionSectionText_title'); + this.description = this.container.locator('.PermissionSection_description'); + // Extract section name from testId (e.g., 'permission_section_user_management_teams' -> 'user_management_teams') + this.sectionName = testId.replace('permission_section_', ''); + // Use the dropdown button ID which is more reliable + this.dropdownButton = panelContainer.page().locator(`#systemRolePermissionDropdown${this.sectionName}`); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get the current permission value (e.g., "Can edit", "Read only", "No access") + */ + async getPermissionValue(): Promise { + return (await this.dropdownButton.locator('.PermissionSectionDropdownButton_text').textContent()) ?? ''; + } + + /** + * Set permission for this subsection + * @param permission - "Can edit", "Read only", or "No access" + */ + async setPermission(permission: 'Can edit' | 'Read only' | 'No access') { + // Wait for subsection to be visible + await this.toBeVisible(); + + // Click the dropdown button to open the menu + await expect(this.dropdownButton).toBeVisible(); + await this.dropdownButton.click(); + + // Wait for the MenuWrapper to have --open class which indicates menu is open + const menuWrapper = this.dropdownButton.locator('xpath=ancestor::div[contains(@class, "MenuWrapper")]'); + await expect(menuWrapper).toHaveClass(/MenuWrapper--open/); + + // Find the menu items and click the one matching the permission + const menuItem = menuWrapper.locator('.Menu__content li').filter({hasText: permission}); + await expect(menuItem).toBeVisible(); + await menuItem.click(); + + // Wait for menu to close + await expect(menuWrapper).not.toHaveClass(/MenuWrapper--open/); + } +} + +class AssignedPeoplePanel { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + readonly addPeopleButton: Locator; + readonly searchInput: Locator; + readonly userRows: Locator; + readonly paginationInfo: Locator; + readonly previousPageButton: Locator; + readonly nextPageButton: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.getByRole('heading', {name: 'Assigned People'}); + this.description = container.getByText('List of people assigned to this system role.'); + this.addPeopleButton = container.getByRole('button', {name: 'Add People'}); + this.searchInput = container.getByTestId('searchInput'); + this.userRows = container.locator('.DataGrid_rows .DataGrid_row'); + this.paginationInfo = container.locator('.DataGrid_footer span'); + this.previousPageButton = container.locator('.DataGrid_footer .prev'); + this.nextPageButton = container.locator('.DataGrid_footer .next'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.title).toBeVisible(); + } + + async clickAddPeople() { + await this.addPeopleButton.click(); + } + + async searchUsers(searchTerm: string) { + await this.searchInput.fill(searchTerm); + } + + async clearSearch() { + await this.searchInput.clear(); + } + + async getUserCount(): Promise { + return this.userRows.count(); + } + + /** + * Get a user row by index (0-based) + */ + getUserRowByIndex(index: number): AssignedUserRow { + return new AssignedUserRow(this.userRows.nth(index)); + } + + /** + * Get a user row by username + */ + getUserRowByUsername(username: string): AssignedUserRow { + const row = this.userRows.filter({hasText: username}); + return new AssignedUserRow(row); + } +} + +class AssignedUserRow { + readonly container: Locator; + readonly avatar: Locator; + readonly name: Locator; + readonly email: Locator; + readonly removeLink: Locator; + + constructor(container: Locator) { + this.container = container; + this.avatar = container.locator('.Avatar'); + this.name = container.locator('.UserGrid_name span').first(); + this.email = container.locator('.ug-email'); + this.removeLink = container.getByRole('link', {name: 'Remove'}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async getName(): Promise { + return (await this.name.textContent()) ?? ''; + } + + async getEmail(): Promise { + return (await this.email.textContent()) ?? ''; + } + + async remove() { + await this.removeLink.click(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/user_detail.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/user_detail.ts new file mode 100644 index 00000000000..197ca12ff0c --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/user_detail.ts @@ -0,0 +1,214 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console -> User Management -> Users -> User Detail + * Accessed by clicking on a user row in the Users list + */ +export default class UserDetail { + readonly container: Locator; + + // Header + readonly backLink: Locator; + readonly header: Locator; + + // User Card + readonly userCard: AdminUserCard; + + // Team Membership Panel + readonly teamMembershipPanel: TeamMembershipPanel; + + // Save section + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly errorMessage: Locator; + + constructor(container: Locator) { + this.container = container.locator('.SystemUserDetail'); + + // Header + this.backLink = this.container.locator('.admin-console__header .back'); + this.header = this.container.getByText('User Configuration', {exact: true}); + + // User Card + this.userCard = new AdminUserCard(this.container.locator('.AdminUserCard')); + + // Team Membership Panel + this.teamMembershipPanel = new TeamMembershipPanel( + this.container.locator('.AdminPanel').filter({hasText: 'Team Membership'}), + ); + + // Save section + this.saveButton = this.container.getByTestId('saveSetting'); + this.cancelButton = this.container.getByRole('button', {name: 'Cancel'}); + this.errorMessage = this.container.locator('.error-message'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } + + async goBack() { + await this.backLink.click(); + } + + async save() { + await expect(this.saveButton).toBeEnabled(); + await this.saveButton.click(); + } + + async cancel() { + await expect(this.cancelButton).toBeVisible(); + await this.cancelButton.click(); + } + + async waitForSaveComplete() { + await expect(this.saveButton).toBeDisabled(); + } +} + +class AdminUserCard { + readonly container: Locator; + + // Header section + readonly profileImage: Locator; + readonly displayName: Locator; + readonly nickname: Locator; + readonly userId: Locator; + + // Body section (two-column layout with fields) + readonly body: Locator; + readonly twoColumnLayout: Locator; + readonly fieldRows: Locator; + + // Footer section + readonly resetPasswordButton: Locator; + readonly deactivateButton: Locator; + readonly manageUserSettingsButton: Locator; + + constructor(container: Locator) { + this.container = container; + + // Header + const header = container.locator('.AdminUserCard__header'); + this.profileImage = header.locator('.Avatar'); + this.displayName = header.locator('.AdminUserCard__user-info span').first(); + this.nickname = header.locator('.AdminUserCard__user-nickname'); + this.userId = header.locator('.AdminUserCard__user-id'); + + // Body + this.body = container.locator('.AdminUserCard__body'); + this.twoColumnLayout = this.body.locator('.two-column-layout'); + this.fieldRows = this.body.locator('.field-row'); + + // Footer + const footer = container.locator('.AdminUserCard__footer'); + this.resetPasswordButton = footer.getByRole('button', {name: 'Reset Password'}); + this.deactivateButton = footer.getByRole('button', {name: 'Deactivate'}); + this.manageUserSettingsButton = footer.getByRole('button', {name: 'Manage User Settings'}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + getFieldByLabel(labelText: string): Locator { + return this.body.getByLabel(labelText); + } + + getSelectByLabel(labelText: string): Locator { + return this.body.getByLabel(labelText); + } + + async fillField(labelText: string, value: string) { + const input = this.getFieldByLabel(labelText); + await input.clear(); + await input.fill(value); + } + + async getFieldValue(labelText: string): Promise { + const input = this.getFieldByLabel(labelText); + return await input.inputValue(); + } + + async getUserId(): Promise { + const text = (await this.userId.textContent()) ?? ''; + return text.replace('User ID: ', ''); + } +} + +class TeamMembershipPanel { + readonly container: Locator; + readonly title: Locator; + readonly description: Locator; + readonly addTeamButton: Locator; + readonly teamRows: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.getByRole('heading', {name: 'Team Membership'}); + this.description = container.getByText('Teams to which this user belongs'); + this.addTeamButton = container.getByRole('button', {name: 'Add Team'}); + this.teamRows = container.locator('.TeamRow'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.title).toBeVisible(); + } + + async clickAddTeam() { + await this.addTeamButton.click(); + } + + async getTeamCount(): Promise { + return this.teamRows.count(); + } + + getTeamRowByIndex(index: number): TeamRow { + return new TeamRow(this.teamRows.nth(index)); + } + + getTeamRowByName(teamName: string): TeamRow { + return new TeamRow(this.teamRows.filter({hasText: teamName})); + } +} + +class TeamRow { + readonly container: Locator; + readonly teamName: Locator; + readonly teamType: Locator; + readonly role: Locator; + readonly actionMenuButton: Locator; + + constructor(container: Locator) { + this.container = container; + this.teamName = container.locator('.TeamRow__team-name b'); + this.teamType = container.locator('.TeamRow__description').first(); + this.role = container.locator('.TeamRow__description').last(); + this.actionMenuButton = container.locator('.TeamRow__actions button'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async getTeamName(): Promise { + return (await this.teamName.textContent()) ?? ''; + } + + async getTeamType(): Promise { + return (await this.teamType.textContent()) ?? ''; + } + + async getRole(): Promise { + return (await this.role.textContent()) ?? ''; + } + + async openActionMenu() { + await this.actionMenuButton.click(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/index.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/index.ts new file mode 100644 index 00000000000..aa18798e01f --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/index.ts @@ -0,0 +1,213 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +import UserDetail from '../user_detail'; + +import {ColumnToggleMenu, DateRangeMenu, FilterMenu, FilterPopover} from './menus'; +import {ManageRolesModal, ResetPasswordModal, UpdateEmailModal} from './modals'; +import {UsersTable} from './users_table'; + +import {ConfirmModal} from '@/ui/components/system_console/base_modal'; + +// Re-export sub-components for external use +export {ColumnToggleMenu, DateRangeMenu, FilterMenu, FilterPopover} from './menus'; +export {ManageRolesModal, ResetPasswordModal, UpdateEmailModal} from './modals'; +export {UserActionMenu} from './user_action_menu'; +export {UserRow, UsersTable} from './users_table'; + +/** + * System Console -> User Management -> Users + */ +export default class Users { + readonly container: Locator; + private readonly page; + + // User Detail page (accessed by clicking on a user row) + readonly userDetail: UserDetail; + + // Modals (opened from user actions) + readonly confirmModal: ConfirmModal; + readonly manageRolesModal: ManageRolesModal; + readonly resetPasswordModal: ResetPasswordModal; + readonly updateEmailModal: UpdateEmailModal; + + // Header + readonly header: Locator; + readonly revokeAllSessionsButton: Locator; + + // Filters section + readonly searchInput: Locator; + readonly filtersButton: Locator; + readonly columnToggleMenuButton: Locator; + readonly dateRangeSelectorMenuButton: Locator; + readonly exportButton: Locator; + + // Loading indicator + readonly loadingSpinner: Locator; + + // Table + readonly usersTable: UsersTable; + + // Menus and Popovers (populated from page-level locators) + readonly columnToggleMenu: ColumnToggleMenu; + readonly filterPopover: FilterPopover; + readonly roleFilterMenu: FilterMenu; + readonly statusFilterMenu: FilterMenu; + readonly dateRangeMenu: DateRangeMenu; + + // Pagination + readonly paginationInfo: Locator; + readonly previousPageButton: Locator; + readonly nextPageButton: Locator; + readonly rowsPerPageSelector: Locator; + + constructor(container: Locator) { + this.container = container; + this.page = container.page(); + + this.userDetail = new UserDetail(container); + + // Modals + this.confirmModal = new ConfirmModal(this.page.locator('#confirmModal')); + this.manageRolesModal = new ManageRolesModal(this.page.locator('.manage-teams')); + this.resetPasswordModal = new ResetPasswordModal(this.page.locator('#resetPasswordModal')); + this.updateEmailModal = new UpdateEmailModal(this.page.locator('#resetEmailModal')); + + this.header = container.getByText('Mattermost Users', {exact: true}); + this.revokeAllSessionsButton = container.getByRole('button', {name: 'Revoke All Sessions'}); + + this.searchInput = container.getByRole('textbox', {name: 'Search users'}); + this.filtersButton = container.getByRole('button', {name: /Filters/}); + this.columnToggleMenuButton = container.locator('#systemUsersColumnTogglerMenuButton'); + this.dateRangeSelectorMenuButton = container.locator('#systemUsersDateRangeSelectorMenuButton'); + this.exportButton = container.getByText('Export'); + + this.loadingSpinner = container.getByText('Loading'); + + this.usersTable = new UsersTable(container.locator('#systemUsersTable')); + + this.columnToggleMenu = new ColumnToggleMenu(this.page.locator('#systemUsersColumnTogglerMenu')); + this.filterPopover = new FilterPopover(this.page.locator('#systemUsersFilterPopover')); + this.roleFilterMenu = new FilterMenu(this.page.locator('.DropDown__menu')); + this.statusFilterMenu = new FilterMenu(this.page.locator('.DropDown__menu')); + this.dateRangeMenu = new DateRangeMenu(this.page.locator('#systemUsersDateRangeSelectorMenu')); + + const footer = container.locator('.adminConsoleListTabletOptionalFoot'); + this.paginationInfo = footer.locator('span').first(); + this.previousPageButton = footer.getByRole('button', {name: 'Go to previous page'}); + this.nextPageButton = footer.getByRole('button', {name: 'Go to next page'}); + this.rowsPerPageSelector = footer.locator('.adminConsoleListTablePageSize .react-select'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.header).toBeVisible(); + } + + /** + * Wait for loading to complete + */ + async isLoadingComplete() { + await expect(this.loadingSpinner).toHaveCount(0); + } + + /** + * Search for users by typing in the search input + */ + async searchUsers(searchTerm: string) { + await this.searchInput.fill(searchTerm); + await this.isLoadingComplete(); + } + + /** + * Clear the search input + */ + async clearSearch() { + await this.searchInput.clear(); + } + + /** + * Get the current filter count from the Filters button + */ + async getFilterCount(): Promise { + const buttonText = await this.filtersButton.textContent(); + const match = buttonText?.match(/Filters \((\d+)\)/); + return match ? parseInt(match[1], 10) : 0; + } + + /** + * Open the column toggle menu + */ + async openColumnToggleMenu(): Promise { + await expect(this.columnToggleMenuButton).toBeVisible(); + await this.columnToggleMenuButton.click(); + await this.columnToggleMenu.toBeVisible(); + return this.columnToggleMenu; + } + + /** + * Open the filter popover + */ + async openFilterPopover(): Promise { + await expect(this.filtersButton).toBeVisible(); + await this.filtersButton.click(); + await this.filterPopover.toBeVisible(); + return this.filterPopover; + } + + /** + * Open the date range selector menu + */ + async openDateRangeSelectorMenu(): Promise { + await expect(this.dateRangeSelectorMenuButton).toBeVisible(); + await this.dateRangeSelectorMenuButton.click(); + await this.dateRangeMenu.toBeVisible(); + return this.dateRangeMenu; + } + + /** + * Click the Export button + */ + async clickExport() { + await this.exportButton.click(); + } + + /** + * Click Revoke All Sessions button + */ + async clickRevokeAllSessions() { + await this.revokeAllSessionsButton.click(); + } + + /** + * Go to next page + */ + async goToNextPage() { + await this.nextPageButton.click(); + } + + /** + * Go to previous page + */ + async goToPreviousPage() { + await this.previousPageButton.click(); + } + + /** + * Get the pagination info text (e.g., "Showing 1 - 10 of 212 users") + */ + async getPaginationInfo(): Promise { + return (await this.paginationInfo.textContent()) ?? ''; + } + + /** + * Get the total user count from pagination info + */ + async getTotalUserCount(): Promise { + const text = await this.getPaginationInfo(); + const match = text.match(/of (\d+) users/); + return match ? parseInt(match[1], 10) : 0; + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/menus.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/menus.ts new file mode 100644 index 00000000000..424a22bd219 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/menus.ts @@ -0,0 +1,196 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * Column toggle menu that appears when clicking the Columns button + */ +export class ColumnToggleMenu { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get a menu item by text + */ + async getMenuItem(menuItem: string): Promise { + const menuItemLocator = this.container.getByRole('menuitemcheckbox').filter({hasText: menuItem}); + await menuItemLocator.waitFor(); + return menuItemLocator; + } + + /** + * Get all menu items + */ + getAllMenuItems(): Locator { + return this.container.getByRole('menuitemcheckbox'); + } + + /** + * Click a menu item to toggle it + */ + async clickMenuItem(menuItem: string) { + const item = await this.getMenuItem(menuItem); + await item.click(); + } + + /** + * Close the menu + */ + async close() { + await this.container.press('Escape'); + await expect(this.container).not.toBeVisible(); + } +} + +/** + * Filter popover that appears when clicking the Filters button + */ +export class FilterPopover { + readonly container: Locator; + readonly teamMenuInput: Locator; + readonly roleMenuButton: Locator; + readonly statusMenuButton: Locator; + readonly applyButton: Locator; + + constructor(container: Locator) { + this.container = container; + this.teamMenuInput = container.locator('#asyncTeamSelectInput'); + this.roleMenuButton = container.locator('#DropdownInput_filterRole'); + this.statusMenuButton = container.locator('#DropdownInput_filterStatus'); + this.applyButton = container.getByText('Apply'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.applyButton).toBeVisible(); + } + + /** + * Save/apply the filter settings and wait for popover to close + */ + async save() { + await this.applyButton.click(); + // Apply button closes the popover, wait for it to close + await expect(this.container).not.toBeVisible({timeout: 5000}); + } + + /** + * Search in the team filter and wait for dropdown options + */ + async searchInTeamMenu(teamDisplayName: string) { + await expect(this.teamMenuInput).toBeVisible(); + await this.teamMenuInput.fill(teamDisplayName); + // Wait a bit for async search results to appear + await this.container.page().waitForTimeout(500); + } + + /** + * Open the role filter menu + */ + async openRoleMenu() { + await expect(this.roleMenuButton).toBeVisible(); + await this.roleMenuButton.click(); + } + + /** + * Open the status filter menu + */ + async openStatusMenu() { + await expect(this.statusMenuButton).toBeVisible(); + await this.statusMenuButton.click(); + } + + /** + * Close the popover (if still open) + */ + async close() { + const isVisible = await this.container.isVisible(); + if (isVisible) { + await this.container.press('Escape'); + await expect(this.container).not.toBeVisible(); + } + } +} + +/** + * Generic filter menu for role/status dropdowns (react-select dropdown) + */ +export class FilterMenu { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get a menu item by text + */ + async getMenuItem(menuItem: string): Promise { + const menuItemLocator = this.container.getByText(menuItem); + await menuItemLocator.waitFor(); + return menuItemLocator; + } + + /** + * Click a menu item (this also closes the dropdown automatically) + */ + async clickMenuItem(menuItem: string) { + const item = await this.getMenuItem(menuItem); + await item.click(); + // Dropdown closes automatically after selection, wait for it + await expect(this.container).not.toBeVisible({timeout: 5000}); + } + + /** + * Close the menu (if still open) + */ + async close() { + const isVisible = await this.container.isVisible(); + if (isVisible) { + await this.container.press('Escape'); + } + } +} + +/** + * Date range selector menu + */ +export class DateRangeMenu { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Click a menu item + */ + async clickMenuItem(menuItem: string) { + const item = this.container.getByText(menuItem); + await item.waitFor(); + await item.click(); + } + + /** + * Close the menu + */ + async close() { + await this.container.press('Escape'); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/modals.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/modals.ts new file mode 100644 index 00000000000..42f682297c6 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/modals.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +import BaseModal from '@/ui/components/system_console/base_modal'; + +/** + * Manage Roles modal (System Console -> Users -> Action -> Manage roles) + */ +export class ManageRolesModal extends BaseModal { + readonly saveButton: Locator; + + constructor(container: Locator) { + super(container); + this.saveButton = container.getByRole('button', {name: 'Save'}); + } + + async save() { + await this.saveButton.click(); + await expect(this.container).not.toBeVisible(); + } +} + +/** + * Reset Password modal (System Console -> Users -> Action -> Reset password) + */ +export class ResetPasswordModal extends BaseModal { + readonly resetButton: Locator; + readonly passwordInput: Locator; + + constructor(container: Locator) { + super(container); + this.resetButton = container.getByRole('button', {name: 'Reset'}); + this.passwordInput = container.locator('input[type="password"]'); + } + + async reset() { + await this.resetButton.click(); + await expect(this.container).not.toBeVisible(); + } + + async fillPassword(password: string) { + await this.passwordInput.fill(password); + } +} + +/** + * Update Email modal (System Console -> Users -> Action -> Update email) + */ +export class UpdateEmailModal extends BaseModal { + readonly updateButton: Locator; + readonly emailInput: Locator; + + constructor(container: Locator) { + super(container); + this.updateButton = container.getByRole('button', {name: 'Update'}); + this.emailInput = container.locator('input[type="email"]'); + } + + async update() { + await this.updateButton.click(); + await expect(this.container).not.toBeVisible(); + } + + async fillEmail(email: string) { + await this.emailInput.fill(email); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/user_action_menu.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/user_action_menu.ts new file mode 100644 index 00000000000..b2db999dd46 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/user_action_menu.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * User action menu that appears when clicking the action button on a user row + */ +export class UserActionMenu { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get a menu item by text + */ + getMenuItem(text: string): Locator { + return this.container.getByText(text, {exact: true}); + } + + /** + * Click a menu item by text + */ + async clickMenuItem(text: string) { + const item = this.getMenuItem(text); + await item.click(); + } + + async clickDeactivate() { + await this.clickMenuItem('Deactivate'); + } + + async clickActivate() { + await this.clickMenuItem('Activate'); + } + + async clickManageRoles() { + await this.clickMenuItem('Manage roles'); + } + + async clickManageTeams() { + await this.clickMenuItem('Manage teams'); + } + + async clickResetPassword() { + await this.clickMenuItem('Reset password'); + } + + async clickUpdateEmail() { + await this.clickMenuItem('Update email'); + } + + async clickRevokeSessions() { + await this.clickMenuItem('Revoke sessions'); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/users_table.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/users_table.ts new file mode 100644 index 00000000000..feb9d1c840a --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/user_management/users/users_table.ts @@ -0,0 +1,209 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +import {UserActionMenu} from './user_action_menu'; + +/** + * Users table component + */ +export class UsersTable { + readonly container: Locator; + readonly headerRow: Locator; + readonly bodyRows: Locator; + + // Column headers + readonly userDetailsHeader: Locator; + readonly emailHeader: Locator; + readonly memberSinceHeader: Locator; + readonly lastLoginHeader: Locator; + readonly lastActivityHeader: Locator; + readonly lastPostHeader: Locator; + readonly daysActiveHeader: Locator; + readonly messagesPostedHeader: Locator; + readonly actionsHeader: Locator; + + constructor(container: Locator) { + this.container = container; + this.headerRow = container.locator('thead tr'); + this.bodyRows = container.locator('tbody tr'); + + // Column headers + this.userDetailsHeader = container.locator('#systemUsersTable-header-usernameColumn'); + this.emailHeader = container.locator('#systemUsersTable-header-emailColumn'); + this.memberSinceHeader = container.locator('#systemUsersTable-header-createAtColumn'); + this.lastLoginHeader = container.locator('#systemUsersTable-header-lastLoginColumn'); + this.lastActivityHeader = container.locator('#systemUsersTable-header-lastStatusAtColumn'); + this.lastPostHeader = container.locator('#systemUsersTable-header-lastPostDateColumn'); + this.daysActiveHeader = container.locator('#systemUsersTable-header-daysActiveColumn'); + this.messagesPostedHeader = container.locator('#systemUsersTable-header-totalPostsColumn'); + this.actionsHeader = container.locator('#systemUsersTable-header-actionsColumn'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Get a user row by index (0-based) + */ + getRowByIndex(index: number): UserRow { + return new UserRow(this.bodyRows.nth(index), index); + } + + /** + * Get a column header by display name + */ + getColumnHeader(columnName: string): Locator { + const headerMap: Record = { + 'User details': this.userDetailsHeader, + Email: this.emailHeader, + 'Member since': this.memberSinceHeader, + 'Last login': this.lastLoginHeader, + 'Last activity': this.lastActivityHeader, + 'Last post': this.lastPostHeader, + 'Days active': this.daysActiveHeader, + 'Messages posted': this.messagesPostedHeader, + Actions: this.actionsHeader, + }; + const header = headerMap[columnName]; + if (!header) { + throw new Error(`Unknown column: ${columnName}`); + } + return header; + } + + /** + * Click on a column header to sort by that column + */ + async clickSortOnColumn(columnName: string) { + const header = this.getColumnHeader(columnName); + await header.click(); + } + + /** + * Click on a sortable column header and wait for sort to complete + * @param columnName - The display name of the column + * @returns The new sort direction after clicking + */ + async sortByColumn(columnName: string): Promise<'ascending' | 'descending' | 'none'> { + const header = this.getColumnHeader(columnName); + + // Get current sort direction + const currentSort = await header.getAttribute('aria-sort'); + + // Click to sort + await header.click(); + + // Wait for sort direction to change (or for it to be set if it wasn't before) + if (currentSort) { + // Wait for the attribute to change + await expect(header).not.toHaveAttribute('aria-sort', currentSort); + } else { + // Wait for the attribute to be set + await expect(header).toHaveAttribute('aria-sort'); + } + + // Wait for table to stabilize + await this.waitForLoadingComplete(); + + // Return the new sort direction + const newSort = await header.getAttribute('aria-sort'); + return (newSort as 'ascending' | 'descending' | 'none') ?? 'none'; + } + + /** + * Wait for the table to finish loading (spinner to disappear) + */ + async waitForLoadingComplete() { + // Wait for any loading spinners to disappear + const loadingSpinner = this.container.locator('.loading-screen, .LoadingSpinner'); + await loadingSpinner.waitFor({state: 'detached', timeout: 10000}).catch(() => { + // Spinner may not appear for fast loads, ignore timeout + }); + // Also wait for at least one row to be visible + await this.bodyRows.first().waitFor({state: 'visible'}); + } +} + +/** + * A single row in the users table + */ +export class UserRow { + readonly container: Locator; + readonly index: number; + + // Cells + readonly userDetailsCell: Locator; + readonly emailCell: Locator; + readonly memberSinceCell: Locator; + readonly lastLoginCell: Locator; + readonly lastActivityCell: Locator; + readonly lastPostCell: Locator; + readonly daysActiveCell: Locator; + readonly messagesPostedCell: Locator; + readonly actionsCell: Locator; + + // User details components + readonly profilePicture: Locator; + readonly displayName: Locator; + readonly userName: Locator; + + // Action menu button + readonly actionMenuButton: Locator; + + // Action menu (populated after opening) + private readonly actionMenu: UserActionMenu; + + constructor(container: Locator, index: number) { + this.container = container; + this.index = index; + + this.userDetailsCell = container.locator('.usernameColumn'); + this.emailCell = container.locator('.emailColumn'); + this.memberSinceCell = container.locator('.createAtColumn'); + this.lastLoginCell = container.locator('.lastLoginColumn'); + this.lastActivityCell = container.locator('.lastStatusAtColumn'); + this.lastPostCell = container.locator('.lastPostDateColumn'); + this.daysActiveCell = container.locator('.daysActiveColumn'); + this.messagesPostedCell = container.locator('.totalPostsColumn'); + this.actionsCell = container.locator('.actionsColumn'); + + this.profilePicture = this.userDetailsCell.locator('.profilePicture'); + this.displayName = this.userDetailsCell.locator('.displayName'); + this.userName = this.userDetailsCell.locator('.userName'); + + this.actionMenuButton = this.actionsCell.getByRole('button'); + + this.actionMenu = new UserActionMenu(container.page().locator(`#actionMenu-systemUsersTable-${index}`)); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + /** + * Click on the row to view user details + */ + async click() { + await this.container.click(); + } + + /** + * Get the email + */ + async getEmail(): Promise { + return (await this.emailCell.textContent()) ?? ''; + } + + /** + * Click the action menu button to open the actions dropdown + * Returns the action menu for further interactions + */ + async openActionMenu(): Promise { + await this.actionMenuButton.click(); + await this.actionMenu.toBeVisible(); + return this.actionMenu; + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar.ts index 05d8c76692e..e06fddd6905 100644 --- a/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar.ts +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar.ts @@ -3,37 +3,342 @@ import {Locator, expect} from '@playwright/test'; +import SystemConsoleSidebarHeader from './sidebar_header'; + export default class SystemConsoleSidebar { readonly container: Locator; - + readonly header: SystemConsoleSidebarHeader; readonly searchInput: Locator; + readonly about: AboutCategory; + readonly reporting: ReportingCategory; + readonly userManagement: UserManagementCategory; + readonly systemAttributes: SystemAttributesCategory; + readonly environment: EnvironmentCategory; + readonly siteConfiguration: SiteConfigurationCategory; + readonly authentication: AuthenticationCategory; + readonly plugins: PluginsCategory; + readonly integrations: IntegrationsCategory; + readonly compliance: ComplianceCategory; + readonly experimental: ExperimentalCategory; + constructor(container: Locator) { this.container = container; - + this.header = new SystemConsoleSidebarHeader(container.locator('.AdminSidebarHeader')); this.searchInput = container.getByPlaceholder('Find settings'); + + this.about = new AboutCategory(container.getByTestId('about')); + this.reporting = new ReportingCategory(container.getByTestId('reporting')); + this.userManagement = new UserManagementCategory(container.getByTestId('user_management')); + this.systemAttributes = new SystemAttributesCategory(container.getByTestId('system_attributes')); + this.environment = new EnvironmentCategory(container.getByTestId('environment')); + this.siteConfiguration = new SiteConfigurationCategory(container.getByTestId('site')); + this.authentication = new AuthenticationCategory(container.getByTestId('authentication')); + this.plugins = new PluginsCategory(container.getByTestId('plugins')); + this.integrations = new IntegrationsCategory(container.getByTestId('integrations')); + this.compliance = new ComplianceCategory(container.getByTestId('compliance')); + this.experimental = new ExperimentalCategory(container.getByTestId('experimental')); } async toBeVisible() { await expect(this.container).toBeVisible(); + await this.header.toBeVisible(); await expect(this.searchInput).toBeVisible(); } - /** - * Clicks on the sidebar section link with the given name. Pass the exact name of the section. - * @param sectionName - */ - async goToItem(sectionName: string) { - const section = this.container.getByText(sectionName, {exact: true}); - await section.waitFor(); - await section.click(); + async search(text: string) { + await this.searchInput.fill(text); } - /** - * Searches for the given item in the sidebar search input. - * @param itemName - */ - async searchForItem(itemName: string) { - await this.searchInput.fill(itemName); + async clearSearch() { + await this.searchInput.clear(); + } + + // Convenience shortcuts + get editionAndLicense() { + return this.about.editionAndLicense; + } + get users() { + return this.userManagement.users; + } + get groups() { + return this.userManagement.groups; + } + get teams() { + return this.userManagement.teams; + } + get channels() { + return this.userManagement.channels; + } + get permissions() { + return this.userManagement.permissions; + } + get delegatedGranularAdministration() { + return this.userManagement.delegatedGranularAdministration; + } + get mobileSecurity() { + return this.environment.mobileSecurity; + } + get notifications() { + return this.siteConfiguration.notifications; + } + get pluginManagement() { + return this.plugins.pluginManagement; + } +} + +class SidebarSection { + readonly container: Locator; + readonly link: Locator; + + constructor(container: Locator, link: Locator) { + this.container = container; + this.link = link; + } + + async click() { + await this.link.click(); + } + + async isActive(): Promise { + const classAttr = await this.link.getAttribute('class'); + return classAttr?.includes('sidebar-section-title--active') ?? false; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +class SidebarCategory { + readonly container: Locator; + readonly title: Locator; + readonly sections: Locator; + + constructor(container: Locator) { + this.container = container; + this.title = container.locator('.category-title'); + this.sections = container.locator('ul.sections'); + } + + protected section(name: string): SidebarSection { + const link = this.sections.getByRole('link', {name, exact: true}); + return new SidebarSection(this.sections, link); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +class AboutCategory extends SidebarCategory { + readonly editionAndLicense: SidebarSection; + + constructor(container: Locator) { + super(container); + this.editionAndLicense = this.section('Edition and License'); + } +} + +class ReportingCategory extends SidebarCategory { + readonly workspaceOptimization: SidebarSection; + readonly siteStatistics: SidebarSection; + readonly teamStatistics: SidebarSection; + readonly serverLogs: SidebarSection; + + constructor(container: Locator) { + super(container); + this.workspaceOptimization = this.section('Workspace Optimization'); + this.siteStatistics = this.section('Site Statistics'); + this.teamStatistics = this.section('Team Statistics'); + this.serverLogs = this.section('Server Logs'); + } +} + +class UserManagementCategory extends SidebarCategory { + readonly users: SidebarSection; + readonly groups: SidebarSection; + readonly teams: SidebarSection; + readonly channels: SidebarSection; + readonly permissions: SidebarSection; + readonly delegatedGranularAdministration: SidebarSection; + + constructor(container: Locator) { + super(container); + this.users = this.section('Users'); + this.groups = this.section('Groups'); + this.teams = this.section('Teams'); + this.channels = this.section('Channels'); + this.permissions = this.section('Permissions'); + this.delegatedGranularAdministration = this.section('Delegated Granular Administration'); + } +} + +class SystemAttributesCategory extends SidebarCategory { + readonly userAttributes: SidebarSection; + readonly attributeBasedAccess: SidebarSection; + + constructor(container: Locator) { + super(container); + this.userAttributes = this.section('User Attributes'); + this.attributeBasedAccess = this.section('Attribute-Based Access'); + } +} + +class EnvironmentCategory extends SidebarCategory { + readonly webServer: SidebarSection; + readonly database: SidebarSection; + readonly elasticsearch: SidebarSection; + readonly fileStorage: SidebarSection; + readonly imageProxy: SidebarSection; + readonly smtp: SidebarSection; + readonly pushNotificationServer: SidebarSection; + readonly highAvailability: SidebarSection; + readonly cacheSettings: SidebarSection; + readonly rateLimiting: SidebarSection; + readonly logging: SidebarSection; + readonly sessionLengths: SidebarSection; + readonly performanceMonitoring: SidebarSection; + readonly developer: SidebarSection; + readonly mobileSecurity: SidebarSection; + + constructor(container: Locator) { + super(container); + this.webServer = this.section('Web Server'); + this.database = this.section('Database'); + this.elasticsearch = this.section('Elasticsearch'); + this.fileStorage = this.section('File Storage'); + this.imageProxy = this.section('Image Proxy'); + this.smtp = this.section('SMTP'); + this.pushNotificationServer = this.section('Push Notification Server'); + this.highAvailability = this.section('High Availability'); + this.cacheSettings = this.section('Cache Settings'); + this.rateLimiting = this.section('Rate Limiting'); + this.logging = this.section('Logging'); + this.sessionLengths = this.section('Session Lengths'); + this.performanceMonitoring = this.section('Performance Monitoring'); + this.developer = this.section('Developer'); + this.mobileSecurity = this.section('Mobile Security'); + } +} + +class SiteConfigurationCategory extends SidebarCategory { + readonly customization: SidebarSection; + readonly localization: SidebarSection; + readonly usersAndTeams: SidebarSection; + readonly notifications: SidebarSection; + readonly systemWideNotifications: SidebarSection; + readonly emoji: SidebarSection; + readonly posts: SidebarSection; + readonly contentFlagging: SidebarSection; + readonly moveThread: SidebarSection; + readonly fileSharingAndDownloads: SidebarSection; + readonly publicLinks: SidebarSection; + readonly notices: SidebarSection; + + constructor(container: Locator) { + super(container); + this.customization = this.section('Customization'); + this.localization = this.section('Localization'); + this.usersAndTeams = this.section('Users and Teams'); + this.notifications = this.section('Notifications'); + this.systemWideNotifications = this.section('System-wide Notifications'); + this.emoji = this.section('Emoji'); + this.posts = this.section('Posts'); + this.contentFlagging = this.section('Content Flagging'); + this.moveThread = this.section('Move Thread (Beta)'); + this.fileSharingAndDownloads = this.section('File Sharing and Downloads'); + this.publicLinks = this.section('Public Links'); + this.notices = this.section('Notices'); + } +} + +class AuthenticationCategory extends SidebarCategory { + readonly signup: SidebarSection; + readonly email: SidebarSection; + readonly password: SidebarSection; + readonly mfa: SidebarSection; + readonly adLdap: SidebarSection; + readonly saml: SidebarSection; + readonly openIdConnect: SidebarSection; + readonly guestAccess: SidebarSection; + + constructor(container: Locator) { + super(container); + this.signup = this.section('Signup'); + this.email = this.section('Email'); + this.password = this.section('Password'); + this.mfa = this.section('MFA'); + this.adLdap = this.section('AD/LDAP'); + this.saml = this.section('SAML 2.0'); + this.openIdConnect = this.section('OpenID Connect'); + this.guestAccess = this.section('Guest Access'); + } +} + +class PluginsCategory extends SidebarCategory { + readonly pluginManagement: SidebarSection; + readonly agents: SidebarSection; + readonly calls: SidebarSection; + readonly playbooks: SidebarSection; + readonly boards: SidebarSection; + + constructor(container: Locator) { + super(container); + this.pluginManagement = this.section('Plugin Management'); + this.agents = this.section('Agents'); + this.calls = this.section('Calls'); + this.playbooks = this.section('Playbooks'); + this.boards = this.section('Mattermost Boards'); + } + + getPlugin(pluginName: string): SidebarSection { + const link = this.sections.getByRole('link', {name: pluginName, exact: true}); + return new SidebarSection(this.sections, link); + } +} + +class IntegrationsCategory extends SidebarCategory { + readonly integrationManagement: SidebarSection; + readonly botAccounts: SidebarSection; + readonly gif: SidebarSection; + readonly cors: SidebarSection; + readonly embedding: SidebarSection; + + constructor(container: Locator) { + super(container); + this.integrationManagement = this.section('Integration Management'); + this.botAccounts = this.section('Bot Accounts'); + this.gif = this.section('GIF'); + this.cors = this.section('CORS'); + this.embedding = this.section('Embedding'); + } +} + +class ComplianceCategory extends SidebarCategory { + readonly dataRetentionPolicies: SidebarSection; + readonly complianceExport: SidebarSection; + readonly complianceMonitoring: SidebarSection; + readonly auditLogging: SidebarSection; + readonly customTermsOfService: SidebarSection; + + constructor(container: Locator) { + super(container); + this.dataRetentionPolicies = this.section('Data Retention Policies'); + this.complianceExport = this.section('Compliance Export'); + this.complianceMonitoring = this.section('Compliance Monitoring'); + this.auditLogging = this.section('Audit Logging'); + this.customTermsOfService = this.section('Custom Terms of Service'); + } +} + +class ExperimentalCategory extends SidebarCategory { + readonly features: SidebarSection; + readonly featureFlags: SidebarSection; + + constructor(container: Locator) { + super(container); + this.features = this.section('Features'); + this.featureFlags = this.section('Feature Flags'); } } diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar_header.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar_header.ts new file mode 100644 index 00000000000..f0e9d144c47 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sidebar_header.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console Sidebar Header component + */ +export default class SystemConsoleSidebarHeader { + readonly container: Locator; + readonly headerInfo: Locator; + readonly title: Locator; + readonly userName: Locator; + readonly menuButton: Locator; + + constructor(container: Locator) { + this.container = container; + this.headerInfo = container.locator('.header__info'); + this.title = container.getByText('System Console'); + this.userName = container.getByText(/^@/); + this.menuButton = container.getByRole('button', {name: 'Menu Icon'}); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.title).toBeVisible(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/pages/system_console.ts b/e2e-tests/playwright/lib/src/ui/pages/system_console.ts index 076af0aeadb..fc29c31fbb1 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/system_console.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/system_console.ts @@ -3,101 +3,81 @@ import {Page} from '@playwright/test'; -import {components} from '@/ui/components'; +import SystemConsoleNavbar from '@/ui/components/system_console/navbar'; +import SystemConsoleSidebar from '@/ui/components/system_console/sidebar'; +import SystemConsoleHeader from '@/ui/components/system_console/header'; +import EditionAndLicense from '@/ui/components/system_console/sections/about/edition_and_license'; +import TeamStatistics from '@/ui/components/system_console/sections/reporting/team_statistics'; +import Users from '@/ui/components/system_console/sections/user_management/users'; +import DelegatedGranularAdministration from '@/ui/components/system_console/sections/user_management/delegated_granular_administration'; +import MobileSecurity from '@/ui/components/system_console/sections/environment/mobile_security'; +import Notifications from '@/ui/components/system_console/sections/site_configuration/notifications'; +import FeatureDiscovery from '@/ui/components/system_console/sections/system_users/feature_discovery'; export default class SystemConsolePage { readonly page: Page; - readonly sidebar; - readonly navbar; + // Layout + readonly navbar: SystemConsoleNavbar; + readonly sidebar: SystemConsoleSidebar; + readonly header: SystemConsoleHeader; + + // About + readonly editionAndLicense: EditionAndLicense; + + // Reporting + readonly teamStatistics: TeamStatistics; + + // User Management + readonly users: Users; + readonly delegatedGranularAdministration: DelegatedGranularAdministration; + + // Environment + readonly mobileSecurity: MobileSecurity; // Site Configuration + readonly notifications: Notifications; - // System Console > Notifications - readonly notifications; - - /** - * System Console -> User Management -> Users - */ - readonly systemUsers; - readonly systemUsersFilterPopover; - readonly systemUsersRoleMenu; - readonly systemUsersStatusMenu; - readonly systemUsersDateRangeMenu; - readonly systemUsersColumnToggleMenu; - readonly systemUsersActionMenus; - - readonly mobileSecurity; - - readonly featureDiscovery; - - // modal - readonly confirmModal; - readonly exportModal; - readonly saveChangesModal; + // Feature Discovery (license-gated features) + readonly featureDiscovery: FeatureDiscovery; constructor(page: Page) { this.page = page; + // Layout + this.navbar = new SystemConsoleNavbar(page.locator('.backstage-navbar')); + this.sidebar = new SystemConsoleSidebar(page.locator('.admin-sidebar')); + + const adminConsoleWrapper = page.locator('#adminConsoleWrapper'); + this.header = new SystemConsoleHeader(adminConsoleWrapper); + + // About + this.editionAndLicense = new EditionAndLicense(adminConsoleWrapper); + + // Reporting + this.teamStatistics = new TeamStatistics(adminConsoleWrapper); + + // User Management + this.users = new Users(adminConsoleWrapper); + this.delegatedGranularAdministration = new DelegatedGranularAdministration(adminConsoleWrapper); + + // Environment + this.mobileSecurity = new MobileSecurity(adminConsoleWrapper); + // Site Configuration - // System Console > Notifications - this.notifications = new components.SystemConsoleNotifications( - page.getByTestId('sysconsole_section_notifications'), - ); + this.notifications = new Notifications(adminConsoleWrapper); - // Areas of the page - this.navbar = new components.SystemConsoleNavbar(page.locator('.backstage-navbar')); - this.sidebar = new components.SystemConsoleSidebar(page.locator('.admin-sidebar')); - - // Sections and sub-sections - this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection')); - this.mobileSecurity = new components.SystemConsoleMobileSecurity( - page.getByTestId('sysconsole_section_MobileSecuritySettings'), - this.page, - ); - this.featureDiscovery = new components.SystemConsoleFeatureDiscovery(page.getByTestId('featureDiscovery')); - - // Menus & Popovers - this.systemUsersFilterPopover = new components.SystemUsersFilterPopover( - page.locator('#systemUsersFilterPopover'), - ); - this.systemUsersRoleMenu = new components.SystemUsersFilterMenu(page.locator('#DropdownInput_filterRole')); - this.systemUsersStatusMenu = new components.SystemUsersFilterMenu(page.locator('#DropdownInput_filterStatus')); - this.systemUsersColumnToggleMenu = new components.SystemUsersColumnToggleMenu( - page.locator('#systemUsersColumnTogglerMenu'), - ); - this.systemUsersDateRangeMenu = new components.SystemUsersFilterMenu( - page.locator('#systemUsersDateRangeSelectorMenu'), - ); - this.systemUsersActionMenus = Array.from(Array(10).keys()).map( - (index) => new components.SystemUsersFilterMenu(page.locator(`#actionMenu-systemUsersTable-${index}`)), - ); - - this.confirmModal = new components.GenericConfirmModal(page.locator('#confirmModal')); - this.exportModal = new components.GenericConfirmModal(page.getByRole('dialog', {name: 'Export user data'})); - this.saveChangesModal = new components.SystemUsers(page.locator('div.modal-content')); + // Feature Discovery + this.featureDiscovery = new FeatureDiscovery(adminConsoleWrapper); } async toBeVisible() { await this.page.waitForLoadState('networkidle'); - - await this.sidebar.toBeVisible(); await this.navbar.toBeVisible(); + await this.sidebar.toBeVisible(); } async goto() { await this.page.goto('/admin_console'); } - - async saveRoleChange() { - await this.saveChangesModal.container.locator('button.btn-primary:has-text("Save")').click(); - } - - async clickResetButton() { - await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click(); - } - - async clickUpdateEmailButton() { - await this.saveChangesModal.container.locator('button.btn-primary:has-text("Update")').click(); - } } diff --git a/e2e-tests/playwright/specs/functional/channels/notifications/system_console.spec.ts b/e2e-tests/playwright/specs/functional/channels/notifications/system_console.spec.ts index cb7b04cd3ab..b3f84f5e620 100644 --- a/e2e-tests/playwright/specs/functional/channels/notifications/system_console.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/notifications/system_console.spec.ts @@ -33,18 +33,18 @@ test('Push Notification Contents setting displays correctly and saves all option // # Visit Notifications admin console page await systemConsolePage.goto(); await systemConsolePage.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Notifications'); + await systemConsolePage.sidebar.notifications.click(); // # Wait for Notifications section to load const notifications = systemConsolePage.notifications; await notifications.toBeVisible(); // * Verify that setting is visible and matches text content - await notifications.pushNotificationContents.scrollIntoViewIfNeeded(); - await expect(notifications.pushNotificationContents).toBeVisible(); + await notifications.pushNotificationContents.container.scrollIntoViewIfNeeded(); + await notifications.pushNotificationContents.toBeVisible(); // * Verify that the help text is visible and matches text content - const helpText = notifications.pushNotificationContentsHelpText; + const helpText = notifications.pushNotificationContents.helpText; await expect(helpText).toBeVisible(); const contents = [ @@ -69,7 +69,7 @@ test('Push Notification Contents setting displays correctly and saves all option await expect(strongElements.nth(4)).toHaveText(contents[8]); // * Verify that the option/dropdown is visible and has default value - const dropdown = notifications.pushNotificationContentsDropdown; + const dropdown = notifications.pushNotificationContents.dropdown; await expect(dropdown).toBeVisible(); await expect(dropdown).toHaveValue('full'); @@ -86,7 +86,7 @@ test('Push Notification Contents setting displays correctly and saves all option await dropdown.selectOption({label: option.label}); await expect(dropdown).toHaveValue(option.value); - await notifications.saveButton.click(); + await notifications.save(); // * Verify config is saved const {adminClient} = await pw.getAdminClient(); @@ -111,31 +111,30 @@ test('MM-T1210 Can change Support Email setting', async ({pw}) => { // # Visit Notifications admin console page await systemConsolePage.goto(); await systemConsolePage.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Notifications'); + await systemConsolePage.sidebar.notifications.click(); // # Wait for Notifications section to load const notifications = systemConsolePage.notifications; await notifications.toBeVisible(); // # Scroll Support Email section into view and verify that it's visible - const supportEmailSetting = notifications.supportEmailAddress; - await supportEmailSetting.scrollIntoViewIfNeeded(); - await expect(supportEmailSetting).toBeVisible(); + await notifications.supportEmailAddress.container.scrollIntoViewIfNeeded(); + await notifications.supportEmailAddress.toBeVisible(); // * Verify that the help text is visible and matches text content - await expect(notifications.supportEmailHelpText).toBeVisible(); - await expect(notifications.supportEmailHelpText).toHaveText('Email address displayed on support emails.'); + await expect(notifications.supportEmailAddress.helpText).toBeVisible(); + await expect(notifications.supportEmailAddress.helpText).toHaveText('Email address displayed on support emails.'); // # Clear and type new email const newEmail = 'changed_for_test_support@example.com'; - await notifications.supportEmailAddressInput.clear(); - await notifications.supportEmailAddressInput.fill(newEmail); + await notifications.supportEmailAddress.clear(); + await notifications.supportEmailAddress.fill(newEmail); // * Verify that set value is visible and matches text - await expect(notifications.supportEmailAddressInput).toHaveValue(newEmail); + await expect(notifications.supportEmailAddress.input).toHaveValue(newEmail); // # Save setting - await notifications.saveButton.click(); + await notifications.save(); // * Verify that the config is correctly saved in the server const {adminClient} = await pw.getAdminClient(); @@ -158,29 +157,29 @@ test('MM-41671 cannot save the notifications page if mandatory fields are missin // # Visit Notifications admin console page await systemConsolePage.goto(); await systemConsolePage.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Notifications'); + await systemConsolePage.sidebar.notifications.click(); // # Wait for Notifications section to load const notifications = systemConsolePage.notifications; await notifications.toBeVisible(); const tests = [ - {name: 'Support Email Address', fieldInput: notifications.supportEmailAddressInput}, - {name: 'Notification Display Name', fieldInput: notifications.notificationDisplayNameInput}, - {name: 'Notification From Address', fieldInput: notifications.notificationFromAddressInput}, + {name: 'Support Email Address', field: notifications.supportEmailAddress}, + {name: 'Notification Display Name', field: notifications.notificationDisplayName}, + {name: 'Notification From Address', field: notifications.notificationFromAddress}, ]; for (const testCase of tests) { // # Clear the field - await expect(testCase.fieldInput).toBeVisible(); - await testCase.fieldInput.clear(); + await testCase.field.toBeVisible(); + await testCase.field.clear(); // * Error message is shown and save button is disabled await expect(notifications.errorMessage).toHaveText(`"${testCase.name}" is required`); await expect(notifications.saveButton).toBeDisabled(); // # Insert something in the field - await testCase.fieldInput.fill('anything'); + await testCase.field.fill('anything'); // * Ensure no error message is shown and the save button is not disabled await expect(notifications.errorMessage).toHaveCount(0); diff --git a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts index b6aab4251b9..6f05ccde890 100644 --- a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts @@ -25,135 +25,140 @@ test('should be able to enable mobile security settings when licensed', async ({ await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); await systemConsolePage.mobileSecurity.toBeVisible(); // # Enable Biometric Authentication - await systemConsolePage.mobileSecurity.clickEnableBiometricAuthenticationToggleTrue(); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.selectTrue(); // * Verify only Biometric Authentication is enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(false); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeFalse(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeFalse(); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify Biometric Authentication is still enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(false); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeFalse(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeFalse(); // # Enable Prevent Screen Capture - await systemConsolePage.mobileSecurity.clickPreventScreenCaptureToggleTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.selectTrue(); // * Verify only Biometric Authentication and Prevent Screen Capture are enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeFalse(); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify Biometric Authentication and Prevent Screen Capture are still enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeFalse(); // # Enable Jailbreak Protection - await systemConsolePage.mobileSecurity.clickJailbreakProtectionToggleTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.selectTrue(); // * Verify all toggles are enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify all toggles are still enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); if (license.SkuShortName === 'advanced') { // # Enable Secure File Preview - await systemConsolePage.mobileSecurity.clickEnableSecureFilePreviewToggleTrue(); + await systemConsolePage.mobileSecurity.enableSecureFilePreviewMode.selectTrue(); // * Verify all toggles are enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); + await systemConsolePage.mobileSecurity.enableSecureFilePreviewMode.toBeTrue(); + await systemConsolePage.mobileSecurity.allowPdfLinkNavigation.toBeFalse(); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify all toggles are still enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(false); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); + await systemConsolePage.mobileSecurity.enableSecureFilePreviewMode.toBeTrue(); + await systemConsolePage.mobileSecurity.allowPdfLinkNavigation.toBeFalse(); // # Enable Allow PDF Link Navigation - await systemConsolePage.mobileSecurity.clickAllowPdfLinkNavigationToggleTrue(); + await systemConsolePage.mobileSecurity.allowPdfLinkNavigation.selectTrue(); // * Verify all toggles are enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); + await systemConsolePage.mobileSecurity.enableSecureFilePreviewMode.toBeTrue(); + await systemConsolePage.mobileSecurity.allowPdfLinkNavigation.toBeTrue(); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify all toggles are still enabled - expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableBiometricAuthentication.toBeTrue(); + await systemConsolePage.mobileSecurity.preventScreenCapture.toBeTrue(); + await systemConsolePage.mobileSecurity.enableJailbreakProtection.toBeTrue(); + await systemConsolePage.mobileSecurity.enableSecureFilePreviewMode.toBeTrue(); + await systemConsolePage.mobileSecurity.allowPdfLinkNavigation.toBeTrue(); } }); @@ -179,7 +184,7 @@ test('should show mobile security upsell when not licensed', async ({pw}) => { await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); await systemConsolePage.featureDiscovery.toBeVisible(); // * Verify title is correct @@ -213,38 +218,39 @@ test('should show and enable Intune MAM when Enterprise Advanced licensed and Of await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); // * Verify Intune MAM toggle is visible - await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue).toBeVisible(); - await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleFalse).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.trueOption).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.falseOption).toBeVisible(); // # Enable Intune MAM - await systemConsolePage.mobileSecurity.clickEnableIntuneMAMToggleTrue(); + await systemConsolePage.mobileSecurity.enableIntuneMAM.selectTrue(); // * Verify Intune MAM is enabled - await expect(await systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableIntuneMAM.toBeTrue(); - await systemConsolePage.mobileSecurity.selectIntuneAuthService('office365'); + await systemConsolePage.mobileSecurity.authProvider.select('office365'); // # Fill in Intune configuration - await systemConsolePage.mobileSecurity.fillIntuneTenantId('12345678-1234-1234-1234-123456789012'); - await systemConsolePage.mobileSecurity.fillIntuneClientId('87654321-4321-4321-4321-210987654321'); + await systemConsolePage.mobileSecurity.tenantId.fill('12345678-1234-1234-1234-123456789012'); + await systemConsolePage.mobileSecurity.clientId.fill('87654321-4321-4321-4321-210987654321'); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify Intune MAM is still enabled - await expect(await systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableIntuneMAM.toBeTrue(); }); test('should hide Intune MAM when Office365 is not configured', async ({pw}) => { @@ -271,12 +277,12 @@ test('should hide Intune MAM when Office365 is not configured', async ({pw}) => await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); // * Verify Intune MAM toggle is visible - await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleTrue).toBeVisible(); - await expect(systemConsolePage.mobileSecurity.enableIntuneMAMToggleFalse).toBeVisible(); - await expect(systemConsolePage.mobileSecurity.intuneAuthServiceDropdown).toBeDisabled(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.trueOption).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.falseOption).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.authProvider.dropdown).toBeDisabled(); }); test('should configure new IntuneSettings with Office365 auth provider', async ({pw}) => { @@ -307,45 +313,42 @@ test('should configure new IntuneSettings with Office365 auth provider', async ( await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); // * Verify new Intune toggle is visible - await expect(systemConsolePage.mobileSecurity.enableIntuneToggleTrue).toBeVisible(); - await expect(systemConsolePage.mobileSecurity.enableIntuneToggleFalse).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.trueOption).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.falseOption).toBeVisible(); // # Enable Intune - await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue(); + await systemConsolePage.mobileSecurity.enableIntuneMAM.selectTrue(); // * Verify Intune is enabled - expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true); + await systemConsolePage.mobileSecurity.enableIntuneMAM.toBeTrue(); // # Select Office365 as auth provider - await systemConsolePage.mobileSecurity.selectIntuneAuthService('office365'); + await systemConsolePage.mobileSecurity.authProvider.select('office365'); // # Fill in Intune configuration - await systemConsolePage.mobileSecurity.fillIntuneTenantId('12345678-1234-1234-1234-123456789012'); - await systemConsolePage.mobileSecurity.fillIntuneClientId('87654321-4321-4321-4321-210987654321'); + await systemConsolePage.mobileSecurity.tenantId.fill('12345678-1234-1234-1234-123456789012'); + await systemConsolePage.mobileSecurity.clientId.fill('87654321-4321-4321-4321-210987654321'); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify Intune is still enabled and configured - expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.inputValue()).toBe( - '12345678-1234-1234-1234-123456789012', - ); - expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.inputValue()).toBe( - '87654321-4321-4321-4321-210987654321', - ); + await systemConsolePage.mobileSecurity.enableIntuneMAM.toBeTrue(); + expect(await systemConsolePage.mobileSecurity.tenantId.getValue()).toBe('12345678-1234-1234-1234-123456789012'); + expect(await systemConsolePage.mobileSecurity.clientId.getValue()).toBe('87654321-4321-4321-4321-210987654321'); }); test('should configure new IntuneSettings with SAML auth provider', async ({pw}) => { @@ -365,7 +368,7 @@ test('should configure new IntuneSettings with SAML auth provider', async ({pw}) const serverUrl = process.env.MM_SERVER_URL || 'http://localhost:8065'; // # Upload a valid SAML IdP certificate using fetch - const idpCert = `-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKC1r6Qw3v6OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAlVTMRYwFAYDVQQIDA1Tb21lLVN0YXRlMRYwFAYDVQQKDA1FeGFtcGxlIEluYy4wHhcNMTkwMTAxMDAwMDAwWhcNMjkwMTAxMDAwMDAwWjBFMQswCQYDVQQGEwJVUzEWMBQGA1UECAwNU29tZS1TdGF0ZTEWMBQGA1UECgwNRXhhbXBsZSBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6QwIDAQABo1AwTjAdBgNVHQ4EFgQU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKQw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw=\n-----END CERTIFICATE-----\n`; + const idpCert = `-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKC1r6Qw3v6OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAlVTMRYwFAYDVQQIDA1Tb21lLVN0YXRlMRYwFAYDVQQKDA1FeGFtcGxlIEluYy4wHhcNMTkwMTAxMDAwMDAwWhcNMjkwMTAxMDAwMDAwWjBFMQswCQYDVQQGEwJVUzEWMBQGA1UECAwNU29tZS1TdGF0ZTEWMBQGA1UECgwNRXhhbXBsZSBJbmMuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6QwIDAQABo1AwTjAdBgNVHQ4EFgQU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAU6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKQw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw3v6OMC1r6Qw=\n-----END CERTIFICATE-----\n`; const idpFormData = new FormData(); idpFormData.append( 'certificate', @@ -413,41 +416,38 @@ test('should configure new IntuneSettings with SAML auth provider', async ({pw}) await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); // * Verify new Intune toggle is visible - await expect(systemConsolePage.mobileSecurity.enableIntuneToggleTrue).toBeVisible(); + await expect(systemConsolePage.mobileSecurity.enableIntuneMAM.trueOption).toBeVisible(); // # Enable Intune - await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue(); + await systemConsolePage.mobileSecurity.enableIntuneMAM.selectTrue(); // # Select SAML as auth provider - await systemConsolePage.mobileSecurity.selectIntuneAuthService('saml'); + await systemConsolePage.mobileSecurity.authProvider.select('saml'); // # Fill in Intune configuration - await systemConsolePage.mobileSecurity.fillIntuneTenantId('abcdef01-2345-6789-abcd-ef0123456789'); - await systemConsolePage.mobileSecurity.fillIntuneClientId('fedcba98-7654-3210-fedc-ba9876543210'); + await systemConsolePage.mobileSecurity.tenantId.fill('abcdef01-2345-6789-abcd-ef0123456789'); + await systemConsolePage.mobileSecurity.clientId.fill('fedcba98-7654-3210-fedc-ba9876543210'); // # Save settings - await systemConsolePage.mobileSecurity.clickSaveButton(); + await systemConsolePage.mobileSecurity.save(); // # Wait until the save button has settled await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save'); // # Go to any other section and come back to Mobile Security - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); + await systemConsolePage.mobileSecurity.toBeVisible(); // * Verify Intune is still enabled and configured with SAML - expect(await systemConsolePage.mobileSecurity.enableIntuneToggleTrue.isChecked()).toBe(true); - expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.inputValue()).toBe( - 'abcdef01-2345-6789-abcd-ef0123456789', - ); - expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.inputValue()).toBe( - 'fedcba98-7654-3210-fedc-ba9876543210', - ); + await systemConsolePage.mobileSecurity.enableIntuneMAM.toBeTrue(); + expect(await systemConsolePage.mobileSecurity.tenantId.getValue()).toBe('abcdef01-2345-6789-abcd-ef0123456789'); + expect(await systemConsolePage.mobileSecurity.clientId.getValue()).toBe('fedcba98-7654-3210-fedc-ba9876543210'); }); test('should disable Intune inputs when toggle is off', async ({pw}) => { @@ -477,18 +477,18 @@ test('should disable Intune inputs when toggle is off', async ({pw}) => { await systemConsolePage.toBeVisible(); // # Go to Mobile Security section - await systemConsolePage.sidebar.goToItem('Mobile Security'); + await systemConsolePage.sidebar.mobileSecurity.click(); // * Verify Intune inputs are disabled when toggle is off - expect(await systemConsolePage.mobileSecurity.intuneAuthServiceDropdown.isDisabled()).toBe(true); - expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.isDisabled()).toBe(true); - expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.isDisabled()).toBe(true); + expect(await systemConsolePage.mobileSecurity.authProvider.dropdown.isDisabled()).toBe(true); + expect(await systemConsolePage.mobileSecurity.tenantId.input.isDisabled()).toBe(true); + expect(await systemConsolePage.mobileSecurity.clientId.input.isDisabled()).toBe(true); // # Enable Intune - await systemConsolePage.mobileSecurity.clickEnableIntuneToggleTrue(); + await systemConsolePage.mobileSecurity.enableIntuneMAM.selectTrue(); // * Verify Intune inputs are now enabled - expect(await systemConsolePage.mobileSecurity.intuneAuthServiceDropdown.isDisabled()).toBe(false); - expect(await systemConsolePage.mobileSecurity.intuneTenantIdInput.isDisabled()).toBe(false); - expect(await systemConsolePage.mobileSecurity.intuneClientIdInput.isDisabled()).toBe(false); + expect(await systemConsolePage.mobileSecurity.authProvider.dropdown.isDisabled()).toBe(false); + expect(await systemConsolePage.mobileSecurity.tenantId.input.isDisabled()).toBe(false); + expect(await systemConsolePage.mobileSecurity.clientId.input.isDisabled()).toBe(false); }); diff --git a/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts b/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts index 67f1426737a..0c279c2124f 100644 --- a/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts @@ -3,81 +3,7 @@ import {UserProfile} from '@mattermost/types/users'; -import {expect, PlaywrightExtended, test} from '@mattermost/playwright-lib'; - -// setupSystemManagerRole configures the system manager with the given permission ("Can Edit", "Read only", "No access") -// for the given section and subsection (e.g. "permission_section_reporting_site_statistics" and "permission_section_reporting_team_statistics"). -// -// We do this via the system console and not the API because this page has multiple queries to build up the -// final API call that are all ultimately part of the spec, and we want to test the effects of those, not -// merely a hand-created version of the same. -const setupDefaultSystemManagerRole = async ( - pw: PlaywrightExtended, - adminUser: UserProfile, - sectionTestId: string, - subsectionTestId: string, - permissionText: string, -) => { - // Login as admin and navigate to System Console - const {systemConsolePage: adminConsolePage} = await pw.testBrowser.login(adminUser); - - // Go to System Console - await adminConsolePage.goto(); - - // Login to the System Console and navigate to Delegated Granular Administration - await adminConsolePage.sidebar.goToItem('Delegated Granular Administration'); - - // Find the System Manager row in the table - const systemManagerText = adminConsolePage.page.getByText('System Manager', {exact: true}).first(); - await expect(systemManagerText).toBeVisible(); - - // Click on the System Manager text to go to its settings page - await systemManagerText.click(); - - // Expand the section - const sectionReporting = adminConsolePage.page.locator(`data-testid=${sectionTestId}`); - const hideSubsectionsLink = sectionReporting.getByRole('button').filter({hasText: 'Hide'}).first(); - const showSubsectionsLink = sectionReporting.getByRole('button').filter({hasText: 'Show'}).first(); - - // Check which one is visible and click if needed - const isHideVisible = await hideSubsectionsLink.isVisible(); - const isShowVisible = !isHideVisible && (await showSubsectionsLink.isVisible()); - - if (isShowVisible) { - // Need to expand - await showSubsectionsLink.click(); - } - - // Get the whole row - const rowReporting = adminConsolePage.page.locator('.PermissionRow').filter({has: sectionReporting}); - await rowReporting.click(); - - // Find the sub section - const subsectionTeamStatistics = rowReporting.locator(`data-testid=${subsectionTestId}`); - - await subsectionTeamStatistics.click(); - - // Look for dropdown button - const dropdownButton = subsectionTeamStatistics.locator('button').first(); - - // Click the button to open the dropdown menu - await dropdownButton.click(); - - // Click on the desired option in the dropdown - const permissionOption = subsectionTeamStatistics.locator('.dropdown-menu').getByText(permissionText).first(); - await permissionOption.click(); - - // Click Save button - const saveButton = adminConsolePage.page.getByRole('button', {name: 'Save'}).first(); - await saveButton.click(); - - // Wait for save operation to complete - await adminConsolePage.page.waitForLoadState('networkidle'); - - // Go back to the main console - await adminConsolePage.goto(); - await adminConsolePage.page.waitForLoadState('networkidle'); -}; +import {expect, PlaywrightExtended, SystemConsolePage, test} from '@mattermost/playwright-lib'; test( 'MM-63378 System Manager without team access permissions cannot view team details', @@ -97,88 +23,38 @@ test( // Create another team of which the user is not a member. const otherTeam = await adminClient.createTeam(await pw.random.team()); - // Login as the user - const {systemConsolePage} = await pw.testBrowser.login(systemManagerUser); + // Configure the system manager with the default permissions (as admin). + await setupSystemManagerPermission(pw, adminUser, 'reporting', 'team_statistics', 'Can edit'); + await setupSystemManagerPermission(pw, adminUser, 'userManagement', 'teams', 'Can edit'); - // Configure the system manager with the default permissions. - await setupDefaultSystemManagerRole( - pw, - adminUser, - 'permission_section_reporting', - 'permission_section_reporting_team_statistics', - 'Can edit', - ); - await setupDefaultSystemManagerRole( - pw, - adminUser, - 'permission_section_user_management', - 'permission_section_user_management_teams', - 'Can edit', - ); + // Re-login as the system manager (the admin login above replaced the page context) + const {systemConsolePage} = await pw.testBrowser.login(systemManagerUser); // Verify the system manager has access to the site statistics for all teams await systemConsolePage.goto(); - // Navigate to Team Statistics - await systemConsolePage.sidebar.goToItem('Team Statistics'); + // Navigate to Team Statistics and verify access to user's team + await verifyTeamStatisticsAccess(systemConsolePage, team.id, team.display_name); - // Wait for page to fully load - await systemConsolePage.page.waitForLoadState('networkidle'); - - // Find the team filter dropdown - let teamFilterSelect = systemConsolePage.page.getByTestId('teamFilter'); - await expect(teamFilterSelect).toBeVisible(); - - // Select the team by value - await teamFilterSelect.selectOption({value: team.id}); - - // Verify the text shows "Team Statistics for " - let teamStatsHeading = systemConsolePage.page.getByText(`Team Statistics for ${team.display_name}`, { - exact: true, - }); - await expect(teamStatsHeading).toBeVisible(); - - // Select the other team by value - await teamFilterSelect.selectOption({value: otherTeam.id}); - - // Verify the text shows "Team Statistics for " - const otherTeamStatsHeading = systemConsolePage.page.getByText( - `Team Statistics for ${otherTeam.display_name}`, - { - exact: true, - }, - ); - await expect(otherTeamStatsHeading).toBeVisible(); + // Select the other team by value and verify access + await systemConsolePage.teamStatistics.selectTeamById(otherTeam.id); + await systemConsolePage.teamStatistics.toHaveTeamHeader(otherTeam.display_name); // Verify the user has API access to the otherTeam. const fetchedOtherTeam = await systemManagerClient.getTeam(otherTeam.id); expect(fetchedOtherTeam.id).toEqual(otherTeam.id); // Configure the system manager without access to team user management - await setupDefaultSystemManagerRole( - pw, - adminUser, - 'permission_section_user_management', - 'permission_section_user_management_teams', - 'No access', - ); + await setupSystemManagerPermission(pw, adminUser, 'userManagement', 'teams', 'No access'); + + // Re-login as the system manager again after permission change + const {systemConsolePage: systemConsolePage2} = await pw.testBrowser.login(systemManagerUser); // Verify the system manager only has access to the site statistics for the team they belong to - await systemConsolePage.goto(); + await systemConsolePage2.goto(); - // Navigate to Team Statistics - await systemConsolePage.sidebar.goToItem('Team Statistics'); - - // Find the team filter dropdown - teamFilterSelect = systemConsolePage.page.getByTestId('teamFilter'); - await expect(teamFilterSelect).toBeVisible(); - - // Select the team by value - await teamFilterSelect.selectOption({value: team.id}); - - // Verify the text shows "Team Statistics for " - teamStatsHeading = systemConsolePage.page.getByText(`Team Statistics for ${team.display_name}`, {exact: true}); - await expect(teamStatsHeading).toBeVisible(); + // Navigate to Team Statistics and verify access to user's team + await verifyTeamStatisticsAccess(systemConsolePage2, team.id, team.display_name); // Verify the user has no API access to the otherTeam. let apiError: Error | null = null; @@ -191,3 +67,69 @@ test( expect(apiError?.message).toContain('You do not have the appropriate permissions'); }, ); + +// Helper function to navigate to Team Statistics and verify team access +const verifyTeamStatisticsAccess = async ( + systemConsolePage: SystemConsolePage, + teamId: string, + teamDisplayName: string, +) => { + // Navigate to Team Statistics + await systemConsolePage.sidebar.reporting.teamStatistics.click(); + await systemConsolePage.teamStatistics.toBeVisible(); + + // Select the team by value + await systemConsolePage.teamStatistics.selectTeamById(teamId); + + // Verify the text shows "Team Statistics for " + await systemConsolePage.teamStatistics.toHaveTeamHeader(teamDisplayName); +}; + +type PermissionValue = 'Can edit' | 'Read only' | 'No access'; + +// setupSystemManagerRole configures the system manager with the given permission ("Can edit", "Read only", "No access") +// for the given section and subsection. +// +// We do this via the system console and not the API because this page has multiple queries to build up the +// final API call that are all ultimately part of the spec, and we want to test the effects of those, not +// merely a hand-created version of the same. +const setupSystemManagerPermission = async ( + pw: PlaywrightExtended, + adminUser: UserProfile, + sectionName: 'reporting' | 'userManagement', + subsectionName: string, + permission: PermissionValue, +) => { + // Login as admin and navigate to System Console + const {systemConsolePage} = await pw.testBrowser.login(adminUser); + + // Go to System Console + await systemConsolePage.goto(); + + // Navigate to Delegated Granular Administration + await systemConsolePage.sidebar.delegatedGranularAdministration.click(); + await systemConsolePage.delegatedGranularAdministration.toBeVisible(); + + // Click on the System Manager row to go to its settings page + await systemConsolePage.delegatedGranularAdministration.adminRolesPanel.systemManager.clickEdit(); + await systemConsolePage.delegatedGranularAdministration.systemRoles.toBeVisible(); + + // Get the section from privileges panel + const section = systemConsolePage.delegatedGranularAdministration.systemRoles.privilegesPanel[sectionName]; + + // Expand subsections if needed + await section.expandSubsections(); + + // Get the subsection and set permission + const subsection = section.getSubsection(subsectionName); + await subsection.setPermission(permission); + + // Save + await systemConsolePage.delegatedGranularAdministration.systemRoles.save(); + + // Wait for save to complete - successful save redirects to system_roles list + await systemConsolePage.page.waitForURL('**/admin_console/user_management/system_roles'); + + // Wait for the page to fully load + await systemConsolePage.page.waitForLoadState('networkidle'); +}; diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts index de11901b08a..9b2d7b79948 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts @@ -3,10 +3,153 @@ import {type PlaywrightExtended, expect, test} from '@mattermost/playwright-lib'; +test('MM-T5520-1 should activate and deactivate users', async ({pw}) => { + const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and deactivate the user + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickDeactivate(); + + // # Press confirm on the modal + await systemConsolePage.users.confirmModal.confirm(); + + // * Verify user is deactivated + await expect(userRow.container.getByText('Deactivated')).toBeVisible(); + expect((await getUser()).delete_at).toBeGreaterThan(0); + + // # Open menu and reactivate the user + const actionMenu2 = await userRow.openActionMenu(); + await actionMenu2.clickActivate(); + + // * Verify user is activated + await expect(userRow.container.getByText('Member')).toBeVisible(); +}); + +test('MM-T5520-2 should change user roles', async ({pw}) => { + const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and click Manage roles + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickManageRoles(); + + // # Change to System Admin and click Save + const systemAdmin = systemConsolePage.page.locator('input[name="systemadmin"]'); + await systemAdmin.waitFor(); + await systemAdmin.click(); + await systemConsolePage.users.manageRolesModal.save(); + + // * Verify that the role was updated + await expect(userRow.container.getByText('System Admin')).toBeVisible(); + expect((await getUser()).roles).toContain('system_admin'); + + // # Open menu and click Manage roles + const actionMenu2 = await userRow.openActionMenu(); + await actionMenu2.clickManageRoles(); + + // # Change to Member and click Save + const systemMember = systemConsolePage.page.locator('input[name="systemmember"]'); + await systemMember.waitFor(); + await systemMember.click(); + await systemConsolePage.users.manageRolesModal.save(); + + // * Verify that the role was updated + await expect(userRow.container.getByText('Member')).toBeVisible(); + expect((await getUser()).roles).toContain('system_user'); +}); + +test('MM-T5520-3 should be able to manage teams', async ({pw}) => { + const {systemConsolePage} = await setupAndGetRandomUser(pw); + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and click Manage teams + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickManageTeams(); + + // # Click Make Team Admin + const team = systemConsolePage.page.locator('div.manage-teams__team'); + const teamDropdown = team.locator('div.MenuWrapper'); + await teamDropdown.click(); + const makeTeamAdmin = teamDropdown.getByText('Make Team Admin'); + await makeTeamAdmin.click(); + + // * Verify role is updated + await expect(team.getByText('Team Admin')).toBeVisible(); + + // # Change back to Team Member + await teamDropdown.click(); + const makeTeamMember = teamDropdown.getByText('Make Team Member'); + await makeTeamMember.click(); + + // * Verify role is updated + await expect(team.getByText('Team Member')).toBeVisible(); + + // # Click Remove From Team + await teamDropdown.click(); + const removeFromTeam = teamDropdown.getByText('Remove From Team'); + await removeFromTeam.click(); + + // * The team should be detached + await team.waitFor({state: 'detached'}); + await expect(team).not.toBeVisible(); +}); + +test('MM-T5520-4 should reset the users password', async ({pw}) => { + const {systemConsolePage} = await setupAndGetRandomUser(pw); + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and click Reset Password + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickResetPassword(); + + // # Enter a random password and click Reset + await systemConsolePage.users.resetPasswordModal.fillPassword(await pw.random.id()); + await systemConsolePage.users.resetPasswordModal.reset(); +}); + +test('MM-T5520-5 should change the users email', async ({pw}) => { + const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); + const newEmail = `${await pw.random.id()}@example.com`; + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and click Update Email + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickUpdateEmail(); + + // # Enter new email and click Update + await systemConsolePage.users.updateEmailModal.fillEmail(newEmail); + await systemConsolePage.users.updateEmailModal.update(); + + // * Verify that the email updated + await expect(userRow.container.getByText(newEmail)).toBeVisible(); + expect((await getUser()).email).toEqual(newEmail); +}); + +test('MM-T5520-6 should revoke sessions', async ({pw}) => { + const {systemConsolePage} = await setupAndGetRandomUser(pw); + + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + + // # Open menu and revoke sessions + const actionMenu = await userRow.openActionMenu(); + await actionMenu.clickRevokeSessions(); + + // # Press confirm on the modal + await systemConsolePage.users.confirmModal.confirm(); + + // * Verify no error is displayed + await expect(userRow.container.locator('.error')).not.toBeVisible(); +}); + /** * Setup a new random user, and search for it such that it's the first row in the list * @param pw - * @param pages * @returns A function to get the refreshed user, and the System Console page for navigation */ async function setupAndGetRandomUser(pw: PlaywrightExtended) { @@ -19,7 +162,7 @@ async function setupAndGetRandomUser(pw: PlaywrightExtended) { // # Log in as admin const {systemConsolePage} = await pw.testBrowser.login(adminUser); - // # Create a random user to edit for + // # Create a random user to edit const user = await adminClient.createUser(await pw.random.user(), '', ''); const team = await adminClient.createTeam(await pw.random.team()); await adminClient.addToTeam(team.id, user.id); @@ -29,174 +172,15 @@ async function setupAndGetRandomUser(pw: PlaywrightExtended) { await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - // # Search for user-1 - await systemConsolePage.systemUsers.enterSearchText(user.email); - const userRow = await systemConsolePage.systemUsers.getNthRow(1); - await userRow.getByText(user.email).waitFor(); - const innerText = await userRow.innerText(); - expect(innerText).toContain(user.email); + // # Search for the user + await systemConsolePage.users.searchUsers(user.email); + + // Wait for search results + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + await expect(userRow.container.getByText(user.email)).toBeVisible(); return {getUser: () => adminClient.getUser(user.id), systemConsolePage}; } - -test('MM-T5520-1 should activate and deactivate users', async ({pw}) => { - const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); - - // # Open menu and deactivate the user - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const deactivate = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Deactivate'); - await deactivate.click(); - - // # Press confirm on the modal - await systemConsolePage.confirmModal.confirm(); - - // * Verify user is deactivated - const firstRow = await systemConsolePage.systemUsers.getNthRow(1); - await firstRow.getByText('Deactivated').waitFor(); - expect(await firstRow.innerText()).toContain('Deactivated'); - expect((await getUser()).delete_at).toBeGreaterThan(0); - - // # Open menu and reactivate the user - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const activate = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Activate'); - await activate.click(); - - // * Verify user is activated - await firstRow.getByText('Member').waitFor(); - expect(await firstRow.innerText()).toContain('Member'); -}); - -test('MM-T5520-2 should change user roles', async ({pw}) => { - const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); - - // # Open menu and click Manage roles - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - let manageRoles = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Manage roles'); - await manageRoles.click(); - - // # Change to System Admin and click Save - const systemAdmin = systemConsolePage.page.locator('input[name="systemadmin"]'); - await systemAdmin.waitFor(); - await systemAdmin.click(); - systemConsolePage.saveRoleChange(); - - // * Verify that the modal closed and no error showed - await systemAdmin.waitFor({state: 'detached'}); - - // * Verify that the role was updated - const firstRow = await systemConsolePage.systemUsers.getNthRow(1); - expect(await firstRow.innerText()).toContain('System Admin'); - expect((await getUser()).roles).toContain('system_admin'); - - // # Open menu and click Manage roles - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - manageRoles = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Manage roles'); - await manageRoles.click(); - - // # Change to Member and click Save - const systemMember = systemConsolePage.page.locator('input[name="systemmember"]'); - await systemMember.waitFor(); - await systemMember.click(); - await systemConsolePage.saveRoleChange(); - - // * Verify that the modal closed and no error showed - await systemMember.waitFor({state: 'detached'}); - - // * Verify that the role was updated - expect(await firstRow.innerText()).toContain('Member'); - expect((await getUser()).roles).toContain('system_user'); -}); - -test('MM-T5520-3 should be able to manage teams', async ({pw}) => { - const {systemConsolePage} = await setupAndGetRandomUser(pw); - - // # Open menu and click Manage teams - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const manageTeams = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Manage teams'); - await manageTeams.click(); - - // # Click Make Team Admin - const team = systemConsolePage.page.locator('div.manage-teams__team'); - const teamDropdown = team.locator('div.MenuWrapper'); - await teamDropdown.click(); - const makeTeamAdmin = teamDropdown.getByText('Make Team Admin'); - await makeTeamAdmin.click(); - - // * Verify role is updated - expect(await team.innerText()).toContain('Team Admin'); - - // # Change back to Team Member - await teamDropdown.click(); - const makeTeamMember = teamDropdown.getByText('Make Team Member'); - await makeTeamMember.click(); - - // * Verify role is updated - expect(await team.innerText()).toContain('Team Member'); - - // # Click Remove From Team - await teamDropdown.click(); - const removeFromTeam = teamDropdown.getByText('Remove From Team'); - await removeFromTeam.click(); - - // * The team should be detached - await team.waitFor({state: 'detached'}); - expect(team).not.toBeVisible(); -}); - -test('MM-T5520-4 should reset the users password', async ({pw}) => { - const {systemConsolePage} = await setupAndGetRandomUser(pw); - - // # Open menu and click Reset Password - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const resetPassword = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Reset password'); - await resetPassword.click(); - - // # Enter a random password and click Save - const passwordInput = systemConsolePage.page.locator('input[type="password"]'); - await passwordInput.fill(await pw.random.id()); - await systemConsolePage.clickResetButton(); - - // * Verify that the modal closed and no error showed - await passwordInput.waitFor({state: 'detached'}); -}); - -test('MM-T5520-5 should change the users email', async ({pw}) => { - const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); - const newEmail = `${await pw.random.id()}@example.com`; - - // # Open menu and click Update Email - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const updateEmail = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Update email'); - await updateEmail.click(); - - // # Enter new email and click Update - const emailInput = systemConsolePage.page.locator('input[type="email"]'); - await emailInput.fill(newEmail); - await systemConsolePage.clickUpdateEmailButton(); - - // * Verify that the modal closed - await emailInput.waitFor({state: 'detached'}); - - // * Verify that the email updated - const firstRow = await systemConsolePage.systemUsers.getNthRow(1); - expect(await firstRow.innerText()).toContain(newEmail); - expect((await getUser()).email).toEqual(newEmail); -}); - -test('MM-T5520-6 should revoke sessions', async ({pw}) => { - const {systemConsolePage} = await setupAndGetRandomUser(pw); - - // # Open menu and revoke sessions - await systemConsolePage.systemUsers.actionMenuButtons[0].click(); - const removeSessions = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Revoke sessions'); - await removeSessions.click(); - - // # Press confirm on the modal - await systemConsolePage.confirmModal.confirm(); - - const firstRow = await systemConsolePage.systemUsers.getNthRow(1); - expect(await firstRow.innerHTML()).not.toContain('class="error"'); -}); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts index 29b7b207cbf..bbb7ddeedf4 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts @@ -23,28 +23,36 @@ test('MM-T5523-1 Sortable columns should sort the list when clicked', async ({pw await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // * Verify that 'Email' column has aria-sort attribute - const userDetailsColumnHeader = await systemConsolePage.systemUsers.getColumnHeader('Email'); - expect(await userDetailsColumnHeader.isVisible()).toBe(true); - expect(userDetailsColumnHeader).toHaveAttribute('aria-sort'); + const emailColumnHeader = systemConsolePage.users.usersTable.getColumnHeader('Email'); + await expect(emailColumnHeader).toBeVisible(); + await expect(emailColumnHeader).toHaveAttribute('aria-sort'); - // # Store the first row's email before sorting - const firstRowWithoutSort = await systemConsolePage.systemUsers.getNthRow(1); - const firstRowEmailWithoutSort = await firstRowWithoutSort.getByText(pw.simpleEmailRe).allInnerTexts(); + // # Store all emails before sorting to compare order + const rowsBeforeSort = await systemConsolePage.users.usersTable.bodyRows.count(); + const emailsBeforeSort: string[] = []; + for (let i = 0; i < rowsBeforeSort; i++) { + const row = systemConsolePage.users.usersTable.getRowByIndex(i); + const email = await row.getEmail(); + emailsBeforeSort.push(email); + } - // # Click on the 'Email' column header to sort - await systemConsolePage.systemUsers.clickSortOnColumn('Email'); - await systemConsolePage.systemUsers.isLoadingComplete(); + // # Click on the 'Email' column header to sort and wait for sort to complete + await systemConsolePage.users.usersTable.sortByColumn('Email'); - // # Store the first row's email after sorting - const firstRowWithSort = await systemConsolePage.systemUsers.getNthRow(1); - const firstRowEmailWithSort = await firstRowWithSort.getByText(pw.simpleEmailRe).allInnerTexts(); + // # Store all emails after sorting + const emailsAfterSort: string[] = []; + for (let i = 0; i < rowsBeforeSort; i++) { + const row = systemConsolePage.users.usersTable.getRowByIndex(i); + const email = await row.getEmail(); + emailsAfterSort.push(email); + } - // * Verify that the first row is now different - expect(firstRowEmailWithoutSort).not.toBe(firstRowEmailWithSort); + // * Verify that the order has changed (emails array is different) + expect(emailsBeforeSort).not.toEqual(emailsAfterSort); }); test('MM-T5523-2 Non sortable columns should not sort the list when clicked', async ({pw}) => { @@ -67,24 +75,24 @@ test('MM-T5523-2 Non sortable columns should not sort the list when clicked', as await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // * Verify that 'Last login' column does not have aria-sort attribute - const userDetailsColumnHeader = await systemConsolePage.systemUsers.getColumnHeader('Last login'); - expect(await userDetailsColumnHeader.isVisible()).toBe(true); - expect(userDetailsColumnHeader).not.toHaveAttribute('aria-sort'); + const lastLoginColumnHeader = systemConsolePage.users.usersTable.getColumnHeader('Last login'); + await expect(lastLoginColumnHeader).toBeVisible(); + await expect(lastLoginColumnHeader).not.toHaveAttribute('aria-sort'); // # Store the first row's email without sorting - const firstRowWithoutSort = await systemConsolePage.systemUsers.getNthRow(1); - const firstRowEmailWithoutSort = await firstRowWithoutSort.getByText(pw.simpleEmailRe).allInnerTexts(); + const firstRowWithoutSort = systemConsolePage.users.usersTable.getRowByIndex(0); + const firstRowEmailWithoutSort = await firstRowWithoutSort.container.getByText(pw.simpleEmailRe).allInnerTexts(); // # Try to click on the 'Last login' column header to sort - await systemConsolePage.systemUsers.clickSortOnColumn('Last login'); + await systemConsolePage.users.usersTable.clickSortOnColumn('Last login'); // # Store the first row's email after sorting - const firstRowWithSort = await systemConsolePage.systemUsers.getNthRow(1); - const firstRowEmailWithSort = await firstRowWithSort.getByText(pw.simpleEmailRe).allInnerTexts(); + const firstRowWithSort = systemConsolePage.users.usersTable.getRowByIndex(0); + const firstRowEmailWithSort = await firstRowWithSort.container.getByText(pw.simpleEmailRe).allInnerTexts(); // * Verify that the first row's email is still the same expect(firstRowEmailWithoutSort).toEqual(firstRowEmailWithSort); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/column_toggler.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/column_toggler.spec.ts index 853e04f58ec..2d744ed0de1 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/column_toggler.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/column_toggler.spec.ts @@ -18,15 +18,14 @@ test('MM-T5523-3 Should list the column names with checkboxes in the correct ord await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the column toggle menu - await systemConsolePage.systemUsers.openColumnToggleMenu(); - await systemConsolePage.systemUsersColumnToggleMenu.toBeVisible(); + const columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu(); // # Get all the menu items - const menuItems = await systemConsolePage.systemUsersColumnToggleMenu.getAllMenuItems(); + const menuItems = columnToggleMenu.getAllMenuItems(); const menuItemsTexts = await menuItems.allInnerTexts(); // * Verify menu items exists in the correct order @@ -59,23 +58,22 @@ test('MM-T5523-4 Should allow certain columns to be checked and others to be dis await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the column toggle menu - await systemConsolePage.systemUsers.openColumnToggleMenu(); - await systemConsolePage.systemUsersColumnToggleMenu.toBeVisible(); + const columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu(); // * Verify that 'Display Name' is disabled - const displayNameMenuItem = await systemConsolePage.systemUsersColumnToggleMenu.getMenuItem('User details'); + const displayNameMenuItem = await columnToggleMenu.getMenuItem('User details'); expect(displayNameMenuItem).toBeDisabled(); // * Verify that 'Actions' is disabled - const actionsMenuItem = await systemConsolePage.systemUsersColumnToggleMenu.getMenuItem('Actions'); + const actionsMenuItem = await columnToggleMenu.getMenuItem('Actions'); expect(actionsMenuItem).toBeDisabled(); // * Verify that 'Email' however is enabled - const emailMenuItem = await systemConsolePage.systemUsersColumnToggleMenu.getMenuItem('Email'); + const emailMenuItem = await columnToggleMenu.getMenuItem('Email'); expect(emailMenuItem).not.toBeDisabled(); }); @@ -94,36 +92,35 @@ test('MM-T5523-5 Should show/hide the columns which are toggled on/off', async ( await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the column toggle menu - await systemConsolePage.systemUsers.openColumnToggleMenu(); - await systemConsolePage.systemUsersColumnToggleMenu.toBeVisible(); + let columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu(); // # Uncheck the Email and Last login columns to hide them - await systemConsolePage.systemUsersColumnToggleMenu.clickMenuItem('Email'); - await systemConsolePage.systemUsersColumnToggleMenu.clickMenuItem('Last login'); + await columnToggleMenu.clickMenuItem('Email'); + await columnToggleMenu.clickMenuItem('Last login'); // * Close the column toggle menu - await systemConsolePage.systemUsersColumnToggleMenu.close(); + await columnToggleMenu.close(); // * Verify that Email column and Last login column are hidden - expect(await systemConsolePage.systemUsers.doesColumnExist('Email')).toBe(false); - expect(await systemConsolePage.systemUsers.doesColumnExist('Last login')).toBe(false); + await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Email'})).not.toBeVisible(); + await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Last login'})).not.toBeVisible(); // # Now open the column toggle menu again - await systemConsolePage.systemUsers.openColumnToggleMenu(); + columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu(); // # Check the Email column to show it - await systemConsolePage.systemUsersColumnToggleMenu.clickMenuItem('Email'); + await columnToggleMenu.clickMenuItem('Email'); // * Close the column toggle menu - await systemConsolePage.systemUsersColumnToggleMenu.close(); + await columnToggleMenu.close(); // * Verify that Email column is now shown - expect(await systemConsolePage.systemUsers.doesColumnExist('Email')).toBe(true); + await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Email'})).toBeVisible(); // * Verify that however Last login column is still hidden as we did not check it on - expect(await systemConsolePage.systemUsers.doesColumnExist('Last login')).toBe(false); + await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Last login'})).not.toBeVisible(); }); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts index 298a8c61dfe..6a751345a5d 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts @@ -22,28 +22,28 @@ test.fixme('MM-T5522 Should begin export of data when export button is pressed', await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); - // # Change the export pw.duration to 30 days - await systemConsolePage.systemUsers.dateRangeSelectorMenuButton.click(); - await systemConsolePage.systemUsersDateRangeMenu.clickMenuItem('All time'); + // # Change the export duration to All time + const dateRangeMenu = await systemConsolePage.users.openDateRangeSelectorMenu(); + await dateRangeMenu.clickMenuItem('All time'); // # Click Export button and confirm the modal - await systemConsolePage.systemUsers.exportButton.click(); - await systemConsolePage.exportModal.confirm(); + await systemConsolePage.users.clickExport(); + await systemConsolePage.users.confirmModal.confirm(); - // # Change the export pw.duration to all time - await systemConsolePage.systemUsers.dateRangeSelectorMenuButton.click(); - await systemConsolePage.systemUsersDateRangeMenu.clickMenuItem('Last 30 days'); + // # Change the export duration to Last 30 days + const dateRangeMenu2 = await systemConsolePage.users.openDateRangeSelectorMenu(); + await dateRangeMenu2.clickMenuItem('Last 30 days'); // # Click Export button and confirm the modal - await systemConsolePage.systemUsers.exportButton.click(); - await systemConsolePage.exportModal.confirm(); + await systemConsolePage.users.clickExport(); + await systemConsolePage.users.confirmModal.confirm(); // # Click Export again button and confirm the modal - await systemConsolePage.systemUsers.exportButton.click(); - await systemConsolePage.exportModal.confirm(); + await systemConsolePage.users.clickExport(); + await systemConsolePage.users.confirmModal.confirm(); // * Verify that we are told that one is already running expect(page.getByText('Export is in progress')).toBeVisible(); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts index c7801715233..bc84c31b227 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {test} from '@mattermost/playwright-lib'; +import {expect, test} from '@mattermost/playwright-lib'; test('MM-T5521-7 Should be able to filter users with team filter', async ({pw}) => { const {adminUser, adminClient} = await pw.initSetup(); @@ -28,27 +28,26 @@ test('MM-T5521-7 Should be able to filter users with team filter', async ({pw}) await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the filter's popover - await systemConsolePage.systemUsers.openFilterPopover(); - await systemConsolePage.systemUsersFilterPopover.toBeVisible(); + const filterPopover = await systemConsolePage.users.openFilterPopover(); // # Enter the team name of the first user and select it - await systemConsolePage.systemUsersFilterPopover.searchInTeamMenu(team1.display_name); - await systemConsolePage.systemUsersFilterPopover.teamMenuInput.press('Enter'); + await filterPopover.searchInTeamMenu(team1.display_name); + await filterPopover.teamMenuInput.press('Enter'); // # Save the filter and close the popover - await systemConsolePage.systemUsersFilterPopover.save(); - await systemConsolePage.systemUsersFilterPopover.close(); - await systemConsolePage.systemUsers.isLoadingComplete(); + await filterPopover.save(); + await filterPopover.close(); + await systemConsolePage.users.isLoadingComplete(); // * Verify that the user corresponding to the first team is visible as team-1 filter was applied - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the user corresponding to the second team is not visible as team-2 filter was not applied - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-8 Should be able to filter users with role filter', async ({pw}) => { @@ -73,37 +72,35 @@ test('MM-T5521-8 Should be able to filter users with role filter', async ({pw}) await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the filter popover - await systemConsolePage.systemUsers.openFilterPopover(); - await systemConsolePage.systemUsersFilterPopover.toBeVisible(); + const filterPopover = await systemConsolePage.users.openFilterPopover(); - // # Open the role filter in the popover - await systemConsolePage.systemUsersFilterPopover.openRoleMenu(); - await systemConsolePage.systemUsersRoleMenu.toBeVisible(); - - // # Select the Guest role from the role filter - await systemConsolePage.systemUsersRoleMenu.clickMenuItem('Guest'); - await systemConsolePage.systemUsersRoleMenu.close(); + // # Open the role filter in the popover and select Guest + await filterPopover.openRoleMenu(); + // Wait for dropdown options and click on Guest + const guestOption = systemConsolePage.page.getByText('Guest', {exact: true}); + await guestOption.waitFor(); + await guestOption.click(); // # Save the filter and close the popover - await systemConsolePage.systemUsersFilterPopover.save(); - await systemConsolePage.systemUsersFilterPopover.close(); - await systemConsolePage.systemUsers.isLoadingComplete(); + await filterPopover.save(); + await filterPopover.close(); + await systemConsolePage.users.isLoadingComplete(); // # Search for the guest user with the filter already applied - await systemConsolePage.systemUsers.enterSearchText(guestUser.email); + await systemConsolePage.users.searchUsers(guestUser.email); // * Verify that guest user is visible as a 'Guest' role filter was applied - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(guestUser.email); + await expect(systemConsolePage.users.container.getByText(guestUser.email)).toBeVisible(); // # Search for the regular user with the filter already applied - await systemConsolePage.systemUsers.enterSearchText(regularUser.email); + await systemConsolePage.users.searchUsers(regularUser.email); // * Verify that regular user is not visible as 'Guest' role filter was applied - await systemConsolePage.systemUsers.verifyRowWithTextIsFound('No data'); + await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible(); }); test('MM-T5521-9 Should be able to filter users with status filter', async ({pw}) => { @@ -128,35 +125,32 @@ test('MM-T5521-9 Should be able to filter users with status filter', async ({pw} await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Open the filter popover - await systemConsolePage.systemUsers.openFilterPopover(); - await systemConsolePage.systemUsersFilterPopover.toBeVisible(); + const filterPopover = await systemConsolePage.users.openFilterPopover(); - // # Open the status filter in the popover - await systemConsolePage.systemUsersFilterPopover.openStatusMenu(); - await systemConsolePage.systemUsersStatusMenu.toBeVisible(); - await systemConsolePage.systemUsers.isLoadingComplete(); - - // # Select the Deactivated users from the status filter - await systemConsolePage.systemUsersStatusMenu.clickMenuItem('Deactivated users'); - await systemConsolePage.systemUsersStatusMenu.close(); + // # Open the status filter in the popover and select Deactivated users + await filterPopover.openStatusMenu(); + // Wait for dropdown options and click on Deactivated users + const deactivatedOption = systemConsolePage.page.getByText('Deactivated users', {exact: true}); + await deactivatedOption.waitFor(); + await deactivatedOption.click(); // # Save the filter and close the popover - await systemConsolePage.systemUsersFilterPopover.save(); - await systemConsolePage.systemUsersFilterPopover.close(); + await filterPopover.save(); + await filterPopover.close(); // # Search for the deactivated user with the filter already applied - await systemConsolePage.systemUsers.enterSearchText(deactivatedUser.email); + await systemConsolePage.users.searchUsers(deactivatedUser.email); // * Verify that deactivated user is visible as a 'Deactivated' status filter was applied - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(deactivatedUser.email); + await expect(systemConsolePage.users.container.getByText(deactivatedUser.email)).toBeVisible(); // # Search for the regular user with the filter already applied - await systemConsolePage.systemUsers.enterSearchText(regularUser.email); + await systemConsolePage.users.searchUsers(regularUser.email); // * Verify that regular user is not visible as 'Deactivated' status filter was applied - await systemConsolePage.systemUsers.verifyRowWithTextIsFound('No data'); + await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible(); }); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts index 62896f760ef..6349df131e8 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {test} from '@mattermost/playwright-lib'; +import {expect, test} from '@mattermost/playwright-lib'; test('MM-T5521-1 Should be able to search users with their first names', async ({pw}) => { const {adminUser, adminClient} = await pw.initSetup(); @@ -22,17 +22,17 @@ test('MM-T5521-1 Should be able to search users with their first names', async ( await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Enter the 'First Name' of the first user in the search box - await systemConsolePage.systemUsers.enterSearchText(user1.first_name); + await systemConsolePage.users.searchUsers(user1.first_name); // * Verify that the searched user i.e first user is found in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the second user doesnt appear in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-2 Should be able to search users with their last names', async ({pw}) => { @@ -54,17 +54,17 @@ test('MM-T5521-2 Should be able to search users with their last names', async ({ await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Enter the 'Last Name' of the user in the search box - await systemConsolePage.systemUsers.enterSearchText(user1.last_name); + await systemConsolePage.users.searchUsers(user1.last_name); // * Verify that the searched user i.e first user is found in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the second user doesnt appear in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-3 Should be able to search users with their emails', async ({pw}) => { @@ -86,17 +86,17 @@ test('MM-T5521-3 Should be able to search users with their emails', async ({pw}) await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // * Enter the 'Email' of the first user in the search box - await systemConsolePage.systemUsers.enterSearchText(user1.email); + await systemConsolePage.users.searchUsers(user1.email); // * Verify that the searched user i.e first user is found in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the second user doesnt appear in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-4 Should be able to search users with their usernames', async ({pw}) => { @@ -118,17 +118,17 @@ test('MM-T5521-4 Should be able to search users with their usernames', async ({p await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // # Enter the 'Username' of the first user in the search box - await systemConsolePage.systemUsers.enterSearchText(user1.username); + await systemConsolePage.users.searchUsers(user1.username); // * Verify that the searched user i.e first user is found in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the another user is not visible - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-5 Should be able to search users with their nick names', async ({pw}) => { @@ -150,16 +150,16 @@ test('MM-T5521-5 Should be able to search users with their nick names', async ({ await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); + await systemConsolePage.sidebar.users.click(); // # Enter the 'Nickname' of the first user in the search box - await systemConsolePage.systemUsers.enterSearchText(user1.nickname); + await systemConsolePage.users.searchUsers(user1.nickname); // * Verify that the searched user i.e first user is found in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsFound(user1.email); + await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible(); // * Verify that the second user doesnt appear in the list - await systemConsolePage.systemUsers.verifyRowWithTextIsNotFound(user2.email); + await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible(); }); test('MM-T5521-6 Should show no user is found when user doesnt exists', async ({pw}) => { @@ -177,10 +177,10 @@ test('MM-T5521-6 Should show no user is found when user doesnt exists', async ({ await systemConsolePage.toBeVisible(); // # Go to Users section - await systemConsolePage.sidebar.goToItem('Users'); + await systemConsolePage.sidebar.users.click(); // # Enter random text in the search box - await systemConsolePage.systemUsers.enterSearchText(`!${pw.random.id(15)}_^^^_${pw.random.id(15)}!`); + await systemConsolePage.users.searchUsers(`!${pw.random.id(15)}_^^^_${pw.random.id(15)}!`); - await systemConsolePage.systemUsers.verifyRowWithTextIsFound('No data'); + await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible(); }); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/user_attributes_admin_editing.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/user_attributes_admin_editing.spec.ts index 578d5dcc7a9..4f68df88f82 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/user_attributes_admin_editing.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/user_attributes_admin_editing.spec.ts @@ -18,7 +18,7 @@ import {UserProfile} from '@mattermost/types/users'; import {Client4} from '@mattermost/client'; import {UserPropertyField} from '@mattermost/types/properties'; -import {expect, test} from '@mattermost/playwright-lib'; +import {expect, test, SystemConsolePage} from '@mattermost/playwright-lib'; import { CustomProfileAttribute, @@ -87,7 +87,7 @@ let adminUser: UserProfile; let testUser: UserProfile; let attributeFieldsMap: Record; let adminClient: Client4; -let systemConsolePage: any; +let systemConsolePage: SystemConsolePage; test.describe('System Console - Admin User Profile Editing', () => { test.beforeEach(async ({pw}) => { @@ -119,13 +119,13 @@ test.describe('System Console - Admin User Profile Editing', () => { // Navigate to system console users await systemConsolePage.goto(); await systemConsolePage.toBeVisible(); - await systemConsolePage.sidebar.goToItem('Users'); - await systemConsolePage.systemUsers.toBeVisible(); + await systemConsolePage.sidebar.users.click(); + await systemConsolePage.users.toBeVisible(); // Search for target user and navigate to user detail page - await systemConsolePage.systemUsers.enterSearchText(testUser.email); - const userRow = await systemConsolePage.systemUsers.getNthRow(1); - await userRow.getByText(testUser.email).click(); + await systemConsolePage.users.searchUsers(testUser.email); + const userRow = systemConsolePage.users.usersTable.getRowByIndex(0); + await userRow.container.getByText(testUser.email).click(); // Wait for user detail page to load await systemConsolePage.page.waitForURL(`**/admin_console/user_management/user/${testUser.id}`);