diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/ui_and_api/notifications_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/ui_and_api/notifications_spec.js deleted file mode 100644 index 3a281c728c1..00000000000 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/ui_and_api/notifications_spec.js +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @channels @enterprise @system_console - -describe('System Console', () => { - before(() => { - // * Check if server has license for ID Loaded Push Notifications - cy.apiRequireLicenseForFeature('IDLoadedPushNotifications'); - - // # Update to default config - cy.apiUpdateConfig({ - EmailSettings: { - PushNotificationContents: 'full', - FeedbackName: 'Mattermost Test Team', - FeedbackEmail: 'feedback@mattertest.com', - }, - SupportSettings: { - SupportEmail: 'support@mattertest.com', - }, - }); - - // # Visit Notifications admin console page - cy.visit('/admin_console/environment/notifications'); - cy.get('.admin-console__header').should('be.visible').and('have.text', 'Notifications'); - }); - - it('Push Notification Contents', () => { - // * Verify that setting is visible and matches text content - cy.findByTestId('EmailSettings.PushNotificationContents'). - scrollIntoView().should('be.visible'). - find('label').should('be.visible').and('have.text', 'Push Notification Contents:'); - - // * Verify that the help text is visible and matches text content - cy.findByTestId('EmailSettings.PushNotificationContentshelp-text').should('be.visible').within((el) => { - const contents = [ - 'Generic description with only sender name', - ' - Includes only the name of the person who sent the message in push notifications, with no information about channel name or message contents. ', - 'Generic description with sender and channel names', - ' - Includes the name of the person who sent the message and the channel it was sent in, but not the message contents. ', - 'Full message content sent in the notification payload', - ' - Includes the message contents in the push notification payload that is relayed through Apple\'s Push Notification Service (APNS) or Google\'s Firebase Cloud Messaging (FCM). It is ', - 'highly recommended', - ' this option only be used with an "https" protocol to encrypt the connection and protect confidential information sent in messages.', - 'Full message content fetched from the server on receipt', - ' - The notification payload relayed through APNS or FCM contains no message content, instead it contains a unique message ID used to fetch message content from the server when a push notification is received by a device. If the server cannot be reached, a generic notification will be displayed.', - ]; - cy.wrap(el).should('have.text', contents.join('')); - - cy.get('strong').eq(0).should('have.text', contents[0]); - cy.get('strong').eq(1).should('have.text', contents[2]); - cy.get('strong').eq(2).should('have.text', contents[4]); - cy.get('strong').eq(3).should('have.text', contents[6]); - cy.get('strong').eq(4).should('have.text', contents[8]); - }); - - // * Verify that the option/dropdown is visible and has default value - cy.findByTestId('EmailSettings.PushNotificationContentsdropdown'). - should('be.visible'). - and('have.value', 'full'); - - const options = [ - {label: 'Generic description with only sender name', value: 'generic_no_channel'}, - {label: 'Generic description with sender and channel names', value: 'generic'}, - {label: 'Full message content sent in the notification payload', value: 'full'}, - {label: 'Full message content fetched from the server on receipt', value: 'id_loaded'}, - ]; - - // # Select each value and save - // * Verify that the config is correctly saved in the server - options.forEach((option) => { - cy.findByTestId('EmailSettings.PushNotificationContentsdropdown'). - select(option.label). - and('have.value', option.value); - cy.get('#saveSetting').click(); - - cy.apiGetConfig().then(({config}) => { - expect(config.EmailSettings.PushNotificationContents).to.equal(option.value); - }); - }); - }); - - it('MM-T1210+MM-41671 Can change Support Email setting', () => { - // # Scroll Support Email section into view and verify that it's visible - cy.findByTestId('SupportSettings.SupportEmail').scrollIntoView().should('be.visible'); - - // * Verify that setting label is visible and matches text content - cy.findByTestId('SupportSettings.SupportEmaillabel').should('be.visible').and('have.text', 'Support Email Address:'); - - // * Verify that the help text is visible and matches text content - cy.findByTestId('SupportSettings.SupportEmailhelp-text').find('span').should('be.visible').and('have.text', 'Email address displayed on support emails.'); - - const newEmail = 'changed_for_test_support@example.com'; - - // * Verify that set value is visible and matches text - cy.findByTestId('SupportSettings.SupportEmail').find('input').clear().type(newEmail).should('have.value', newEmail); - - // # Save setting - cy.get('#saveSetting').click(); - - // * Verify that the config is correctly saved in the server - cy.apiGetConfig().then(({config}) => { - expect(config.SupportSettings.SupportEmail).to.equal(newEmail); - }); - }); - - describe('MM-41671 cannot save the notifications page if mandatory fields are missing', () => { - const tests = [ - {name: 'Support Email cannot be empty', field: 'SupportSettings.SupportEmail'}, - {name: 'Notification Display Name cannot be empty', field: 'EmailSettings.FeedbackName'}, - {name: 'Notification Email Address cannot be empty', field: 'SupportSettings.SupportEmail'}, - ]; - - tests.forEach((test) => { - it(test.name, () => { - // # Clear the field - cy.findByTestId(test.field).find('input').clear(); - - // * Ensures the save button is disabled - cy.get('#saveSetting').should('be.disabled'); - - // # Insert something in the field - cy.findByTestId(test.field).find('input').type(test.field); - - // * Ensures the save button is disabled - cy.get('#saveSetting').should('be.not.disabled'); - }); - }); - }); -}); diff --git a/e2e-tests/playwright/lib/src/global_setup.ts b/e2e-tests/playwright/lib/src/global_setup.ts index f58df95047b..a3cf66518ba 100644 --- a/e2e-tests/playwright/lib/src/global_setup.ts +++ b/e2e-tests/playwright/lib/src/global_setup.ts @@ -2,13 +2,13 @@ // See LICENSE.txt for license information. import {Client4} from '@mattermost/client'; -import {UserProfile} from '@mattermost/types/users'; import {PluginManifest} from '@mattermost/types/plugins'; import {PreferenceType} from '@mattermost/types/preferences'; +import {UserProfile} from '@mattermost/types/users'; -import {defaultTeam} from './util'; import {createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server'; import {testConfig} from './test_config'; +import {defaultTeam} from './util'; export async function baseGlobalSetup() { let adminClient: Client4; diff --git a/e2e-tests/playwright/lib/src/server/client.ts b/e2e-tests/playwright/lib/src/server/client.ts index 98543aaea8d..19a8efd794c 100644 --- a/e2e-tests/playwright/lib/src/server/client.ts +++ b/e2e-tests/playwright/lib/src/server/client.ts @@ -2,10 +2,25 @@ // See LICENSE.txt for license information. import {Client4} from '@mattermost/client'; +import {AdminConfig} from '@mattermost/types/config'; import {UserProfile} from '@mattermost/types/users'; import {testConfig} from '@/test_config'; +// Extend Client4 with methods used for testing +declare module '@mattermost/client' { + interface Client4 { + updateConfigX: (config: Partial) => Promise; + } +} + +// updateConfigX merges the given config with the current config and updates it to the server +Client4.prototype.updateConfigX = async function (this: Client4, config: Partial) { + const currentConfig = await this.getConfig(); + const newConfig = {...currentConfig, ...config}; + return await this.updateConfig(newConfig); +}; + // Variable to hold cache const clients: Record = {}; diff --git a/e2e-tests/playwright/lib/src/ui/components/index.ts b/e2e-tests/playwright/lib/src/ui/components/index.ts index 45e40d1b581..b3e3ca2c69b 100644 --- a/e2e-tests/playwright/lib/src/ui/components/index.ts +++ b/e2e-tests/playwright/lib/src/ui/components/index.ts @@ -44,6 +44,7 @@ import DeletePostConfirmationDialog from './channels/delete_post_confirmation_di 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 DeleteScheduledPostModal from './channels/delete_scheduled_post_modal'; @@ -91,6 +92,7 @@ const components = { SystemUsersColumnToggleMenu, SystemConsoleFeatureDiscovery, SystemConsoleMobileSecurity, + SystemConsoleNotifications, MessagePriority, UserProfilePopover, UserAccountMenu, @@ -143,6 +145,7 @@ export { SystemUsersColumnToggleMenu, SystemConsoleFeatureDiscovery, SystemConsoleMobileSecurity, + SystemConsoleNotifications, MessagePriority, UserProfilePopover, UserAccountMenu, 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 new file mode 100644 index 00000000000..d0cab8cea29 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/site_configuration/notifications.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect} from '@playwright/test'; + +/** + * System Console -> Site Configuration -> Notifications + */ +export default class SystemConsoleNotifications { + readonly container: Locator; + + // header + readonly header: Locator; + + // Notification Display Name + readonly notificationDisplayName: Locator; + readonly notificationDisplayNameInput: Locator; + readonly notificationDisplayNameHelpText: Locator; + + // Notification From Address + readonly notificationFromAddress: Locator; + readonly notificationFromAddressInput: Locator; + readonly notificationFromAddressHelpText: Locator; + + // Support Email Address + readonly supportEmailAddress: Locator; + readonly supportEmailAddressInput: Locator; + readonly supportEmailHelpText: Locator; + + // Push Notification Contents + readonly pushNotificationContents: Locator; + readonly pushNotificationContentsDropdown: Locator; + readonly pushNotificationContentsHelpText: Locator; + + // Save button + readonly saveButton: Locator; + readonly errorMessage: Locator; + + constructor(container: Locator) { + this.container = container; + + // header + this.header = this.container.locator('.admin-console__header').getByText('Notifications'); + + // 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.pushNotificationContentsHelpText = this.container.getByTestId( + 'EmailSettings.PushNotificationContentshelp-text', + ); + + // Save button and error message + this.saveButton = this.container.getByTestId('saveSetting'); + this.errorMessage = this.container.locator('.has-error'); + } + + async toBeVisible() { + await expect(this.container).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 1ef638e2872..2f4c0fbce3d 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/system_console.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/system_console.ts @@ -11,6 +11,11 @@ export default class SystemConsolePage { readonly sidebar; readonly navbar; + // Site Configuration + + // System Console > Notifications + readonly notifications; + /** * System Console -> User Management -> Users */ @@ -34,6 +39,12 @@ export default class SystemConsolePage { constructor(page: Page) { this.page = page; + // Site Configuration + // System Console > Notifications + this.notifications = new components.SystemConsoleNotifications( + page.getByTestId('sysconsole_section_notifications'), + ); + // Areas of the page this.navbar = new components.SystemConsoleNavbar(page.locator('.backstage-navbar')); this.sidebar = new components.SystemConsoleSidebar(page.locator('.admin-sidebar')); 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 new file mode 100644 index 00000000000..400f8a98a04 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/notifications/system_console.spec.ts @@ -0,0 +1,189 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {AdminConfig} from '@mattermost/types/config'; + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify that the Push Notification Contents setting is properly displayed and can be changed to all available options + */ +test('Push Notification Contents setting displays correctly and saves all options', async ({pw}) => { + const {adminUser, adminClient} = await pw.initSetup(); + + // # Update to default config + await adminClient.updateConfigX({ + EmailSettings: { + PushNotificationContents: 'full', + FeedbackName: 'Mattermost Test Team', + FeedbackEmail: 'feedback@mattertest.com', + }, + SupportSettings: { + SupportEmail: 'support@mattertest.com', + }, + } as Partial); + + if (!adminUser) { + throw new Error('Failed to get admin user'); + } + + // # Log in as admin + const {systemConsolePage} = await pw.testBrowser.login(adminUser); + + // # Visit Notifications admin console page + await systemConsolePage.goto(); + await systemConsolePage.toBeVisible(); + await systemConsolePage.sidebar.goToItem('Notifications'); + + // # 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(); + + // * Verify that the help text is visible and matches text content + const helpText = notifications.pushNotificationContentsHelpText; + await expect(helpText).toBeVisible(); + + const contents = [ + 'Generic description with only sender name', + ' - Includes only the name of the person who sent the message in push notifications, with no information about channel name or message contents. ', + 'Generic description with sender and channel names', + ' - Includes the name of the person who sent the message and the channel it was sent in, but not the message contents. ', + 'Full message content sent in the notification payload', + " - Includes the message contents in the push notification payload that is relayed through Apple's Push Notification Service (APNS) or Google's Firebase Cloud Messaging (FCM). It is ", + 'highly recommended', + ' this option only be used with an "https" protocol to encrypt the connection and protect confidential information sent in messages.', + 'Full message content fetched from the server on receipt', + ' - The notification payload relayed through APNS or FCM contains no message content, instead it contains a unique message ID used to fetch message content from the server when a push notification is received by a device. If the server cannot be reached, a generic notification will be displayed.', + ]; + await expect(helpText).toHaveText(contents.join('')); + + const strongElements = helpText.locator('strong'); + await expect(strongElements.nth(0)).toHaveText(contents[0]); + await expect(strongElements.nth(1)).toHaveText(contents[2]); + await expect(strongElements.nth(2)).toHaveText(contents[4]); + await expect(strongElements.nth(3)).toHaveText(contents[6]); + await expect(strongElements.nth(4)).toHaveText(contents[8]); + + // * Verify that the option/dropdown is visible and has default value + const dropdown = notifications.pushNotificationContentsDropdown; + await expect(dropdown).toBeVisible(); + await expect(dropdown).toHaveValue('full'); + + const options = [ + {label: 'Generic description with only sender name', value: 'generic_no_channel'}, + {label: 'Generic description with sender and channel names', value: 'generic'}, + {label: 'Full message content sent in the notification payload', value: 'full'}, + {label: 'Full message content fetched from the server on receipt', value: 'id_loaded'}, + ]; + + // # Select each value and save + // * Verify that the config is correctly saved in the server + for (const option of options) { + await dropdown.selectOption({label: option.label}); + await expect(dropdown).toHaveValue(option.value); + + await notifications.saveButton.click(); + + // * Verify config is saved + const {adminClient} = await pw.getAdminClient(); + const config = await adminClient.getConfig(); + expect(config.EmailSettings?.PushNotificationContents).toBe(option.value); + } +}); + +/** + * @objective Verify that the Support Email setting can be changed and saved + */ +test('MM-T1210 Can change Support Email setting', async ({pw}) => { + const {adminUser} = await pw.getAdminClient(); + + if (!adminUser) { + throw new Error('Failed to get admin user'); + } + + // # Log in as admin + const {systemConsolePage} = await pw.testBrowser.login(adminUser); + + // # Visit Notifications admin console page + await systemConsolePage.goto(); + await systemConsolePage.toBeVisible(); + await systemConsolePage.sidebar.goToItem('Notifications'); + + // # 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(); + + // * 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.'); + + // # Clear and type new email + const newEmail = 'changed_for_test_support@example.com'; + await notifications.supportEmailAddressInput.clear(); + await notifications.supportEmailAddressInput.fill(newEmail); + + // * Verify that set value is visible and matches text + await expect(notifications.supportEmailAddressInput).toHaveValue(newEmail); + + // # Save setting + await notifications.saveButton.click(); + + // * Verify that the config is correctly saved in the server + const {adminClient} = await pw.getAdminClient(); + const config = await adminClient.getConfig(); + expect(config.SupportSettings?.SupportEmail).toBe(newEmail); +}); + +/** + * @objective Verify that the save button is disabled when mandatory fields are empty + */ +test('MM-41671 cannot save the notifications page if mandatory fields are missing', async ({pw}) => { + const {adminUser} = await pw.getAdminClient(); + if (!adminUser) { + throw new Error('Failed to get admin user'); + } + + // # Log in as admin + const {systemConsolePage} = await pw.testBrowser.login(adminUser); + + // # Visit Notifications admin console page + await systemConsolePage.goto(); + await systemConsolePage.toBeVisible(); + await systemConsolePage.sidebar.goToItem('Notifications'); + + // # 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}, + ]; + + for (const testCase of tests) { + // # Clear the field + await expect(testCase.fieldInput).toBeVisible(); + await testCase.fieldInput.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'); + + // * Ensure no error message is shown and the save button is not disabled + await expect(notifications.errorMessage).toHaveCount(0); + await expect(notifications.saveButton).not.toBeDisabled(); + } +});