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 Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that mentioning a user inside a different pair of users' direct message does not notify the mentioned user.
*/
test('MM-T448 does not notify a user mentioned in a different DM', {tag: '@direct_messages'}, async ({pw}) => {
// # Create the test user plus two more users on the team
const {user, team, adminClient} = await pw.initSetup();
const [partner, mentioned] = await adminClient.createUsers(team.id, 2, 'dmuser');
const token = `dmmention${pw.random.id()}`;
// # As the test user, open a DM with the partner and mention the third user there
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const dmModal = await channelsPage.openDirectChannelsModal();
await dmModal.selectUser(partner);
await dmModal.goToChannel();
await channelsPage.postMessage(`@${mentioned.username} ${token}`);
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(token);
// # Log in as the mentioned user and open Recent Mentions
const {channelsPage: mentionedPage} = await pw.testBrowser.login(mentioned);
await mentionedPage.goto(team.name, 'town-square');
await mentionedPage.toBeVisible();
await mentionedPage.globalHeader.openRecentMentions();
// * Verify the mentioned user did not receive the mention from the other users' DM
await mentionedPage.searchResultsPanel.toBeVisible();
await expect(mentionedPage.searchResultsPanel.getResultByText(token)).toHaveCount(0);
});
/**
* @objective Verify that deleting a parent post from a direct message reply thread removes the post, its reply, and closes the thread.
*/
test(
'MM-T454 deletes a parent post in a direct message from the reply thread',
{tag: '@direct_messages'},
async ({pw}) => {
// # Create the test user plus another user to direct message
const {user, team, adminClient} = await pw.initSetup();
const [otherUser] = await adminClient.createUsers(team.id, 1, 'other');
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Open a direct message channel with the other user
const dmModal = await channelsPage.openDirectChannelsModal();
await dmModal.selectUser(otherUser);
await dmModal.goToChannel();
// # Post a parent message and reply to it in the thread
const parent = `This is the parent post ${pw.random.id()}`;
await channelsPage.postMessage(parent);
const parentPost = await channelsPage.getLastPost();
const parentId = await parentPost.getId();
await parentPost.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.sidebarRight.postMessage(`This is a reply ${pw.random.id()}`);
// # Delete the parent post (which has a reply) while its thread is open.
// The parent is deleted from the center post menu, which is equivalent to and more
// stable than the virtualized RHS root menu; the resulting behavior is identical.
const centerParent = await channelsPage.centerView.getPostById(parentId);
await centerParent.hover();
await centerParent.postMenu.openDotMenu();
await channelsPage.postDotMenu.deleteMenuItem.click();
await channelsPage.deletePostModal.toBeVisible();
await channelsPage.deletePostModal.confirm();
// * Verify the reply thread closes and the parent post is gone from the center channel
await expect(channelsPage.sidebarRight.container).not.toBeVisible();
await expect(channelsPage.centerView.container.getByText(parent, {exact: true})).not.toBeVisible();
},
);
/**
* @objective Verify that a direct message can be opened from a user's profile popover on their post.
*/
test('MM-T455 opens a direct message from a profile popover', {tag: '@direct_messages'}, async ({pw}) => {
// # Create the test user plus another user who posts in a shared channel
const {user, team, adminClient} = await pw.initSetup();
const [author] = await adminClient.createUsers(team.id, 1, 'author');
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
await adminClient.addToChannel(author.id, offTopic.id);
const token = `popover dm ${pw.random.id()}`;
const {client: authorClient} = await pw.makeClient(author);
await authorClient.createPost({channel_id: offTopic.id, message: token});
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
// # Open the author's profile popover from their post and click Message
const post = await channelsPage.getLastPost();
const popover = await channelsPage.openProfilePopover(post);
await popover.message();
// * Verify a direct message channel with the author is opened
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/@${author.username}`);
await channelsPage.centerView.header.toHaveTitle(author.username);
// # Post a message in the direct message channel
const dmMessage = `direct message ${pw.random.id()}`;
await channelsPage.postMessage(dmMessage);
// * Verify the direct message is posted
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(dmMessage);
});

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify a direct message can be edited and the recipient sees the updated body and edited marker without a mention badge.
*/
test('MM-T449 edits a direct message body', {tag: '@direct_messages'}, async ({pw}) => {
const originalMessage = 'Hello';
const editedMessage = 'Hello World';
// # Create a direct message and send its initial post
const {user: sender, team, adminClient} = await pw.initSetup();
const [recipient] = await adminClient.createUsers(team.id, 1, 'dm-edit');
const {client: senderClient} = await pw.makeClient(sender);
const dmChannel = await adminClient.createDirectChannel([sender.id, recipient.id]);
const post = await senderClient.createPost({
channel_id: dmChannel.id,
message: originalMessage,
});
// # Open the direct message as the recipient
const {channelsPage: recipientChannelsPage, page: recipientPage} = await pw.testBrowser.login(recipient);
await recipientChannelsPage.goto(team.name, `@${sender.username}`);
await recipientChannelsPage.toBeVisible();
// * Verify the original message is sent and is not pending
const recipientPost = await recipientChannelsPage.centerView.getPostById(post.id);
await recipientPost.toContainText(originalMessage);
await recipientPost.toHaveId(post.id);
// # Open the direct message as the sender and edit the post with the Up arrow shortcut
const {channelsPage: senderChannelsPage, page: senderPage} = await pw.testBrowser.login(sender);
await senderChannelsPage.goto(team.name, `@${recipient.username}`);
await senderChannelsPage.toBeVisible();
await senderChannelsPage.centerView.postCreate.input.focus();
await senderPage.keyboard.press('ArrowUp');
await senderChannelsPage.centerView.postEdit.toBeVisible();
await expect(senderChannelsPage.centerView.postEdit.input).toHaveValue(originalMessage);
await senderChannelsPage.centerView.postEdit.writeMessage(editedMessage);
await senderChannelsPage.centerView.postEdit.sendMessage();
// * Verify the sender sees the edited body and marker
const senderPost = await senderChannelsPage.centerView.getPostById(post.id);
await senderPost.toContainText(editedMessage);
await senderPost.toBeEdited();
// # Reload the recipient's direct message
await recipientPage.reload();
await recipientChannelsPage.toBeVisible();
// * Verify the recipient sees the edit without an unread mention indicator
const updatedRecipientPost = await recipientChannelsPage.centerView.getPostById(post.id);
await updatedRecipientPost.toContainText(editedMessage);
await updatedRecipientPost.toBeEdited();
await expect(recipientChannelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).not.toBeVisible();
});

View File

@@ -0,0 +1,129 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {getChannelSlugFromUrl} from './helpers';
/**
* @objective Verify users can be added and removed while creating a group message.
*/
test('MM-T460 Add and remove users while creating new Group Message', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const participants = await adminClient.createUsers(team.id, 3, 'gm-edit');
// # Open the Direct Messages modal and select two users
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const modal = await channelsPage.openDirectChannelsModal();
await modal.selectUser(participants[0]);
await modal.selectUser(participants[1]);
// * Verify two users are selected, and the remaining-members counter reflects the selection
// (a group message allows up to 8 members total, 7 besides the current user)
await expect(modal.getRemoveButton(participants[0].username)).toBeVisible();
await expect(modal.getRemoveButton(participants[1].username)).toBeVisible();
await expect(modal.memberLimitHelpText).toContainText('You can add 5 more people');
// # Remove one user via its remove button
await modal.removeUser(participants[1].username);
// * Verify the counter reflects the removal
await expect(modal.memberLimitHelpText).toContainText('You can add 6 more people');
// # Add a different user
await modal.selectUser(participants[2]);
// * Verify the selected list reflects the removal and addition, and the counter updates again
await expect(modal.getRemoveButton(participants[1].username)).toHaveCount(0);
await expect(modal.getRemoveButton(participants[2].username)).toBeVisible();
await expect(modal.memberLimitHelpText).toContainText('You can add 5 more people');
// # Remove the last selected user via backspace in the empty search input
await modal.searchInput.press('Backspace');
// * Verify the selected list and counter reflect the backspace removal
await expect(modal.getRemoveButton(participants[2].username)).toHaveCount(0);
await expect(modal.memberLimitHelpText).toContainText('You can add 6 more people');
});
/**
* @objective Verify the group message intro, sidebar label, and member count render for participants.
*/
test('MM-T465 Create a group message and show participant details', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const participants = await adminClient.createUsers(team.id, 2, 'gm-intro');
// # Create a group message from the Direct Messages modal
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const modal = await channelsPage.openDirectChannelsModal();
for (const participant of participants) {
await modal.selectUser(participant);
}
await modal.goToChannel();
// * Verify the group message intro, participant avatars, sidebar label, and member count. The
// sidebar label sorts the other participants' usernames with numeric-aware locale comparison
// (digit runs compare by value, not by character code), so match that here rather than a plain
// string sort.
const sortedParticipants = [...participants].sort((a, b) =>
a.username.localeCompare(b.username, undefined, {numeric: true}),
);
await expect(channelsPage.centerView.channelIntro).toContainText('This is the start of your group message history');
await expect(channelsPage.centerView.channelIntro.locator('.profile-icon')).toHaveCount(2);
await channelsPage.centerView.header.toHaveTitle(participants[0].username);
await channelsPage.centerView.header.toHaveTitle(participants[1].username);
await expect(channelsPage.sidebarLeft.item(participants[0].username)).toContainText(
`${sortedParticipants[0].username}, ${sortedParticipants[1].username}`,
);
await expect(channelsPage.sidebarLeft.memberCountBadge(participants[0].username)).toContainText('2');
// # Post a message so the group message persists in the sidebar
await channelsPage.postMessage('Hi group');
// * Verify the message appears in the group message
await (await channelsPage.getLastPost()).toContainText('Hi group');
});
/**
* @objective Verify that a group message lists its members and that adding another member creates a new group message.
*/
test(
'MM-T467 adds a user to a group message to create a new group message',
{tag: '@direct_messages'},
async ({pw}) => {
// # Create the test user plus three more users on the team
const {user, team, adminClient} = await pw.initSetup();
const [member1, member2, member3] = await adminClient.createUsers(team.id, 3, 'gm');
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Create a group message with two members
const dmModal = await channelsPage.openDirectChannelsModal();
await dmModal.selectUser(member1);
await dmModal.selectUser(member2);
await dmModal.goToChannel();
const firstSlug = getChannelSlugFromUrl(page);
// * Verify the group message lists both members in the header
await channelsPage.centerView.header.toHaveTitle(member1.username);
await channelsPage.centerView.header.toHaveTitle(member2.username);
// # Create a group message that adds a third member
const dmModal2 = await channelsPage.openDirectChannelsModal();
await dmModal2.selectUser(member1);
await dmModal2.selectUser(member2);
await dmModal2.selectUser(member3);
await dmModal2.goToChannel();
// * Verify a new, different group message channel is created that includes the added member
const secondSlug = getChannelSlugFromUrl(page);
expect(secondSlug).not.toBe(firstSlug);
await channelsPage.centerView.header.toHaveTitle(member3.username);
},
);

View File

@@ -0,0 +1,132 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {duration, expect, test} from '@mattermost/playwright-lib';
import {getChannelSlugFromUrl} from './helpers';
/**
* @objective Verify muting a group message suppresses normal unread notification styling but keeps mention counts.
*/
test('MM-T475 Group Message Channel Preferences Mute channel', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [sender, secondParticipant] = await adminClient.createUsers(team.id, 2, 'gm-mute');
const gmChannel = await adminClient.createGroupChannel([user.id, sender.id, secondParticipant.id]);
// # Open the GM and mute it through notification preferences
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.gotoMessage(team.name, gmChannel.name);
await channelsPage.toBeVisible();
const notificationPreferences = await channelsPage.openChannelNotificationPreferences();
await notificationPreferences.muteChannelCheckbox.check();
await notificationPreferences.save();
// # Post a normal message from another participant while away
// `goto` is a full page navigation, so (re-)stub notifications only once settled on the away channel
await channelsPage.goto(team.name, 'off-topic');
await pw.stubNotification(page, 'granted');
await adminClient.createPost({channel_id: gmChannel.id, user_id: sender.id, message: 'Muted normal GM message'});
// * Verify no notification fires for the normal message while muted. Every GM post implicitly
// mentions all members server-side (server/channels/app/notification.go), so the mention badge
// still shows here even for a non-@mention message — that's current, correct behavior, not a bug.
await pw.wait(duration.two_sec);
expect(await page.evaluate(() => window.getNotifications())).toHaveLength(0);
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
// # Post a message that mentions the user while away
await adminClient.createPost({
channel_id: gmChannel.id,
user_id: sender.id,
message: `@${user.username} Muted mention GM message`,
});
// * Verify muting still suppresses the notification, but the mention indicator appears
await pw.wait(duration.two_sec);
expect(await page.evaluate(() => window.getNotifications())).toHaveLength(0);
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
});
/**
* @objective Verify a closed group message can be reopened from the Direct Messages modal.
*/
test('MM-T478 Closing group message channels and re-opening via Direct Messages modal', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const participants = await adminClient.createUsers(team.id, 2, 'gm-reopen');
const gmChannel = await adminClient.createGroupChannel([user.id, participants[0].id, participants[1].id]);
// # Open then close the group message conversation
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.gotoMessage(team.name, gmChannel.name);
await channelsPage.toBeVisible();
await channelsPage.closeGroupMessage();
await expect(channelsPage.sidebarLeft.item(participants[0].username)).toHaveCount(0);
// # Reopen the same GM from the Direct Messages modal. Searching by a single member's username
// only surfaces an option to start a new conversation with that one person, not the existing
// group message, so reselect both participants to resurface the existing conversation.
const modal = await channelsPage.openDirectChannelsModal();
await modal.selectUser(participants[0]);
await modal.selectUser(participants[1]);
await modal.goToChannel();
// * Verify the existing group message opens again
await channelsPage.centerView.header.toHaveTitle(participants[0].username);
await channelsPage.centerView.header.toHaveTitle(participants[1].username);
});
/**
* @objective Verify that a closed group message can be reopened via a saved message and via the Direct Messages modal.
*
* MM-T477 and MM-T479 are duplicates of MM-T476 and are covered by this test.
*/
test(
'MM-T476 MM-T477 MM-T479 closes and reopens a group message via saved messages and the DM modal',
{tag: '@direct_messages'},
async ({pw}) => {
// # Create the test user plus two more users on the team
const {user, team, adminClient, userClient} = await pw.initSetup();
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'gm');
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Create a group message and post a message, then save that message
const dmModal = await channelsPage.openDirectChannelsModal();
await dmModal.selectUser(member1);
await dmModal.selectUser(member2);
await dmModal.goToChannel();
const slug = getChannelSlugFromUrl(page);
const token = `message to save ${pw.random.id()}`;
await channelsPage.postMessage(token);
const savedPost = await channelsPage.getLastPost();
const savedPostId = await savedPost.getId();
await userClient.savePreferences(user.id, [
{user_id: user.id, category: 'flagged_post', name: savedPostId, value: 'true'},
]);
// # Close the group message conversation
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
// # Reopen the group message by jumping to the saved message
await channelsPage.globalHeader.openSavedMessages();
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
// * Verify the group message channel is reopened
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
// # Close it again and reopen it via the Direct Messages modal
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
const dmModal2 = await channelsPage.openDirectChannelsModal();
await dmModal2.selectUser(member1);
await dmModal2.selectUser(member2);
await dmModal2.goToChannel();
// * Verify the group message channel is reopened again
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
},
);

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {getChannelSlugFromUrl} from './helpers';
/**
* @objective Verify a mention posted in a group message creates an unread mention for the mentioned participant.
*/
test('MM-T469 Create a group message and post a mention for another user', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [sender, secondParticipant] = await adminClient.createUsers(team.id, 2, 'gm-mention');
const gmChannel = await adminClient.createGroupChannel([user.id, sender.id, secondParticipant.id]);
// # View another channel, then have a participant mention the test user in the GM
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await pw.stubNotification(page, 'granted');
await adminClient.createPost({
channel_id: gmChannel.id,
user_id: sender.id,
message: `@${user.username} Hello from GM`,
});
// * Verify a desktop notification fires for the mention
const notifications = await pw.waitForNotification(page, 1);
expect(notifications.length).toBeGreaterThanOrEqual(1);
// * Verify the group message becomes unread with a mention badge
await channelsPage.sidebarLeft.assertItemUnread(sender.username);
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
});
/**
* @objective Verify that a group message can be created with a mention, closed, and then recreated with the same members.
*
* MM-T480 covers the same behavior as MM-T466 and is covered by this test.
*/
test(
'MM-T466 MM-T480 creates a group message with a mention, closes it, and recreates it',
{tag: '@direct_messages'},
async ({pw}) => {
// # Create the test user plus two more users on the team
const {user, team, adminClient} = await pw.initSetup();
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'gm');
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Create a group message with the two members
const dmModal = await channelsPage.openDirectChannelsModal();
await dmModal.selectUser(member1);
await dmModal.selectUser(member2);
await dmModal.goToChannel();
// # Post a message that mentions one of the members
const token = `gm mention ${pw.random.id()}`;
await channelsPage.postMessage(`@${member1.username} ${token}`);
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(token);
// # Close the group message conversation
const slug = getChannelSlugFromUrl(page);
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
// # Recreate the group message with the same members
const dmModal2 = await channelsPage.openDirectChannelsModal();
await dmModal2.selectUser(member1);
await dmModal2.selectUser(member2);
await dmModal2.goToChannel();
// * Verify the same group message channel is reopened
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
},
);

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import type {Page} from '@playwright/test';
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that a group message whose channel has fallen out of the sidebar (because the user
* has more DMs/GMs than the configured "Number of direct messages to show" limit) still appears in the
* Direct Messages modal with its members fully loaded — i.e. with a non-zero member count and the
* participant usernames as its name.
*/
test(
"MM-65058 Direct Messages modal should load group members for GMs which haven't been loaded otherwise",
{tag: '@direct_messages'},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup({withDefaultProfileImage: false});
// Use a lower visible DM limit than the UI normally lets you use to speed up this test
const totalGms = 2;
const visibleLimit = 1;
// # Limit the user's visible DMs/GMs in the sidebar so one GM falls off the sidebar
await userClient.savePreferences(user.id, [
{
user_id: user.id,
category: 'sidebar_settings',
name: 'limit_visible_dms_gms',
value: visibleLimit.toString(),
},
]);
// # Create enough users to populate 11 GMs with unique users
const users = [];
for (let i = 0; i < totalGms * 2; i++) {
const user = await pw.createNewUserProfile(adminClient, {prefix: `mm65058gm${i}`});
users.push(user);
}
// # Log the user in and open the channels page
const {page, channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Create 11 GMs using the Direct Channels modal
const gmChannels = [];
for (let i = 0; i < totalGms; i++) {
const memberA = users[i * 2];
const memberB = users[i * 2 + 1];
// # Open the modal
const dialog = await channelsPage.openDirectChannelsModal();
// # Select the users and create the channel
await dialog.selectUser(memberA);
await dialog.selectUser(memberB);
await dialog.goToChannel();
// # Make a post in the channel to ensure that it has a last_post_at value
await channelsPage.postMessage(`gm message ${i}`);
// # Save the channel's information for later
gmChannels.push({
channel: await getCurrentChannel(page),
members: [memberA, memberB],
});
}
const targetGm = gmChannels[0];
const otherGms = gmChannels.slice(1);
// # Refresh the app and go back to Town Square
await channelsPage.goto(team.name, 'town-square');
// * Verify the target GM is not present in the sidebar to ensure that the sidebar hasn't loaded it
await expect(page.locator(`#sidebarItem_${targetGm.channel.name}`)).toHaveCount(0);
// * Wait until the other GMs are loaded and present in the sidebar
for (const otherGm of otherGms) {
const otherGmEntry = page.locator(`#sidebarItem_${otherGm.channel.name}`);
await expect(otherGmEntry).toHaveCount(1);
await expect(otherGmEntry).toContainText(gmChannelDisplayName(otherGm.members));
}
// * Verify that the members of the target GM haven't been loaded and the members of other GMs have
await assertChannelUsersNotLoaded(page, targetGm.channel.id);
for (const otherGm of otherGms) {
await assertChannelUsersLoaded(page, otherGm.channel.id, otherGm.members);
}
// # Open the Direct Messages modal again
const dialog = await channelsPage.openDirectChannelsModal();
// # Wait for the list to populate
const rows = dialog.container.locator('#multiSelectList .more-modal__row');
await expect.poll(async () => rows.count()).toBeGreaterThanOrEqual(totalGms);
// * Verify the modal contains an entry for every GM the user has, including the one that fell
// * out of the sidebar
for (const {channel, members} of gmChannels) {
// Each GM row renders the member usernames joined by ', '. We use the second member's
// username (which is unique per GM) to locate the corresponding row.
const usernameMarker = `@${members[1].username}`;
const gmRow = rows.filter({hasText: usernameMarker});
// * Verify the row is rendered
await expect(gmRow, `expected to find a row in the DM modal for GM ${channel.id}`).toHaveCount(1);
// * Verify the GM icon shows the correct member count (channel members minus current user)
await expect(
gmRow.locator('.more-modal__gm-icon'),
`expected GM ${channel.id} to show a member count of ${members.length}`,
).toHaveText(members.length.toString());
// * Verify the row's name section includes every participant's username
const nameContainer = gmRow.locator('.more-modal__name');
for (const participant of members) {
await expect(
nameContainer,
`expected GM ${channel.id} to include @${participant.username} in its name`,
).toContainText(`@${participant.username}`);
}
}
// * Double check that the members of the target GM have been loaded now
await assertChannelUsersLoaded(page, targetGm.channel.id, targetGm.members);
},
);
async function getCurrentChannel(page: Page) {
return page.evaluate<Channel>(
'store.getState().entities.channels.channels[store.getState().entities.channels.currentChannelId]',
);
}
function gmChannelDisplayName(users: UserProfile[]) {
return users
.toSorted((a, b) => {
return a.username.localeCompare(b.username, undefined, {numeric: true});
})
.map((user) => user.username)
.join(', ');
}
async function assertChannelUsersLoaded(page: Page, channelId: string, expectedUsers: UserProfile[]) {
// profilesInChannel contains Sets which aren't serializable for return from page.evaluate
const loadedIds = await page.evaluate(
`Array.from(store.getState().entities.users.profilesInChannel['${channelId}'])`,
);
await expect(loadedIds).toHaveLength(expectedUsers.length);
await expect(loadedIds).toEqual(expect.arrayContaining(expectedUsers.map((user) => user.id)));
}
async function assertChannelUsersNotLoaded(page: Page, channelId: string) {
const loadedIds = await page.evaluate(`store.getState().entities.users.profilesInChannel['${channelId}']`);
await expect(loadedIds).toBeUndefined();
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Page} from '@playwright/test';
/**
* Returns the current channel slug (the last path segment) from the page URL,
* e.g. the group-message name from `/team/messages/<slug>`.
*/
export function getChannelSlugFromUrl(page: Page): string {
return new URL(page.url()).pathname.split('/').pop() as string;
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify deleting the last post in a direct message keeps the user in that conversation.
*/
test('MM-T218 removes the last post without leaving the direct message', {tag: '@direct_messages'}, async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [otherUser] = await adminClient.createUsers(team.id, 1, 'dm-user');
await adminClient.createDirectChannel([user.id, otherUser.id]);
// # Log in and open the direct message with the other user
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, `@${otherUser.username}`);
await channelsPage.toBeVisible();
// # Post a message, then delete it from its post menu
await channelsPage.postMessage('Test');
const post = await channelsPage.getLastPost();
await post.hover();
await post.postMenu.openDotMenu();
await channelsPage.postDotMenu.deleteMenuItem.click();
await channelsPage.deletePostModal.toBeVisible();
await channelsPage.deletePostModal.confirm();
// * Verify the post is gone and the direct-message route remains open
await expect(post.container).not.toBeVisible();
await expect(page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
});