fix flaky notifications tests and migrated to playwright (#34449)

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
sabril 2025-12-17 14:51:20 +08:00 committed by GitHub
parent 5fe3987e91
commit d7aebb555d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 297 additions and 139 deletions

View file

@ -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');
});
});
});
});

View file

@ -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;

View file

@ -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<AdminConfig>) => Promise<AdminConfig>;
}
}
// updateConfigX merges the given config with the current config and updates it to the server
Client4.prototype.updateConfigX = async function (this: Client4, config: Partial<AdminConfig>) {
const currentConfig = await this.getConfig();
const newConfig = {...currentConfig, ...config};
return await this.updateConfig(newConfig);
};
// Variable to hold cache
const clients: Record<string, ClientCache> = {};

View file

@ -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,

View file

@ -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();
}
}

View file

@ -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'));

View file

@ -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<AdminConfig>);
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();
}
});