snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify disabling "Ignore mentions" counts @channel, @here, and @all as mentions.
|
||||
*/
|
||||
test('MM-T568 Channel Notifications turn off Ignore mentions for @channel, @here and @all', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [sender] = await adminClient.createUsers(team.id, 1, 'channel-mentions');
|
||||
const channelA = await adminClient.createPublicChannel(team.id, 'Mention Source');
|
||||
const channelB = await adminClient.createPublicChannel(team.id, 'Mention Away');
|
||||
await adminClient.addToChannel(user.id, channelA.id);
|
||||
await adminClient.addToChannel(user.id, channelB.id);
|
||||
await adminClient.addToChannel(sender.id, channelA.id);
|
||||
|
||||
// # Open channel notification preferences and turn off ignore mentions
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelA.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const notificationPreferences = await channelsPage.openChannelNotificationPreferences();
|
||||
if (await notificationPreferences.ignoreMentionsCheckbox.isChecked()) {
|
||||
await notificationPreferences.ignoreMentionsCheckbox.uncheck();
|
||||
}
|
||||
await notificationPreferences.save();
|
||||
|
||||
// # Move away and post each channel-wide mention from another user. @here only mentions users who
|
||||
// are currently online, so force the receiving user's status online first to avoid a race against
|
||||
// their websocket connection setting it asynchronously. Space the posts apart since firing them
|
||||
// back-to-back races the per-post async mention-count update and can under-count the total.
|
||||
await adminClient.updateStatus({user_id: user.id, status: 'online'});
|
||||
await channelsPage.goto(team.name, channelB.name);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@all test'});
|
||||
await pw.wait(duration.one_sec);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@channel test'});
|
||||
await pw.wait(duration.one_sec);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@here test'});
|
||||
|
||||
// * Verify the channel is unread and counts all 3 channel-wide mentions exactly
|
||||
await channelsPage.sidebarLeft.assertItemUnread(channelA.name);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(channelA.name)).toHaveText('3');
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
// The notify-all confirmation modal appears once a channel has more than this many members.
|
||||
const NOTIFY_ALL_THRESHOLD = 5;
|
||||
|
||||
const NOTIFY_ALL_TITLE = 'Confirm sending notifications to entire channel';
|
||||
|
||||
/**
|
||||
* @objective Verify that channel-wide mentions with uppercase letters trigger notifications and are properly highlighted.
|
||||
*
|
||||
* @precondition
|
||||
* - Two users are members of the same team
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T483 triggers notification with uppercase channel-wide mention and highlights message for all users',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup and get the required users and team
|
||||
const {team, adminUser, user} = await pw.initSetup();
|
||||
|
||||
// # Log in as the admin in one browser session and navigate to the "town-square" channel
|
||||
const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
|
||||
// # Stub the Notification in the admin's browser to capture notifications
|
||||
await pw.stubNotification(adminPage, 'granted');
|
||||
|
||||
// # Log in as the regular user in a separate browser and navigate to the "off-topic" channel
|
||||
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(user);
|
||||
await otherChannelsPage.goto(team.name, 'off-topic');
|
||||
await otherChannelsPage.toBeVisible();
|
||||
|
||||
// # Post a channel-wide mention message "@ALL" in uppercase from the user's browser
|
||||
const message = `@ALL good morning, ${team.name}!`;
|
||||
await otherChannelsPage.postMessage(message);
|
||||
|
||||
// * Verify notification is received in the admin's browser with correct content
|
||||
const notifications = await pw.waitForNotification(adminPage);
|
||||
expect(notifications.length).toBe(1);
|
||||
|
||||
const notification = notifications[0];
|
||||
expect(notification.title).toBe('Off-Topic');
|
||||
expect(notification.body).toBe(`@${user.username}: ${message}`);
|
||||
|
||||
// The Notifications API tag must not leak the message body; the app uses the
|
||||
// opaque post ID as the tag so notifications coalesce per conversation.
|
||||
const otherLastPost = await otherChannelsPage.getLastPost();
|
||||
expect(notification.tag).toBe(await otherLastPost.getId());
|
||||
expect(notification.tag).not.toContain(message);
|
||||
|
||||
expect(notification.icon).toContain('.png');
|
||||
expect(notification.requireInteraction).toBe(false);
|
||||
expect(notification.silent).toBe(false);
|
||||
|
||||
// * Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
|
||||
await otherLastPost.toContainText(message);
|
||||
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
|
||||
// # Navigate admin to the "off-topic" channel
|
||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||
|
||||
// * Verify the message is posted and highlighted correctly for the admin user
|
||||
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||
await adminLastPost.toContainText(message);
|
||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a channel-wide mention typed in mixed case (@channEL) is recognized as a mention,
|
||||
* prompts the notify-all confirmation, and is posted with its original casing preserved.
|
||||
*/
|
||||
test(
|
||||
'MM-T484 recognizes a mixed-case @channEL channel-wide mention and preserves its casing',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
|
||||
// # Add enough members so the channel exceeds the notify-all threshold
|
||||
for (let i = 0; i < NOTIFY_ALL_THRESHOLD; i++) {
|
||||
const extraUser = await pw.createNewUserProfile(adminClient, {prefix: 'channel-wide'});
|
||||
await adminClient.addToTeam(team.id, extraUser.id);
|
||||
}
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Type a mixed-case channel-wide mention and send it
|
||||
const message = `@channEL mixed case mention ${pw.random.id()}`;
|
||||
await channelsPage.centerView.postCreate.writeMessage(message);
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
|
||||
// * Verify the notify-all confirmation modal appears (mention was recognized despite casing)
|
||||
const confirmModal = channelsPage.getConfirmModal(NOTIFY_ALL_TITLE);
|
||||
await confirmModal.toBeVisible();
|
||||
|
||||
// # Confirm sending the notification
|
||||
await confirmModal.confirm();
|
||||
|
||||
// * Verify the posted message preserves the mixed-case mention
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(message);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify an @HERE channel-wide mention typed in uppercase is recognized as a mention,
|
||||
* prompts the notify-all confirmation, and keeps its uppercase casing after switching channels.
|
||||
*/
|
||||
test(
|
||||
'MM-T485 recognizes an uppercase @HERE channel-wide mention and preserves its casing',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
|
||||
// # Add enough members so the channel exceeds the notify-all threshold
|
||||
for (let i = 0; i < NOTIFY_ALL_THRESHOLD; i++) {
|
||||
const extraUser = await pw.createNewUserProfile(adminClient, {prefix: 'channel-wide'});
|
||||
await adminClient.addToTeam(team.id, extraUser.id);
|
||||
}
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// * Verify the "Channel-wide mentions" keyword is enabled by default in notification settings
|
||||
const settingsModal = await channelsPage.openSettings();
|
||||
const notificationSettings = await settingsModal.openNotificationsTab();
|
||||
await notificationSettings.keywordsTriggerNotificationsEditButton.click();
|
||||
await expect(notificationSettings.channelWideMentionsCheckbox).toBeChecked();
|
||||
await settingsModal.close();
|
||||
|
||||
// # Type an uppercase channel-wide mention and send it
|
||||
const message = `@HERE channel-wide mention ${pw.random.id()}`;
|
||||
await channelsPage.centerView.postCreate.writeMessage(message);
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
|
||||
// * Verify the notify-all confirmation modal appears (mention was recognized despite uppercase)
|
||||
const confirmModal = channelsPage.getConfirmModal(NOTIFY_ALL_TITLE);
|
||||
await confirmModal.toBeVisible();
|
||||
|
||||
// # Confirm sending the notification
|
||||
await confirmModal.confirm();
|
||||
|
||||
// * Verify the posted message preserves the uppercase mention
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(message);
|
||||
|
||||
// # Switch to another channel and back
|
||||
await channelsPage.sidebarLeft.goToItem('town-square');
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
|
||||
// * Verify the uppercase mention is still shown after returning to the channel
|
||||
const postAfterSwitch = await channelsPage.getLastPost();
|
||||
await postAfterSwitch.toContainText(message);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify clicking a desktop notification for a message in another team's channel navigates
|
||||
* the user to that team and channel.
|
||||
*
|
||||
* @precondition
|
||||
* - Desktop notifications are set to "All new messages" so a non-mention post triggers a notification
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T492 navigates to the originating team and channel when a desktop notification is clicked',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Add the user to a second team that hosts the channel the notification will come from
|
||||
const secondTeam = await pw.createNewTeam(adminClient, {
|
||||
name: 'team',
|
||||
displayName: 'Second Team',
|
||||
type: 'O',
|
||||
unique: true,
|
||||
});
|
||||
await adminClient.addToTeam(secondTeam.id, user.id);
|
||||
await adminClient.addToTeam(secondTeam.id, adminUser.id);
|
||||
const otherChannel = await adminClient.getChannelByName(secondTeam.id, 'off-topic');
|
||||
|
||||
// # Set the user's desktop notifications to fire for all new messages
|
||||
await adminClient.patchUser({
|
||||
id: user.id,
|
||||
notify_props: {
|
||||
...user.notify_props,
|
||||
desktop: 'all',
|
||||
desktop_sound: 'false',
|
||||
},
|
||||
});
|
||||
|
||||
// # Log in and view the first team's Town Square, then stub notifications
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
// # Post a message from the admin in the second team's channel
|
||||
const message = `Click notification from other team ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: otherChannel.id, user_id: adminUser.id, message});
|
||||
|
||||
// * Verify a desktop notification is received for the second team's channel
|
||||
const notifications = await pw.waitForNotification(page);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].title).toBe('Off-Topic');
|
||||
expect(notifications[0].body).toContain(message);
|
||||
|
||||
// # Click the desktop notification
|
||||
await pw.clickNotification(page);
|
||||
|
||||
// * Verify the app navigates to the second team's channel where the message was posted
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/${secondTeam.name}/`);
|
||||
await channelsPage.centerView.header.toHaveTitle('Off-Topic');
|
||||
await channelsPage.centerView.waitUntilLastPostContains(message);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify clicking a desktop notification for a direct message navigates the user to that DM.
|
||||
*
|
||||
* @precondition
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T493 navigates to the direct message when its desktop notification is clicked',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a direct message channel between the admin and the user
|
||||
const dmChannel = await adminClient.createDirectChannel([adminUser.id, user.id]);
|
||||
|
||||
// # Log in and view Town Square, then stub notifications
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
// # Send a direct message from the admin to the user
|
||||
const message = `Click notification from DM ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message});
|
||||
|
||||
// * Verify a desktop notification is received for the direct message
|
||||
const notifications = await pw.waitForNotification(page);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].body).toContain(message);
|
||||
|
||||
// # Click the desktop notification
|
||||
await pw.clickNotification(page);
|
||||
|
||||
// * Verify the app navigates to the direct message conversation with the admin
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain('/messages/');
|
||||
await channelsPage.centerView.header.toHaveTitle(adminUser.username);
|
||||
await channelsPage.centerView.waitUntilLastPostContains(message);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user in "Do Not Disturb" does not receive desktop notifications, and receives them
|
||||
* again once the status is cleared.
|
||||
*
|
||||
* @precondition
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T495 suppresses desktop notifications while Do Not Disturb is enabled',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a direct message channel between the admin and the user
|
||||
const dmChannel = await adminClient.createDirectChannel([adminUser.id, user.id]);
|
||||
|
||||
// # Log in and view Town Square (not the DM), then stub notifications
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
// # Send a direct message from the admin while the user is available
|
||||
const firstMessage = `DND baseline ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: firstMessage});
|
||||
|
||||
// * Verify the direct message triggers a desktop notification
|
||||
const notifications = await pw.waitForNotification(page);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].body).toContain(firstMessage);
|
||||
|
||||
// # Enable Do Not Disturb via the slash command
|
||||
await channelsPage.postMessage('/dnd');
|
||||
|
||||
// * Verify the Do Not Disturb system message confirms it is enabled
|
||||
await channelsPage.centerView.waitUntilLastPostContains('Do Not Disturb is enabled');
|
||||
|
||||
// # Send another direct message from the admin while the user is in Do Not Disturb
|
||||
const secondMessage = `DND suppressed ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: secondMessage});
|
||||
|
||||
// * Verify no second desktop notification is received while in Do Not Disturb
|
||||
const suppressed = await pw.waitForNotification(page, 2, pw.duration.two_sec);
|
||||
expect(suppressed).toHaveLength(0);
|
||||
|
||||
// # Clear the status back to online via the slash command
|
||||
await channelsPage.postMessage('/online');
|
||||
|
||||
// * Verify the account menu reflects the online status
|
||||
const accountMenu = await channelsPage.openUserAccountMenu();
|
||||
await expect(accountMenu.online).toHaveAttribute('aria-checked', 'true');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test, testConfig} from '@mattermost/playwright-lib';
|
||||
|
||||
async function postToWebhook(webhookId: string, payload: Record<string, unknown>) {
|
||||
const hookUrl = `${testConfig.baseURL}/hooks/${webhookId}`;
|
||||
const resp = await fetch(hookUrl, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Webhook POST failed: ${resp.status} ${await resp.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
function notificationText(n: {title?: string; body?: string}) {
|
||||
return `${n.title ?? ''}\n${n.body ?? ''}`;
|
||||
}
|
||||
|
||||
function channelHasMessage(posts: {posts: Record<string, {message?: string}>}, message: string) {
|
||||
return Object.values(posts.posts).some((p) => p.message?.includes(message));
|
||||
}
|
||||
|
||||
test.describe('Silent notification webhook delivery', () => {
|
||||
/**
|
||||
* @objective Verify integration posts with silent=true are delivered in-channel without unread or desktop notify side effects.
|
||||
*
|
||||
* @precondition
|
||||
* - User is a member of a dedicated channel with an incoming webhook
|
||||
* - User is viewing a different channel so webhook posts are not auto-marked read
|
||||
* - Desktop notify level is mention-based (default); messages include @username
|
||||
*/
|
||||
test(
|
||||
'incoming webhook silent post is visible without unread or desktop notification',
|
||||
{tag: ['@notifications']},
|
||||
async ({pw, headless, browserName}) => {
|
||||
test.skip(
|
||||
headless && browserName !== 'firefox' && browserName !== 'webkit',
|
||||
'In headless mode, Notification stubbing is only reliable in Firefox (and WebKit when configured). Use npm run playwright-ui (headed), --project=firefox, or PW_HEADLESS=false.',
|
||||
);
|
||||
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'silent-hook',
|
||||
displayName: 'Silent Hook Test',
|
||||
unique: true,
|
||||
}),
|
||||
);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const webhook = await adminClient.createIncomingWebhook({
|
||||
channel_id: channel.id,
|
||||
display_name: 'Silent E2E Hook',
|
||||
});
|
||||
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Mark the webhook channel as read, then view another channel
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
const channelLink = channelsPage.sidebarLeft.container.locator(`#sidebarItem_${channel.name}`);
|
||||
|
||||
// # Post a normal webhook message with @mention to verify unread + notification baseline
|
||||
const normalMessage = `normal webhook @${user.username} ${Date.now()}`;
|
||||
await postToWebhook(webhook.id, {text: normalMessage});
|
||||
await expect(channelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
||||
await expect
|
||||
.poll(async () => (await page.evaluate(() => window.getNotifications())).length)
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
await channelsPage.sidebarLeft.goToItem(channel.name);
|
||||
const normalPost = await channelsPage.getLastPost();
|
||||
await normalPost.toContainText(normalMessage);
|
||||
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await expect(channelLink).not.toHaveClass(/unread-title/);
|
||||
|
||||
const syncChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'sync-pulse',
|
||||
displayName: 'Sync Pulse',
|
||||
unique: true,
|
||||
}),
|
||||
);
|
||||
await adminClient.addToChannel(user.id, syncChannel.id);
|
||||
const syncChannelLink = channelsPage.sidebarLeft.container.locator(`#sidebarItem_${syncChannel.name}`);
|
||||
|
||||
await pw.clearCapturedNotifications(page);
|
||||
|
||||
// # Post silent webhook, then a normal post on another channel to flush the WS pipeline (FIFO)
|
||||
const silentMessage = `silent webhook @${user.username} ${Date.now()}`;
|
||||
await postToWebhook(webhook.id, {text: silentMessage, silent: true});
|
||||
|
||||
await expect
|
||||
.poll(async () => channelHasMessage(await adminClient.getPosts(channel.id, 0, 30), silentMessage))
|
||||
.toBe(true);
|
||||
|
||||
const pulseMessage = `pulse ${Date.now()}`;
|
||||
await adminClient.createPost({channel_id: syncChannel.id, message: pulseMessage});
|
||||
|
||||
await expect(syncChannelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
||||
await expect(channelLink).not.toHaveClass(/unread-title/);
|
||||
|
||||
const notificationsAfterSilent = await page.evaluate(() => window.getNotifications());
|
||||
expect(notificationsAfterSilent.some((n) => notificationText(n).includes(silentMessage))).toBe(false);
|
||||
|
||||
// * Silent post is still visible; no New Messages separator (final assertions — goToItem marks viewed)
|
||||
await channelsPage.sidebarLeft.goToItem(channel.name);
|
||||
const silentPost = await channelsPage.getLastPost();
|
||||
await silentPost.toContainText(silentMessage);
|
||||
await expect(page.locator('[data-testid="NotificationSeparator"]')).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user @mentioned in a silent webhook post does not receive a desktop notification.
|
||||
*/
|
||||
test(
|
||||
'silent webhook @mention does not desktop-notify the mentioned user',
|
||||
{tag: ['@notifications']},
|
||||
async ({pw, headless, browserName}) => {
|
||||
test.skip(
|
||||
headless && browserName !== 'firefox' && browserName !== 'webkit',
|
||||
'Desktop notification stubbing is only reliable in headless Firefox and WebKit.',
|
||||
);
|
||||
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
const mentionedUser = await pw.createNewUserProfile(adminClient);
|
||||
await adminClient.addToTeam(team.id, mentionedUser.id);
|
||||
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'silent-mention',
|
||||
displayName: 'Silent Mention Test',
|
||||
unique: true,
|
||||
}),
|
||||
);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
await adminClient.addToChannel(mentionedUser.id, channel.id);
|
||||
|
||||
const webhook = await adminClient.createIncomingWebhook({
|
||||
channel_id: channel.id,
|
||||
display_name: 'Silent Mention Hook',
|
||||
});
|
||||
|
||||
const {channelsPage: posterChannelsPage} = await pw.testBrowser.login(user);
|
||||
await posterChannelsPage.goto(team.name, channel.name);
|
||||
await posterChannelsPage.toBeVisible();
|
||||
await posterChannelsPage.goto(team.name, 'off-topic');
|
||||
await posterChannelsPage.toBeVisible();
|
||||
|
||||
const {page: mentioneePage, channelsPage: mentioneeChannelsPage} =
|
||||
await pw.testBrowser.login(mentionedUser);
|
||||
await mentioneeChannelsPage.goto(team.name, 'off-topic');
|
||||
await mentioneeChannelsPage.toBeVisible();
|
||||
await pw.stubNotification(mentioneePage, 'granted');
|
||||
|
||||
const channelLink = mentioneeChannelsPage.sidebarLeft.container.locator(`#sidebarItem_${channel.name}`);
|
||||
|
||||
// # Baseline: normal @mention webhook should notify the mentioned user and mark channel unread
|
||||
const normalMessage = `normal mention @${mentionedUser.username} ${Date.now()}`;
|
||||
await postToWebhook(webhook.id, {text: normalMessage});
|
||||
await expect(channelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
||||
await expect
|
||||
.poll(async () => (await mentioneePage.evaluate(() => window.getNotifications())).length)
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// # Mark the channel read by opening it, then return to off-topic for a clean baseline
|
||||
await mentioneeChannelsPage.sidebarLeft.goToItem(channel.name);
|
||||
await mentioneeChannelsPage.toBeVisible();
|
||||
await mentioneeChannelsPage.goto(team.name, 'off-topic');
|
||||
await mentioneeChannelsPage.toBeVisible();
|
||||
await expect(channelLink).not.toHaveClass(/unread-title/);
|
||||
|
||||
await pw.clearCapturedNotifications(mentioneePage);
|
||||
|
||||
// # Silent @mention should not notify the mentioned user nor mark the channel unread
|
||||
const silentMessage = `silent mention @${mentionedUser.username} ${Date.now()}`;
|
||||
await postToWebhook(webhook.id, {text: silentMessage, silent: true});
|
||||
|
||||
await expect
|
||||
.poll(async () => channelHasMessage(await adminClient.getPosts(channel.id, 0, 30), silentMessage))
|
||||
.toBe(true);
|
||||
|
||||
await expect
|
||||
.poll(async () => (await mentioneePage.evaluate(() => window.getNotifications())).length)
|
||||
.toBe(0);
|
||||
await expect(channelLink).not.toHaveClass(/unread-title/);
|
||||
|
||||
const notificationsAfterSilent = await mentioneePage.evaluate(() => window.getNotifications());
|
||||
expect(notificationsAfterSilent.some((n) => notificationText(n).includes(silentMessage))).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {AdminConfig} from '@mattermost/types/config';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import type {TextInputSetting} from '@mattermost/playwright-lib';
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* Patch the Notifications page required fields to known valid values so tests
|
||||
* that load the page always start with a saveable form state, regardless of
|
||||
* what other parallel tests may have left in the server config.
|
||||
*/
|
||||
async function resetNotificationsConfig(adminClient: {
|
||||
patchConfig: (config: DeepPartial<AdminConfig>) => Promise<unknown>;
|
||||
}) {
|
||||
await adminClient.patchConfig({
|
||||
EmailSettings: {
|
||||
FeedbackName: 'Mattermost Notification',
|
||||
FeedbackEmail: 'notification@mattertest.com',
|
||||
},
|
||||
SupportSettings: {
|
||||
SupportEmail: 'support@mattertest.com',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait until API reflects required notification fields (guards against concurrent initSetup). */
|
||||
async function waitForNotificationsServerPreconditions(adminClient: {getConfig: () => Promise<unknown>}) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const c = (await adminClient.getConfig()) as AdminConfig;
|
||||
const support = c.SupportSettings?.SupportEmail?.trim();
|
||||
const feedbackEmail = c.EmailSettings?.FeedbackEmail?.trim();
|
||||
const feedbackName = c.EmailSettings?.FeedbackName?.trim();
|
||||
return Boolean(support && feedbackEmail && feedbackName);
|
||||
},
|
||||
{timeout: 90_000, intervals: [300, 800, 1500, 3000]},
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the required notification text fields so the form is valid before saving. This does not
|
||||
* assert that Save becomes enabled: the admin console only enables it once the form is dirty, and
|
||||
* these values may already be what the server holds (e.g. a reused server where this spec ran before).
|
||||
* Assert on Save at the call sites that actually changed something.
|
||||
*/
|
||||
async function fillRequiredNotificationFields(notifications: {
|
||||
notificationDisplayName: TextInputSetting;
|
||||
notificationFromAddress: TextInputSetting;
|
||||
supportEmailAddress: TextInputSetting;
|
||||
notificationReplyToAddress: TextInputSetting;
|
||||
}) {
|
||||
await notifications.notificationDisplayName.container.scrollIntoViewIfNeeded();
|
||||
await notifications.notificationDisplayName.fill('Mattermost Notification');
|
||||
await notifications.notificationFromAddress.container.scrollIntoViewIfNeeded();
|
||||
await notifications.notificationFromAddress.fill('notification@mattertest.com');
|
||||
await notifications.supportEmailAddress.container.scrollIntoViewIfNeeded();
|
||||
await notifications.supportEmailAddress.fill('support@mattertest.com');
|
||||
await notifications.notificationReplyToAddress.container.scrollIntoViewIfNeeded();
|
||||
await notifications.notificationReplyToAddress.fill('notification@mattertest.com');
|
||||
}
|
||||
|
||||
test.describe('System Console Notifications', () => {
|
||||
test.describe.configure({mode: 'serial'});
|
||||
|
||||
/**
|
||||
* @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}) => {
|
||||
// Multiple reload/save/retry rounds — default 60 s CI timeout is too tight when shards contend on config.
|
||||
test.setTimeout(240000);
|
||||
|
||||
const {adminUser, adminClient} = await pw.getAdminClient();
|
||||
|
||||
if (!adminUser || !adminClient) {
|
||||
throw new Error('Failed to get admin user');
|
||||
}
|
||||
|
||||
// Ensure required Notifications fields are populated so the Save button
|
||||
// starts enabled — prevents state pollution from concurrent initSetup() calls
|
||||
// that reset FeedbackName and SupportEmail to '' via updateConfig(defaultConfig).
|
||||
await resetNotificationsConfig(adminClient);
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
|
||||
// # Update to default config with required notification fields populated
|
||||
await adminClient.patchConfig({
|
||||
EmailSettings: {
|
||||
FeedbackName: 'Mattermost Notification',
|
||||
FeedbackEmail: 'notification@mattertest.com',
|
||||
PushNotificationContents: 'full',
|
||||
},
|
||||
SupportSettings: {
|
||||
SupportEmail: 'support@mattertest.com',
|
||||
},
|
||||
});
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit Notifications admin console page (direct URL — sidebar link can be off-screen in CI)
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
await systemConsolePage.gotoNotificationsSettings();
|
||||
|
||||
// # Wait for Notifications section to load
|
||||
const notifications = systemConsolePage.notifications;
|
||||
await notifications.toBeVisible();
|
||||
|
||||
// Re-apply guard: a concurrent initSetup() may have cleared SupportEmail (a required
|
||||
// field) between the initial resetNotificationsConfig call and the page rendering here,
|
||||
// leaving the Save button disabled. Re-apply the config and reload so the form
|
||||
// renders with all required fields populated.
|
||||
await resetNotificationsConfig(adminClient);
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
await systemConsolePage.page.reload();
|
||||
await systemConsolePage.gotoNotificationsSettings();
|
||||
await notifications.toBeVisible();
|
||||
await fillRequiredNotificationFields(notifications);
|
||||
|
||||
// * Verify that setting is visible and matches text content
|
||||
await notifications.pushNotificationContents.container.scrollIntoViewIfNeeded();
|
||||
await notifications.pushNotificationContents.toBeVisible();
|
||||
|
||||
// * Verify that the help text is visible and matches text content
|
||||
const helpText = notifications.pushNotificationContents.helpText;
|
||||
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.pushNotificationContents.dropdown;
|
||||
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) {
|
||||
let saved = false;
|
||||
for (let attempt = 0; attempt < 8 && !saved; attempt++) {
|
||||
await resetNotificationsConfig(adminClient);
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
await systemConsolePage.page.reload();
|
||||
await systemConsolePage.gotoNotificationsSettings();
|
||||
await notifications.toBeVisible();
|
||||
await notifications.pushNotificationContents.container.scrollIntoViewIfNeeded();
|
||||
await notifications.pushNotificationContents.toBeVisible();
|
||||
|
||||
const loopDropdown = notifications.pushNotificationContents.dropdown;
|
||||
await expect(loopDropdown).toBeVisible();
|
||||
await loopDropdown.selectOption({label: option.label});
|
||||
await expect(loopDropdown).toHaveValue(option.value);
|
||||
await fillRequiredNotificationFields(notifications);
|
||||
|
||||
// The dropdown change above dirties the form, so Save must become enabled here.
|
||||
await expect(notifications.saveButton).not.toBeDisabled({timeout: 25000});
|
||||
await notifications.save();
|
||||
|
||||
const {adminClient: pollClient} = await pw.getAdminClient();
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const config = await pollClient.getConfig();
|
||||
return config.EmailSettings?.PushNotificationContents;
|
||||
},
|
||||
{timeout: 45000, intervals: [500, 1000, 2000, 3000]},
|
||||
)
|
||||
.toBe(option.value);
|
||||
saved = true;
|
||||
} catch {
|
||||
// Concurrent full-config resets can drop the save — reload and retry.
|
||||
}
|
||||
}
|
||||
if (!saved) {
|
||||
throw new Error(`Failed to save PushNotificationContents=${option.value} after retries`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that the Support Email setting can be changed and saved
|
||||
*/
|
||||
test('MM-T1210 Can change Support Email setting', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.getAdminClient();
|
||||
|
||||
if (!adminUser || !adminClient) {
|
||||
throw new Error('Failed to get admin user');
|
||||
}
|
||||
|
||||
// Ensure required Notifications fields are populated so the Save button
|
||||
// starts enabled — prevents state pollution from other parallel tests.
|
||||
await resetNotificationsConfig(adminClient);
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit Notifications admin console page (direct URL — sidebar link can be off-screen in CI)
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
await systemConsolePage.gotoNotificationsSettings();
|
||||
|
||||
// # 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
|
||||
await notifications.supportEmailAddress.container.scrollIntoViewIfNeeded();
|
||||
await notifications.supportEmailAddress.toBeVisible();
|
||||
|
||||
// * Verify that the help text is visible and matches text content
|
||||
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.supportEmailAddress.clear();
|
||||
await notifications.supportEmailAddress.fill(newEmail);
|
||||
|
||||
// * Verify that set value is visible and matches text
|
||||
await expect(notifications.supportEmailAddress.input).toHaveValue(newEmail);
|
||||
|
||||
// # Wait for Save button to be enabled (React processes fill() events asynchronously)
|
||||
await expect(notifications.saveButton).not.toBeDisabled();
|
||||
|
||||
// # Save setting
|
||||
await notifications.save();
|
||||
|
||||
// * Verify that the config is correctly saved in the server
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const config = await adminClient.getConfig();
|
||||
return 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, adminClient} = await pw.getAdminClient();
|
||||
if (!adminUser || !adminClient) {
|
||||
throw new Error('Failed to get admin user');
|
||||
}
|
||||
|
||||
// Ensure all required fields are populated before the test starts so that
|
||||
// clearing one field at a time reliably disables the save button, and
|
||||
// restoring it reliably re-enables it (no other empty field blocking save).
|
||||
await resetNotificationsConfig(adminClient);
|
||||
await waitForNotificationsServerPreconditions(adminClient);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit Notifications admin console page (direct URL — sidebar link can be off-screen in CI)
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
await systemConsolePage.gotoNotificationsSettings();
|
||||
|
||||
// # Wait for Notifications section to load
|
||||
const notifications = systemConsolePage.notifications;
|
||||
await notifications.toBeVisible();
|
||||
|
||||
const tests = [
|
||||
{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 testCase.field.toBeVisible();
|
||||
await testCase.field.clear();
|
||||
|
||||
// Scope error check to this field's container to avoid strict-mode failure
|
||||
// when other fields on the page also have validation errors simultaneously.
|
||||
const fieldError = testCase.field.container.locator('.has-error');
|
||||
|
||||
// * Error message is shown and save button is disabled
|
||||
await expect(fieldError).toHaveText(`"${testCase.name}" is required`);
|
||||
await expect(notifications.saveButton).toBeDisabled();
|
||||
|
||||
// # Restore the field with a valid value so format-validation errors from
|
||||
// this field don't interfere with the next iteration.
|
||||
await testCase.field.fill('test@example.com');
|
||||
|
||||
// * Ensure error for this field is gone and save button is enabled
|
||||
await expect(fieldError).toHaveCount(0);
|
||||
await expect(notifications.saveButton).not.toBeDisabled();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user