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

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,41 @@
// 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 the autocomplete highlight follows the mouse hover and the arrow keys.
*/
test('MM-T71 moves the autocomplete highlight with hover and arrow keys', {tag: '@messaging'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Type a partial emoji name to open the autocomplete with multiple suggestions
const {postCreate} = channelsPage.centerView;
await postCreate.writeMessage(':sm');
await expect(postCreate.suggestionList).toBeVisible();
const options = postCreate.suggestionOptions;
await expect.poll(() => options.count()).toBeGreaterThanOrEqual(3);
// # Hover over the second suggestion
await options.nth(1).hover();
// * Verify the hovered suggestion becomes highlighted
await expect(options.nth(1)).toHaveAttribute('data-testid', 'suggestion-selected');
// # Press ArrowDown
await postCreate.input.press('ArrowDown');
// * Verify the highlight follows the arrow down key to the next suggestion
await expect(options.nth(2)).toHaveAttribute('data-testid', 'suggestion-selected');
// # Press ArrowUp
await postCreate.input.press('ArrowUp');
// * Verify the highlight follows the arrow up key to the previous suggestion
await expect(options.nth(1)).toHaveAttribute('data-testid', 'suggestion-selected');
});

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify channel autocomplete closes when a second tilde turns channel syntax into strikethrough syntax.
*/
test(
'MM-T173 Edit a post with strikethrough and close channel autocomplete after the second tilde',
{tag: '@messaging'},
async ({pw}) => {
const {user, team} = await pw.initSetup();
const message = `Hello${pw.random.id()}`;
// # Log in, post a message, and open it for editing with ArrowUp
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(message);
await channelsPage.centerView.postCreate.input.focus();
await channelsPage.centerView.postCreate.input.press('ArrowUp');
await channelsPage.centerView.postEdit.toBeVisible();
const editInput = channelsPage.centerView.postEdit.input;
const suggestions = channelsPage.centerView.postEdit.suggestionList;
// # Type one tilde at the start of the message
await editInput.press('Home');
await editInput.type('~');
// * Verify channel autocomplete opens
await expect(suggestions).toBeVisible();
// # Type a second tilde at the start of the message
await editInput.press('Home');
await editInput.press('ArrowRight');
await editInput.type('~');
// * Verify channel autocomplete closes
await expect(suggestions).not.toBeVisible();
// # Type one tilde after a space at the end of the message
await editInput.press('End');
await editInput.type(' ~');
// * Verify channel autocomplete opens
await expect(suggestions).toBeVisible();
// # Type the second tilde and remove the preceding space
await editInput.press('End');
await editInput.type('~');
await expect(suggestions).not.toBeVisible();
await editInput.press('End');
await editInput.press('ArrowLeft');
await editInput.press('ArrowLeft');
await editInput.press('Backspace');
// * Verify autocomplete stays closed
await expect(suggestions).not.toBeVisible();
// # Save the edited message
await editInput.press('Enter');
// * Verify the original message is rendered as strikethrough
const editedPost = await channelsPage.getLastPost();
await editedPost.toHaveStrikethroughText(message);
},
);
/**
* @objective Verify channel autocomplete closes when the tilde that opened it is deleted.
*/
test('MM-T174 Autocomplete should close if tildes are deleted using backspace', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
// # Log in, post a message, and open it for editing with ArrowUp
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage('foo');
await channelsPage.centerView.postCreate.input.focus();
await channelsPage.centerView.postCreate.input.press('ArrowUp');
await channelsPage.centerView.postEdit.toBeVisible();
const editInput = channelsPage.centerView.postEdit.input;
const suggestions = channelsPage.centerView.postEdit.suggestionList;
// # Insert a tilde at the start of the message
await editInput.press('Home');
await editInput.type('~');
// * Verify channel autocomplete opens
await expect(suggestions).toBeVisible();
// # Delete the tilde with Backspace (the caret is already after the inserted tilde)
await editInput.press('Backspace');
// * Verify channel autocomplete closes
await expect(suggestions).not.toBeVisible();
// # Save the edit
await channelsPage.centerView.postEdit.sendMessage();
await channelsPage.centerView.postEdit.toNotBeVisible();
});

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify sending a message in a channel with unread content scrolls to the newly sent message.
*/
test('MM-T216 scrolls to the bottom when sending a message', {tag: '@messaging'}, async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [sender] = await adminClient.createUsers(team.id, 1, 'sender');
const testChannel = await adminClient.createPublicChannel(team.id, 'Channel Users Interactions');
await adminClient.addToChannel(user.id, testChannel.id);
await adminClient.addToChannel(sender.id, testChannel.id);
// # Log in, visit the test channel once, then move to Off-Topic
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, testChannel.name);
await channelsPage.toBeVisible();
await channelsPage.postMessage('Hello');
await channelsPage.sidebarLeft.goToItem('off-topic');
// # Have another user post a tall message in the test channel, then post in Off-Topic
await adminClient.createPost({
channel_id: testChannel.id,
user_id: sender.id,
message: `I'm messaging!${'\n2'.repeat(30)}`,
});
await channelsPage.postMessage('Hello');
await (await channelsPage.getLastPost()).toContainText('Hello');
// # Return to the test channel
await channelsPage.sidebarLeft.goToItem(testChannel.name);
// * Verify the new-message separator is visible
await channelsPage.centerView.toHaveNewMessagesSeparator();
// # Send a message in the current channel
await channelsPage.postMessage('message123');
// * Verify the newly sent message is the visible last post
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(lastPost.container).toBeInViewport();
await expect(lastPost.messageText).toHaveText('message123');
});

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {test} from '@mattermost/playwright-lib';
/**
* @objective Verify CJK text can be posted as both a channel message and a thread reply.
*/
test('MM-T182 Typing using CJK keyboard', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const message = '안녕하세요';
const reply = '닥터 카레브';
// # Log in and post a CJK message
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(message);
// * Verify the posted message contains the CJK text
const rootPost = await channelsPage.getLastPost();
await rootPost.toContainText(message);
// # Open the post thread and send a CJK reply
await rootPost.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.sidebarRight.postMessage(reply);
// * Verify the posted reply contains the CJK text
const lastReply = await channelsPage.sidebarRight.getLastPost();
await lastReply.toContainText(reply);
});

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {test} from '@mattermost/playwright-lib';
/**
* @objective Verify a user can view a message's edit history and restore a previous version, and that
* cancelling the restore confirmation keeps the edited version.
*/
test('MM-T5537 views message edit history and restores a previous version', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message
const message = 'This is a message to post and then edit';
await channelsPage.postMessage(message);
// # Open the inline edit box for the last post, append text, and save
await channelsPage.centerView.postCreate.input.focus();
await page.keyboard.press('ArrowUp');
await channelsPage.centerView.postEdit.toBeVisible();
await channelsPage.centerView.postEdit.input.press('End');
await channelsPage.centerView.postEdit.input.pressSequentially('. I have now edited this message.');
await channelsPage.centerView.postEdit.sendMessage();
// * Verify the post shows the edited text
const editedMessage = `${message}. I have now edited this message.`;
const post = await channelsPage.getLastPost();
await post.toContainText(editedMessage);
// # Open the edit history from the "Edited" indicator
await post.openEditHistory();
await channelsPage.sidebarRight.toBeVisible();
// # Open the restore confirmation for the previous version, then cancel it
await channelsPage.sidebarRight.restorePreviousPostVersionIcon.first().click();
const restoreModal = channelsPage.centerView.postEdit.restorePostConfirmationDialog;
await restoreModal.toBeVisible();
await restoreModal.cancelButton.click();
await restoreModal.notToBeVisible();
// * Verify the edited version is still displayed
await post.toContainText(editedMessage);
// # Restore the previous version and confirm
await channelsPage.sidebarRight.restorePreviousPostVersionIcon.first().click();
await restoreModal.toBeVisible();
await restoreModal.confirmRestore();
await restoreModal.notToBeVisible();
// * Verify the post is reverted to the original message
const restoredPost = await channelsPage.getLastPost();
await restoredPost.toContainText(message);
await restoredPost.toNotContainText('I have now edited this message');
});

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify selecting an emoji from the picker inserts it at the current caret position.
*/
test('MM-T95 Selecting an emoji from emoji picker should insert it at the cursor position', async ({pw}) => {
const {user, team} = await pw.initSetup();
// # Log in and place the caret between "Hello" and "World"
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.centerView.postCreate.writeMessage('HelloWorld!');
for (let i = 0; i < 'World!'.length; i++) {
await channelsPage.centerView.postCreate.input.press('ArrowLeft');
}
// # Select the grinning emoji from the picker
await channelsPage.centerView.postCreate.openEmojiPicker();
await channelsPage.emojiGifPickerPopup.toBeVisible();
await channelsPage.emojiGifPickerPopup.clickEmoji('grinning');
// * Verify the emoji was inserted at the caret and can be posted
await expect(channelsPage.centerView.postCreate.input).toHaveValue('Hello 😀 World!');
await channelsPage.centerView.postCreate.sendMessage();
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText('Hello 😀 World!');
});
/**
* @objective Verify an emoji followed by punctuation renders as an emoji without separating the punctuation.
*/
test('MM-T222 Emoji characters followed by punctuation', async ({pw}) => {
const {user, team} = await pw.initSetup();
// # Post an emoticon followed by punctuation
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(':)=');
// * Verify the emoticon renders and the punctuation remains immediately adjacent, with no stray
// whitespace inserted between the rendered emoji and the punctuation
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.emoticon.first()).toHaveAttribute('aria-label', ':slightly_smiling_face:');
await expect(lastPost.messageText.first()).toHaveText(':)=');
});
/**
* @objective Verify that a leading colon is ignored when searching in the emoji picker.
*/
test('MM-T156 ignores a leading colon when searching the emoji picker', {tag: '@messaging'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message and open the emoji reaction picker on it
await channelsPage.postMessage(`emoji picker search ${pw.random.id()}`);
const post = await channelsPage.getLastPost();
await post.openReactionPicker();
// * Verify the emoji picker is open
await channelsPage.reactionEmojiPicker.toBeVisible();
// # Search using a leading colon
await channelsPage.reactionEmojiPicker.searchEmoji(':tax');
// * Verify the leading colon is ignored and the taxi emoji is returned
await expect(channelsPage.reactionEmojiPicker.getEmoji('taxi')).toBeVisible();
});

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify an inline Markdown image opens in the image preview.
*/
test('MM-T187 Inline markdown images open preview window', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const imageUrl =
'https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/image-small-height.png';
// # Log in and post an inline Markdown image
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(`Hello ![test image](${imageUrl})`);
// * Verify the inline image is visible
const imagePost = await channelsPage.getLastPost();
await expect(imagePost.getInlineImage('test image')).toBeVisible();
// # Click the inline image
await imagePost.openInlineImagePreview('test image');
// * Verify the image opens in the preview
await channelsPage.imagePreviewModal.toBeVisible();
});
/**
* @objective Verify an inline Markdown image nested in a link renders correctly and points to the external destination.
*/
test('MM-T188 Inline markdown image that is a link, opens the link', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const linkUrl = 'https://www.google.com';
const imageUrl =
'https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/mattermost-icon_128x128.png';
const label = 'Build Status';
// # Log in and post a linked inline Markdown image
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(`[![${label}](${imageUrl})](${linkUrl})`);
// * Verify the image link has the correct destination and opens in a new tab
const imagePost = await channelsPage.getLastPost();
const imageLink = imagePost.getInlineImageLink(label);
await expect(imageLink).toHaveAttribute('href', linkUrl);
await expect(imageLink).toHaveAttribute('target', '_blank');
// * Verify the linked image is visible with the expected source and alt text
const linkedImage = imagePost.getInlineImage(label);
await expect(linkedImage).toBeVisible();
await expect(linkedImage).toHaveAttribute('src', imageUrl);
});

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
async function emojiHeight(post: {emoticon: {boundingBox: () => Promise<{height: number} | null>}}): Promise<number> {
const box = await post.emoticon.boundingBox();
if (!box) {
throw new Error('Expected the emoji to have a bounding box');
}
return box.height;
}
/**
* @objective Verify an emoji-only message renders as a jumbo (large) emoji, and that leading/trailing
* whitespace around the emoji does not stop it from rendering jumbo.
*/
test(
'MM-T2179 renders emoji-only messages as jumbo even when surrounded by whitespace',
{tag: '@messaging'},
async ({pw}) => {
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
// # Post an emoji inline with text (renders at the normal inline size)
await channelsPage.postMessage('jumbo test :taco:');
const inlineHeight = await emojiHeight(await channelsPage.getLastPost());
// # Post the same emoji on its own surrounded by whitespace
await channelsPage.postMessage(' :taco: ');
const jumboHeight = await emojiHeight(await channelsPage.getLastPost());
// * Verify the whitespace-surrounded emoji-only message renders larger (jumbo) than the inline emoji
expect(jumboHeight).toBeGreaterThan(inlineHeight);
},
);

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {setupFileServer, test} from '@mattermost/playwright-lib';
let fileServerUrl: string;
setupFileServer().then((serverUrl) => {
fileServerUrl = serverUrl;
});
/**
* @objective Verify collapsing a link preview is user-specific while removing it removes the preview for other users.
*/
test('MM-T199 Removing a link preview removes it from the views of other users', {tag: '@messaging'}, async ({pw}) => {
const {adminClient, adminUser, userClient, team, user} = await pw.initSetup();
// Use the in-repo OpenGraph fixture instead of a live third-party URL so preview
// generation does not depend on an external site remaining scrapeable.
await adminClient.patchConfig({
ServiceSettings: {
EnableLinkPreviews: true,
AllowedUntrustedInternalConnections: new URL(fileServerUrl).hostname,
},
});
await Promise.all([
userClient.savePreferences(user.id, [
{user_id: user.id, category: 'display_settings', name: 'link_previews', value: 'true'},
{user_id: user.id, category: 'display_settings', name: 'collapse_previews', value: 'false'},
]),
adminClient.savePreferences(adminUser.id, [
{user_id: adminUser.id, category: 'display_settings', name: 'link_previews', value: 'true'},
{user_id: adminUser.id, category: 'display_settings', name: 'collapse_previews', value: 'false'},
]),
]);
const message = `${fileServerUrl}/opengraph-huge.html`;
// # Log in as the test user, post a link, and wait for its preview
const {channelsPage: userChannelsPage, page: userPage} = await pw.testBrowser.login(user);
await userChannelsPage.goto(team.name, 'off-topic');
await userChannelsPage.toBeVisible();
const postResponsePromise = userPage.waitForResponse(
(response) => response.url().endsWith('/api/v4/posts') && response.request().method() === 'POST',
);
await userChannelsPage.postMessage(message);
const postId = ((await (await postResponsePromise).json()) as {id: string}).id;
const userPost = await userChannelsPage.centerView.getPostById(postId);
const userPreview = userPost.getLinkPreview();
// * Verify the link preview and its image are shown
await userPreview.toBeVisible(pw.duration.half_min);
await userPreview.toHaveExpandedImage();
// # Log in as the other user and visit the same channel
const {channelsPage: adminChannelsPage, page: adminPage} = await pw.testBrowser.login(adminUser);
await adminChannelsPage.goto(team.name, 'off-topic');
await adminChannelsPage.toBeVisible();
const adminPost = await adminChannelsPage.centerView.getPostById(postId);
const adminPreview = adminPost.getLinkPreview();
// * Verify the other user also sees the expanded preview
await adminPreview.toBeVisible(pw.duration.half_min);
await adminPreview.toHaveExpandedImage();
// # Collapse the preview as the test user
await userPreview.hideImage();
// # Reload the channel as the other user
await adminPage.reload();
await adminChannelsPage.toBeVisible();
// * Verify the preview remains expanded for the other user
await adminPreview.toHaveExpandedImage();
// # Remove the link preview as the test user
await userPage.reload();
await userChannelsPage.toBeVisible();
await userPreview.remove();
// # Reload the channel as the other user
await adminPage.reload();
await adminChannelsPage.toBeVisible();
// * Verify the preview is also removed for the other user
await adminPreview.toNotBeVisible();
});

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {test} from '@mattermost/playwright-lib';
/**
* @objective Verify deleting a root post while another user drafts a reply removes the original message and does not send the draft.
*/
test(
'MM-T113 Delete a message during reply and show the other user a message deleted placeholder',
{tag: '@messaging'},
async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [author] = await adminClient.createUsers(team.id, 1, 'deleted-reply-author');
const channel = await adminClient.createPublicChannel(team.id, 'Deleted While Replying');
await adminClient.addToChannel(user.id, channel.id);
await adminClient.addToChannel(author.id, channel.id);
const message = `root-${pw.random.id()}`;
const draftMessage = `draft-${pw.random.id()}`;
const root = await adminClient.createPost({channel_id: channel.id, user_id: author.id, message});
// # Log in as the other user, open the root thread, and type an unsent draft
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const centerRoot = await channelsPage.centerView.getPostById(root.id);
await centerRoot.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.sidebarRight.postCreate.writeMessage(draftMessage);
const rhsRoot = await channelsPage.sidebarRight.getPostById(root.id);
// # Delete the root post as its author
await adminClient.deletePost(root.id);
// * Verify the other user sees a deleted-message placeholder without the original message
await centerRoot.toBeDeleted(message);
await rhsRoot.toBeDeleted(message);
// * Verify the draft was not sent as a reply
const lastRhsPost = await channelsPage.sidebarRight.getLastPost();
await lastRhsPost.toNotContainText(draftMessage);
// # Log in as the author and revisit the channel
const {channelsPage: authorChannelsPage} = await pw.testBrowser.login(author);
await authorChannelsPage.goto(team.name, channel.name);
await authorChannelsPage.toBeVisible();
// * Verify the deleted post's original message is absent
await authorChannelsPage.toNotContainText(message);
},
);

View File

@@ -0,0 +1,75 @@
// 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 the RHS thread refreshes replies when the center channel has changed.
*/
test('MM-T94 RHS fetches messages on reconnect while a different channel is in center', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const [author] = await adminClient.createUsers(team.id, 1, 'reconnect-author');
const threadChannel = await adminClient.createPublicChannel(team.id, 'RHS Reconnect');
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
await adminClient.addToChannel(user.id, threadChannel.id);
await adminClient.addToChannel(author.id, threadChannel.id);
// # Open a thread in the RHS and add an initial reply
const root = await adminClient.createPost({
channel_id: threadChannel.id,
user_id: author.id,
message: 'reconnect root',
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await pw.mockWebsockets(page);
await channelsPage.goto(team.name, threadChannel.name);
await channelsPage.toBeVisible();
await pw.connectWebsockets(page);
await (await channelsPage.centerView.getPostById(root.id)).reply();
await channelsPage.sidebarRight.postMessage('def');
// # Change the center channel, then go offline and have another user reply while disconnected
await channelsPage.sidebarLeft.goToItem(offTopic.name);
await pw.closeWebsockets(page);
await adminClient.createPost({channel_id: threadChannel.id, user_id: author.id, message: 'ghi', root_id: root.id});
await pw.wait(duration.four_sec);
// * Verify the reply posted while disconnected has not reached the RHS
await expect(channelsPage.sidebarRight.container).toContainText('def');
await expect(channelsPage.sidebarRight.container).not.toContainText('ghi');
// # Reconnect and nudge the client with some activity so it resyncs missed messages
await pw.connectWebsockets(page);
await channelsPage.sidebarLeft.goToItem('town-square');
await channelsPage.postMessage('nudge 1');
await channelsPage.sidebarLeft.goToItem(offTopic.name);
await channelsPage.postMessage('nudge 2');
await pw.wait(duration.two_sec);
// * Verify the RHS fetches both replies after reconnecting
await channelsPage.sidebarRight.toContainText('def');
await channelsPage.sidebarRight.toContainText('ghi');
});
/**
* @objective Verify channel short-linking still works when the channel reference is surrounded by brackets.
*/
test('MM-T175 Channel short-linking still works when placed in brackets', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const linkedChannel = await adminClient.createPublicChannel(team.id, 'Shortlink Target');
await adminClient.addToChannel(user.id, linkedChannel.id);
// # Post a bracketed channel shortlink from a different channel
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(`(~${linkedChannel.name})`);
// # Click the rendered channel link
const lastPost = await channelsPage.getLastPost();
await lastPost.getLink(linkedChannel.display_name).click();
// * Verify the linked channel opens
await expect(page).toHaveURL(`/${team.name}/channels/${linkedChannel.name}`);
await channelsPage.centerView.header.toHaveTitle(linkedChannel.display_name);
});

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify an overlong thread reply shows the character-count warning and is not posted.
*/
test('MM-T106 Webapp: Message too long warning text', {tag: '@messaging'}, async ({pw}) => {
const {user, team, adminClient} = await pw.initSetup();
// MaxPostSize is a computed client-config value, so read it rather than hardcoding.
const clientConfig = await adminClient.getClientConfig();
const maxReplyLength = parseInt(clientConfig.MaxPostSize, 10);
const validReply = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ';
const tooLongReply = validReply.repeat(maxReplyLength / validReply.length + 1);
// # Log in, open Off-Topic, post a root message, and open its thread
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
await channelsPage.postMessage(`Hello ${pw.random.id()}`);
const rootPost = await channelsPage.getLastPost();
await rootPost.reply();
await channelsPage.sidebarRight.toBeVisible();
// # Post a valid reply
await channelsPage.sidebarRight.postMessage(validReply);
// * Verify no overlong-message warning is shown
await channelsPage.sidebarRight.postCreate.toNotHaveMessageTooLongWarning();
// # Enter an overlong reply and try to send it
await channelsPage.sidebarRight.postCreate.input.fill(tooLongReply);
// * Verify the character-count warning is visible without replacing the reply textbox
await channelsPage.sidebarRight.postCreate.toHaveMessageTooLongWarning(tooLongReply.length, maxReplyLength);
await expect(channelsPage.sidebarRight.postCreate.input).toBeVisible();
await channelsPage.sidebarRight.postCreate.input.press('Enter');
// * Verify the warning remains and the last posted reply is still the valid reply
await channelsPage.sidebarRight.postCreate.toHaveMessageTooLongWarning(tooLongReply.length, maxReplyLength);
const lastReply = await channelsPage.sidebarRight.getLastPost();
await lastReply.toContainText(validReply);
});

View File

@@ -0,0 +1,35 @@
// 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 pasting with Ctrl/Cmd+Shift+V skips Mattermost's own paste-formatting
* logic (e.g. auto-converting a pasted HTML table into markdown table syntax), unlike a normal
* paste which applies it.
*/
test('MM-T1434 pasting with Ctrl/Cmd+Shift+V skips Mattermost paste formatting', {tag: '@messaging'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const html = '<table><tr><td>foo</td><td>bar</td></tr></table>';
const plainText = 'foo\tbar';
const {postCreate} = channelsPage.centerView;
const {input} = postCreate;
// # Paste the HTML table normally
await postCreate.pasteHtml(html, plainText);
// * Verify Mattermost formatted it into markdown table syntax
await expect(input).toHaveValue('| foo | bar |\n| --- | --- |\n');
// # Clear the input, then paste the same HTML table with the "without formatting" flag
await input.fill('');
await postCreate.pasteHtml(html, plainText, {withoutFormatting: true});
// * Verify the plain-text fallback was inserted without markdown table formatting
await expect(input).toHaveValue(plainText);
await expect(input).not.toHaveValue(/\|/);
});

View File

@@ -0,0 +1,86 @@
// 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 clicking a post timestamp in the RHS or center channel highlights the post in the center channel.
*/
test('MM-T176 highlights the center post when its timestamp is clicked', {tag: ['@messaging']}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message and open its thread, then reply in the RHS
await channelsPage.postMessage(`timestamp highlight ${pw.random.id()}`);
const rootPost = await channelsPage.getLastPost();
const rootId = await rootPost.getId();
await rootPost.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.sidebarRight.postMessage('Reply to test timestamp click');
// # Click the root post timestamp in the RHS
await page.locator(`#RHS_ROOT_time_${rootId}`).click();
// * Verify the matching post is highlighted in the center channel
const centerPost = await channelsPage.centerView.getPostById(rootId);
await expect(centerPost.container).toHaveClass(/post--highlight/);
// # Close the RHS and wait for the highlight to fade
await channelsPage.sidebarRight.close();
await expect(centerPost.container).not.toHaveClass(/post--highlight/);
// # Click the same post timestamp in the center channel
await page.locator(`#CENTER_time_${rootId}`).click();
// * Verify the post is highlighted again in the center channel
await expect(centerPost.container).toHaveClass(/post--highlight/);
});
/**
* @objective Verify that following a permalink to the first post in a channel loads the channel start without an endless loading indicator above it.
*/
test('MM-T3308 follows a permalink to the first post without endless loading', {tag: '@messaging'}, async ({pw}) => {
const messageCount = 10;
const base = `permalink-${pw.random.id()}`;
// # Create and log in as a test user, and create a private channel with several posts
const {user, team, adminClient} = await pw.initSetup();
const privateChannel = await adminClient.createPrivateChannel(team.id, 'Permalink First');
await adminClient.addToChannel(user.id, privateChannel.id);
let firstPostId = '';
for (let i = 1; i <= messageCount; i++) {
const created = await adminClient.createPost({channel_id: privateChannel.id, message: `${base}-${i}`});
if (i === 1) {
firstPostId = created.id;
}
}
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
// # Post a permalink to the first (oldest) post into the public channel
const permalink = `${new URL(page.url()).origin}/${team.name}/pl/${firstPostId}`;
await channelsPage.postMessage(permalink);
await channelsPage.centerView.waitUntilLastPostContains(permalink);
// # Click the permalink in the posted message
const permalinkPost = await channelsPage.getLastPost();
await permalinkPost.body.getByRole('link', {name: permalink}).click();
// * Verify navigation lands in the target private channel
await expect
.poll(() => page.url(), {timeout: pw.duration.ten_sec})
.toContain(`/${team.name}/channels/${privateChannel.name}`);
// * Verify the channel start intro is shown, proving the top loaded without an endless loading indicator
await expect(page.getByText(`This is the start of ${privateChannel.display_name}`)).toBeVisible();
// * Verify the most recent post is the last message that was created
const lastPost = await channelsPage.getLastPost();
await lastPost.toContainText(`${base}-${messageCount}`);
});

View File

@@ -0,0 +1,51 @@
// 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 long reply draft keeps the expanded reply textbox height after switching threads.
*/
test('MM-T212 keeps a long draft expanded in the reply input', {tag: '@messaging'}, async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
const firstPost = await adminClient.createPost({
channel_id: offTopic.id,
user_id: user.id,
message: 'test post 1',
});
const secondPost = await adminClient.createPost({
channel_id: offTopic.id,
user_id: user.id,
message: 'test post 2',
});
// # Log in, open the latest post's thread, and record the reply textbox's initial height
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, offTopic.name);
await channelsPage.toBeVisible();
await (await channelsPage.centerView.getPostById(secondPost.id)).reply();
await channelsPage.sidebarRight.toBeVisible();
const replyInput = channelsPage.sidebarRight.postCreate.input;
await expect(replyInput).toHaveCSS('height', '46px');
const initialBox = await replyInput.boundingBox();
expect(initialBox).not.toBeNull();
// # Enter a reply draft with enough line breaks to expand the textbox
await channelsPage.sidebarRight.postCreate.writeMessage(`test${'\n'.repeat(8)}test`);
// * Verify the reply textbox grows to more than twice its original height
const expandedBox = await replyInput.boundingBox();
expect(expandedBox).not.toBeNull();
expect(expandedBox!.height).toBeGreaterThan(initialBox!.height * 2);
// # Switch to the first post's thread, then return to the latest post's thread
await (await channelsPage.centerView.getPostById(firstPost.id)).reply();
await (await channelsPage.centerView.getPostById(secondPost.id)).reply();
// * Verify the long draft and expanded textbox height are restored
await expect(replyInput).toHaveValue(`test${'\n'.repeat(8)}test`);
const restoredBox = await replyInput.boundingBox();
expect(restoredBox).not.toBeNull();
expect(restoredBox!.height).toBeGreaterThan(initialBox!.height * 2);
});

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {test} from '@mattermost/playwright-lib';
import type {PlaywrightExtended} from '@mattermost/playwright-lib';
/**
* @objective Verify a system message does not show a user availability status in standard message display.
*/
test('MM-T427_1 system message has no status in standard view', async ({pw}) => {
await verifySystemMessageHasNoStatus(pw, 'clean');
});
/**
* @objective Verify a system message does not show a user availability status in compact message display.
*/
test('MM-T427_2 system message has no status in compact view', async ({pw}) => {
await verifySystemMessageHasNoStatus(pw, 'compact');
});
async function verifySystemMessageHasNoStatus(pw: PlaywrightExtended, messageDisplay: 'clean' | 'compact') {
// # Create a channel header system message and set the user's message display preference
const {user, team, adminClient, userClient} = await pw.initSetup();
const channel = await userClient.getChannelByName(team.id, 'town-square');
await userClient.createPost({channel_id: channel.id, message: 'Test for no status of a system message'});
const newHeader = `Updated header ${pw.random.id()}`;
await adminClient.patchChannel(channel.id, {header: newHeader});
await userClient.savePreferences(user.id, [
{
user_id: user.id,
category: 'display_settings',
name: 'message_display',
value: messageDisplay,
},
]);
// # Open the channel and locate the resulting system message
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
await channelsPage.centerView.waitUntilLastPostContains(newHeader);
const systemPost = await channelsPage.getLastPost();
// * Verify the post is styled only as a system message and has no availability status icon
await systemPost.toBeSystemMessageContaining(newHeader);
}