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,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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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 `);
|
||||
|
||||
// * 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(`[](${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);
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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(/\|/);
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user