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,325 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @collapsed_reply_threads
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import type {PostMessageResp} from '@/support/task_commands';
import {spyNotificationAs} from '@/support/notification';
describe('CRT Desktop notifications', () => {
let testTeam: Team;
let testChannelUrl: string;
let testChannelId: string;
let testChannelName: string;
let receiver: UserProfile;
let sender: UserProfile;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_on',
},
});
cy.apiCreateUser().then(({user}) => {
sender = user;
});
cy.apiInitSetup().then(({team, channel, user, channelUrl}) => {
testTeam = team;
receiver = user;
testChannelUrl = channelUrl;
testChannelId = channel.id;
testChannelName = channel.display_name;
cy.apiAddUserToTeam(testTeam.id, sender.id).then(() => {
cy.apiAddUserToChannel(testChannelId, sender.id);
});
// # Login as receiver
cy.apiLogin(receiver);
});
});
it('MM-T4417_1 Trigger notifications on all replies when channel setting is checked', () => {
// # Visit channel
cy.visit(testChannelUrl);
cy.uiOpenChannelMenu('Notification Preferences');
// # Click on Mute Channel to Unmute Channel
cy.findByText('Mute channel').should('be.visible').click({force: true});
// * Verify that channel is muted alert is visible
cy.findByText('This channel is muted').should('be.visible');
// # Save the changes
cy.findByText('Save').should('be.visible').click();
// Setup notification spy
spyNotificationAs('notifySpy', 'granted');
// # Set users notification settings
cy.uiOpenChannelMenu('Notification Preferences');
// # Click on Mute Channel to Unmute Channel
cy.findByText('Mute channel').should('be.visible').click({force: true});
// # Click "Desktop Notifications"
cy.findByText('Desktop Notifications').should('be.visible');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-all').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-all').should('be.checked');
cy.get('#desktopNotification-mention').should('be.visible').click().then(() => {
cy.get('[data-testid="desktopReplyThreads"]').should('be.checked');
cy.get('[data-testid="desktopReplyThreads"]').should('be.visible').click();
cy.get('[data-testid="desktopReplyThreads"]').should('not.be.checked');
});
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-mention').should('be.checked');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-none').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-none').should('be.checked');
// # Save the changes
cy.findByText('Save').should('be.visible').click();
// # Set users notification settings
cy.uiOpenChannelMenu('Notification Preferences');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-none').should('be.checked');
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-all').scrollIntoView().should('be.visible').click();
// # Save the changes
cy.findByText('Save').should('be.visible').click();
// # Post a root message as other user
cy.postMessageAs({sender, message: 'This is a not followed root message', channelId: testChannelId, rootId: ''}).then(({id: postId}) => {
// # Switch to town-square so that unread notifications in test channel may be triggered
cy.uiClickSidebarItem('town-square');
// # Post a message in unfollowed thread as another user
cy.postMessageAs({sender, message: 'This is a reply to the unfollowed thread', channelId: testChannelId, rootId: postId});
// * Verify stub was not called for unfollowed thread
cy.get('@notifySpy').should('not.be.called');
});
// # Visit channel
cy.visit(testChannelUrl);
// Setup notification spy
spyNotificationAs('notifySpy', 'granted');
// # Post a message
cy.postMessage('Hi there, this is a root message');
// # Get post id of message
cy.getLastPostId().then((postId) => {
// # Switch to town-square so that unread notifications in test channel may be triggered
cy.uiClickSidebarItem('town-square');
// # Post a message in original thread as another user
const message = 'This is a reply to the root post';
cy.postMessageAs({sender, message, channelId: testChannelId, rootId: postId});
// * Verify stub was called with correct title and body
cy.get('@notifySpy').should('have.been.calledWithMatch', `Reply in ${testChannelName}`, (args: {body: string}) => {
expect(args.body, `Notification body: "${args.body}" should match: "${message}"`).to.equal(`@${sender.username}: ${message}`);
return true;
});
// # Cleanup
cy.apiDeletePost(postId);
});
});
it('MM-T4417_2 Click on sameMobileSettingsDesktop and check if additional settings still appears', () => {
cy.visit(testChannelUrl);
// # Open channel's notification preferences
cy.uiOpenChannelMenu('Notification Preferences');
// * As per previous conditions the mobile and desktop settings should be the same
cy.get('[data-testid="sameMobileSettingsDesktop"]').scrollIntoView().should('be.checked');
// * Verify that Notify me about section of mobile settings is not visible
cy.get('[data-testid="mobile-notify-me-radio-section"]').should('not.exist');
// # Now uncheck the sameMobileSettingsDesktop so that mobile and desktop settings are different
cy.get('[data-testid="sameMobileSettingsDesktop"]').scrollIntoView().should('be.visible').click();
// * Verify that Notify me about section of mobile settings is visible
cy.get('[data-testid="mobile-notify-me-radio-section"]').should('be.visible').scrollIntoView().within(() => {
cy.findByText('Notify me about…').should('be.visible');
// # Click on mentions option
cy.get('[data-testid="MobileNotification-mention"]').should('be.visible').click();
});
// * Verify that Thread reply notifications section of mobile settings is visible
cy.get('[data-testid="mobile-reply-threads-checkbox-section"]').should('be.visible').scrollIntoView().within(() => {
cy.findByText('Notify me about replies to threads I’m following').should('be.visible');
});
// # Close the modal
cy.get('body').type('{esc}');
});
it('MM-T4417_3 Trigger notifications only on mention replies when channel setting is unchecked', () => {
// # Visit the test channel
cy.visit(testChannelUrl);
// # Setup notification spy
spyNotificationAs('notifySpy1', 'granted');
// # Open channel's notification preferences for test channel
cy.uiOpenChannelMenu('Notification Preferences');
// # Select "Mentions, direct messages, and keywords only" as notify me about option
cy.get('#desktopNotification-mention').scrollIntoView().should('be.visible').click();
// # Unselect "Notify me about replies to threads I’m following"
cy.get('[data-testid="desktopReplyThreads"]').scrollIntoView().should('be.visible').then(($el) => {
if ($el.is(':checked')) {
cy.wrap($el).click();
}
});
// # Select notification checkbox active
cy.get('[data-testid="desktopNotificationSoundsCheckbox"]').scrollIntoView().should('be.visible').then(($el) => {
if (!$el.is(':checked')) {
cy.wrap($el).click();
}
});
// # Select a notification sound from dropdown
cy.get('#desktopNotificationSoundsSelect').scrollIntoView().should('be.visible').click();
cy.findByText('Crackle').should('be.visible').click();
// # Save the changes
cy.findByText('Save').should('be.visible').click();
// # Go to the town-square channel
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Post a root message as other user in the test channel
cy.postMessageAs({sender, message: 'This is the root message which will not have a at-mention in thread', channelId: testChannelId, rootId: ''}).then(({id: postId}) => {
// # Post a message in the thread without at-mention
cy.postMessageAs({sender, message: 'Reply without at-mention', channelId: testChannelId, rootId: postId}).then(() => {
// * Verify Notification stub was not called for threads which does not have at-mention as per channel settings
cy.get('@notifySpy1').should('not.be.called');
});
// # Cleanup
cy.apiDeletePost(postId);
});
// Setup another notification spy
spyNotificationAs('notifySpy2', 'granted');
// # Post another message in the test channel
cy.postMessageAs({sender, message: 'This is another root message which will have a at-mention in thread', channelId: testChannelId, rootId: ''}).then(({id: postId}) => {
const message = `Reply with at-mention @${receiver.username}`;
// # Post a message in the thread with at-mention
cy.postMessageAs({sender, message, channelId: testChannelId, rootId: postId});
// * Verify Notification stub was called with correct title and body with at-mention as per channel settings
cy.get('@notifySpy2').should('have.been.calledWithMatch', `Reply in ${testChannelName}`, (args: {body: string}) => {
expect(args.body, `Notification body: "${args.body}" should match: "${message}"`).to.equal(`@${sender.username}: ${message}`);
return true;
});
// # Cleanup
cy.apiDeletePost(postId);
});
});
it('When a reply is deleted in open channel, the notification should be cleared', () => {
// # Visit channel
cy.visit(testChannelUrl);
// # Post a root message as other user
cy.postMessageAs({sender, message: 'a thread', channelId: testChannelId, rootId: ''});
// # Get post id of message
cy.getLastPostId().then((postId) => {
// # Post a reply to the thread, which will trigger a follow
cy.postMessageAs({sender: receiver, message: 'following the thread', channelId: testChannelId, rootId: postId});
// # Post a reply to the thread
cy.postMessageAs({sender, message: 'a reply', channelId: testChannelId, rootId: postId}).as('reply');
// # Post a mention reply to the thread
cy.postMessageAs({sender, message: `@${receiver.username} mention reply`, channelId: testChannelId, rootId: postId}).
as('replyMention');
// * Verify there is a notification
cy.get('#sidebarItem_threads #unreadMentions').should('exist').and('have.text', '1');
// # Delete the replies
const replies = ['@reply', '@replyMention'];
for (const reply of replies) {
cy.get<PostMessageResp>(reply).then(({id}) => {
cy.apiDeletePost(id);
});
}
// * Verify there is no notification
cy.get('#sidebarItem_threads #unreadMentions').should('not.exist');
// # Cleanup
cy.apiDeletePost(postId);
});
});
it('When a reply is deleted in DM channel, the notification should be cleared', () => {
cy.apiCreateDirectChannel([receiver.id, sender.id]).then(({channel: dmChannel}) => {
// # Visit channel
cy.visit(`/${testTeam.name}/messages/@${sender.username}`);
// # Post a root message as other user
cy.postMessageAs({sender, message: 'a thread', channelId: dmChannel.id, rootId: ''}).as('rootPost');
// # Get post id of message
cy.get<PostMessageResp>('@rootPost').then(({id: rootId}) => {
// # Post a reply to the thread, which will trigger a follow
cy.postMessageAs({sender: receiver, message: 'following the thread', channelId: dmChannel.id, rootId});
// # Post a reply to the thread
cy.postMessageAs({sender, message: 'a reply', channelId: dmChannel.id, rootId}).as('reply');
// # Post a mention reply to the thread
cy.postMessageAs({sender, message: `@${receiver.username} mention reply`, channelId: dmChannel.id, rootId}).
as('replyMention');
// * Verify there is a notification
cy.get('#sidebarItem_threads #unreadMentions').should('exist');
// # Delete the replies
const replies = ['@reply', '@replyMention'];
for (const reply of replies) {
cy.get<PostMessageResp>(reply).then(({id}) => {
cy.apiDeletePost(id);
});
}
// * Verify there is no notification
cy.get('#sidebarItem_threads #unreadMentions').should('not.exist');
// # Cleanup
cy.apiDeletePost(rootId);
});
});
});
});

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Team} from '@mattermost/types/teams';
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and visit channel
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
testTeam = team;
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
});
});
it('MM-T4140 should be able to toggle CRT on/off', () => {
// # Set CRT to off
cy.uiChangeCRTDisplaySetting('OFF');
// * Threads menu in sidebar should not be visible
cy.get('.SidebarGlobalThreads').should('not.exist');
// # Set CRT to on
cy.uiChangeCRTDisplaySetting('ON');
// * Threads menu in sidebar should be visible
cy.get('.SidebarGlobalThreads').should('exist');
// # Visit global threads
cy.visit(`/${testTeam.name}/threads`);
// * should see No followed threads yet in H3 title
cy.get('h3.no-results__title').should('have.text', 'No followed threads yet');
});
});

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads @not_cloud
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {interceptFileUpload} from '../files_and_attachments/helpers';
import * as MESSAGES from '@/fixtures/messages';
import {matterpollPlugin} from '@/utils/plugins';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testChannel: Channel;
let user1: UserProfile;
before(() => {
cy.apiUpdateConfig({
PluginSettings: {
Enable: true,
},
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
user1 = user;
testChannel = channel;
cy.apiSaveCRTPreference(user1.id, 'on');
});
});
beforeEach(() => {
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
interceptFileUpload();
});
it('MM-T4776 should display poll text without Markdown in the threads list', () => {
cy.shouldNotRunOnCloudEdition();
cy.shouldHavePluginUploadEnabled();
// # Upload and enable "matterpoll" plugin
cy.apiUploadAndEnablePlugin(matterpollPlugin);
// # In center post the following: /poll "Do you like https://mattermost.com?"
cy.postMessage('/poll "Do you like https://mattermost.com?"');
cy.getLastPostId().then((pollId) => {
const threadAuthorName = user1.nickname || user1.username;
// # Post a reply on the POLL to create a thread and follow
cy.postMessageAs({sender: user1, message: MESSAGES.SMALL, channelId: testChannel.id, rootId: pollId});
// # Click "Yes" or "No" on the poll post
cy.get(`#post_${pollId}`).within(() => {
cy.findByText('Yes').click();
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * Text in ThreadItem should say 'username: Do you like https://mattermost.com?'
cy.get('.ThreadItem').last().within(() => {
cy.contains(threadAuthorName + ': Do you like https://mattermost.com?').should('be.visible');
cy.contains('Total votes: 1').should('be.visible');
});
// # Open the thread
cy.get('.ThreadItem').last().click();
// # Go to channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # End the poll
cy.get(`#post_${pollId}`).within(() => {
cy.findByText('End Poll').click();
});
cy.findByText('End').click();
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * Text in ThreadItem should say 'username: Do you like https://mattermost.com?'
cy.get('.ThreadItem').last().within(() => {
cy.contains(threadAuthorName + ': Do you like https://mattermost.com?').should('be.visible');
cy.contains('This poll has ended. The results are:').should('be.visible');
});
// # Cleanup
cy.apiDeletePost(pollId);
});
});
});

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import {waitUntilUploadComplete, interceptFileUpload} from '../files_and_attachments/helpers';
import * as MESSAGES from '@/fixtures/messages';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testChannel: Channel;
let user1: UserProfile;
const files = [
{
testCase: 'MM-T4777_2',
filename: 'word-file.doc',
extensions: 'DOC',
icon: 'icon-file-word-outline',
},
{
testCase: 'MM-T4777_3',
filename: 'wordx-file.docx',
extensions: 'DOCX',
icon: 'icon-file-word-outline',
},
{
testCase: 'MM-T4777_4',
filename: 'powerpoint-file.ppt',
extensions: 'PPT',
icon: 'icon-file-powerpoint-outline',
},
{
testCase: 'MM-T4777_5',
filename: 'powerpointx-file.pptx',
extensions: 'PPTX',
icon: 'icon-file-powerpoint-outline',
},
{
testCase: 'MM-T4777_6',
filename: 'mp3-audio-file.mp3',
extensions: 'MP3',
icon: 'icon-file-audio-outline',
},
{
testCase: 'MM-T4777_7',
filename: 'mp4-video-file.mp4',
extensions: 'MP4',
icon: 'icon-file-video-outline',
},
{
testCase: 'MM-T4777_8',
filename: 'theme.json',
extensions: 'JSON',
icon: 'icon-file-code-outline',
},
];
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
user1 = user;
testChannel = channel;
cy.apiSaveCRTPreference(user1.id, 'on');
});
});
beforeEach(() => {
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
interceptFileUpload();
});
it('MM-T4777_1 should show image thumbnail in thread list item', () => {
const image = 'jpg-image-file.jpg';
cy.get('#advancedTextEditorCell').find('#fileUploadInput').attachFile(image);
waitUntilUploadComplete();
cy.get('.post-image__thumbnail').should('be.visible');
cy.uiGetPostTextBox().clear().type('{enter}');
cy.getLastPostId().then((rootId) => {
// # Post a reply to create a thread and follow
cy.postMessageAs({sender: user1, message: MESSAGES.SMALL, channelId: testChannel.id, rootId});
// # Visit Global Threads
cy.uiClickSidebarItem('threads');
// * Text should be the filename
cy.get('.file_card__name').should('have.text', image);
// * Image should be shown
cy.get('.file_card__image.post-image.small').should('be.visible');
// # Cleanup
cy.apiDeletePost(rootId);
});
});
files.forEach((file) => {
it(`${file.testCase} should display correct icon for ${file.extensions} on threads list`, () => {
// # Post a file
cy.get('#advancedTextEditorCell').find('#fileUploadInput').attachFile(file.filename);
waitUntilUploadComplete();
cy.get('.post-image__thumbnail').should('be.visible');
cy.uiGetPostTextBox().clear().type('{enter}');
cy.getLastPostId().then((rootId) => {
// # Post a reply to create a thread and follow
cy.postMessageAs({sender: user1, message: MESSAGES.SMALL, channelId: testChannel.id, rootId});
// # Visit Global Threads
cy.uiClickSidebarItem('threads');
// * Thread item should display the correct icon
cy.get('.file_card__attachment').should('have.class', file.icon);
// * Thread item should display text
cy.get('.file_card__name').should('have.text', file.filename);
// # Cleanup
cy.apiDeletePost(rootId);
});
});
});
});

View File

@@ -0,0 +1,278 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @collapsed_reply_threads
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import * as TIMEOUTS from '@/fixtures/timeouts';
import {isMac} from '@/utils';
import type {ChainableT} from '@/types';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testUser: UserProfile;
let otherUser: UserProfile;
let testChannel: Channel;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
testUser = user;
testChannel = channel;
cy.apiSaveCRTPreference(testUser.id, 'on');
cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => {
otherUser = user1;
cy.apiAddUserToTeam(testTeam.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, otherUser.id);
});
});
});
});
beforeEach(() => {
// # Visit channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
});
it('MM-T4141_1 should follow a thread after replying', () => {
// # Post message as other user
cy.postMessageAs({
sender: otherUser,
message: 'Root post,',
channelId: testChannel.id,
}).then(({id: rootId}) => {
// * Thread footer should not be visible
cy.get(`#post_${rootId}`).find('.ThreadFooter').should('not.exist');
// # Click on post to open RHS
cy.get(`#post_${rootId}`).click();
// * Button on header should say Follow as current user is not following
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Follow');
// # Post a reply as current user
cy.postMessageReplyInRHS('Reply!');
// # Get last root post
cy.get(`#post_${rootId}`).
// * thread footer should exist now
get('.ThreadFooter').should('exist').
within(() => {
// * the button on the footer should say Following
cy.get('.FollowButton').should('have.text', 'Following');
});
// * the button on the RHS header should now say Following
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Following');
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * There should be a thread there
cy.get('div.ThreadItem').should('have.have.lengthOf', 1);
});
});
it('MM-T4141_2 should follow a thread after marking it as unread', () => {
// # Post a root post as other user
postMessageWithReply(testChannel.id, otherUser, 'Another interesting post,', otherUser, 'Self reply!').then(({rootId, replyId}) => {
// # Get root post
cy.get(`#post_${rootId}`).within(() => {
// * Thread footer should be visible
cy.get('.ThreadFooter').should('exist').
// * Thread footer button should say 'Follow'
find('.FollowButton').should('have.text', 'Follow');
});
// # Click on root post to open thread
cy.get(`#post_${rootId}`).click();
// * RHS header button should say 'Follow'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Follow');
// # Click on the reply's dot menu and mark as unread
cy.uiClickPostDropdownMenu(replyId, 'Mark as Unread', 'RHS_COMMENT');
// * RHS header button should say 'Following'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Following');
// # Get root post
cy.get(`#post_${rootId}`).within(() => {
// * Thread footer should be visible
cy.get('.ThreadFooter').should('exist').
// * Thread footer button should say 'Following'
find('.FollowButton').should('have.text', 'Following');
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * There should be 2 threads now
cy.get('div.ThreadItem').should('have.have.lengthOf', 2);
});
});
it('MM-T4141_3 clicking "Following" button in the footer should unfollow the thread', () => {
// # Post a root post as other user
postMessageWithReply(testChannel.id, otherUser, 'Another interesting post,', testUser, 'Self reply!');
// # Get last root post in channel
cy.getLastPostId().then((rootId) => {
// # Open the thread
cy.get(`#post_${rootId}`).click();
// * RHS header button should say 'Following'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Following');
// # Get thread footer of last root post
cy.get(`#post_${rootId}`).within(() => {
// * thread footer should exist
cy.get('.ThreadFooter').should('exist');
// * thread footer button should say 'Following'
cy.get('.FollowButton').should('have.text', 'Following');
// # Click thread footer Following button
cy.get('.FollowButton').click({force: true});
// * thread footer button should say 'Follow'
cy.get('.FollowButton').should('have.text', 'Follow');
});
// * RHS header button should say 'Follow'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Follow');
// # Close RHS
cy.uiCloseRHS();
});
});
it('MM-T4141_4 clicking "Follow" button in the footer should follow the thread', () => {
// # Post a root post as other user
postMessageWithReply(testChannel.id, otherUser, 'Another interesting post,', otherUser, 'Self reply!');
// # Get last root post in channel
cy.getLastPostId().then((rootId) => {
// # Open the thread
cy.get(`#post_${rootId}`).click();
// * RHS header button should say 'Follow'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Follow');
// # Get thread footer of last root post
cy.get(`#post_${rootId}`).within(() => {
// * thread footer should exist
cy.get('.ThreadFooter').should('exist');
// * thread footer button should say 'Follow'
cy.get('.FollowButton').should('have.text', 'Follow');
// # Click thread footer 'Follow' button
cy.get('.FollowButton').click({force: true});
// * thread footer button should say 'Following'
cy.get('.FollowButton').should('have.text', 'Following');
});
// * RHS header button should say 'Following'
cy.get('#rhsContainer').find('.FollowButton').should('have.text', 'Following');
// # Close RHS
cy.uiCloseRHS();
});
});
it('MM-T4682 should show search guidance at the end of the list after scroll loading', () => {
// # Create more than 25 threads so we can use scroll loading in the Threads list
for (let i = 1; i <= 30; i++) {
postMessageWithReply(testChannel.id, otherUser, `Another interesting post ${i}`, testUser, `Another reply ${i}!`).then(({rootId}) => {
// # Mark last thread as Unread
if (i === 30) {
// # Click on root post to open thread
cy.get(`#post_${rootId}`).click();
// # Click on the reply's dot menu and mark as unread
cy.uiClickPostDropdownMenu(rootId, 'Mark as Unread', 'RHS_ROOT');
}
});
}
cy.uiClickSidebarItem('threads');
// # Scroll load the threads list to reach the end
const maxScrolls = 3;
scrollThreadsListToEnd(maxScrolls);
// * Search guidance item should be shown at the end of the threads list
cy.get('.ThreadList .no-results__wrapper').should('be.visible').within(() => {
// * Title, subtitle and shortcut keys should be shown
cy.findByText('That’s the end of the list').should('be.visible');
cy.contains('If you’re looking for older conversations, try searching with ').should('be.visible').within(() => {
cy.findByText(isMac() ? '⌘' : 'Ctrl').should('be.visible');
cy.findByText('Shift').should('be.visible');
cy.findByText('F').should('be.visible');
});
});
// # Click Unreads button
cy.findByText('Unreads').click();
// * Search guidance item should not be shown at the end of the Unreads threads list
cy.get('.ThreadList .no-results__wrapper').should('not.exist');
});
});
function postMessageWithReply(channelId: string, postSender: {username: string; password: string}, postMessage: string, replySender: {username: string; password: string}, replyMessage: string): ChainableT {
return cy.postMessageAs({
sender: postSender,
message: postMessage || 'Another interesting post.',
channelId,
}).then(({id: rootId}) => {
cy.postMessageAs({
sender: replySender || postSender,
message: replyMessage || 'Another reply!',
channelId,
rootId,
}).then(({id: replyId}) => (Promise.resolve({rootId, replyId})));
});
}
function scrollThreadsListToEnd(maxScrolls = 1, scrolls = 0): ChainableT<void> {
if (scrolls === maxScrolls) {
return cy.wrap(undefined);
}
return cy.get('.ThreadList .virtualized-thread-list').scrollTo('bottom').then(($el) => {
const element = $el.find('.no-results__wrapper');
if (element.length < 1) {
return cy.wait(TIMEOUTS.ONE_SEC).then(() => {
return scrollThreadsListToEnd(maxScrolls, scrolls + 1);
});
}
return cy.wrap(element).scrollIntoView().then(() => undefined);
});
}

View File

@@ -0,0 +1,519 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @collapsed_reply_threads
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import type {PostMessageResp} from '@/support/task_commands';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testChannel: Channel;
let user1: UserProfile;
let user2: UserProfile;
let user3: UserProfile;
let rootPost: PostMessageResp;
let replyPost1: PostMessageResp;
let replyPost2: PostMessageResp;
const messages = {
ROOT: 'ROOT POST',
REPLY1: 'REPLY 1',
REPLY2: 'REPLY 2',
};
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
user1 = user;
testChannel = channel;
cy.apiSaveCRTPreference(user1.id, 'on');
cy.apiCreateUser({prefix: 'user2'}).then(({user: newUser}) => {
user2 = newUser;
cy.apiAddUserToTeam(testTeam.id, user2.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, user2.id);
});
});
cy.apiCreateUser({prefix: 'user3'}).then(({user: newUser}) => {
user3 = newUser;
cy.apiAddUserToTeam(testTeam.id, user3.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, user3.id);
});
});
});
});
beforeEach(() => {
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Post a message as other user
cy.postMessageAs({sender: user1, message: messages.ROOT, channelId: testChannel.id}).then((post) => {
rootPost = post;
});
});
it('MM-T4379 Display: Click to open threads', () => {
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Get the root post
cy.getLastPost().click();
// * Verify the post is opened in RHS
cy.get(`#rhsPost_${rootPost.id}`).should('be.visible');
// # Close RHS
cy.uiCloseRHS();
// # Open settings modal and disable opening the thread in RHS when being clicked
cy.uiOpenSettingsModal('Display');
// # Open section "click to open threads"
cy.get('#click_to_replyTitle').click();
// # Click radio button with option B ('off')
cy.get('#click_to_replyFormatB').click();
// # Save settings
cy.get('#saveSetting').click();
// # close settings modal
cy.uiClose();
// # (Re-) Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Get the root post
cy.getLastPost().click();
// * Verify the post is opened in RHS
cy.get(`rhsPost_${rootPost.id}`).should('not.exist');
// # Cleanup for next test
cy.apiDeletePost(rootPost.id);
});
it('MM-T4445 CRT - Delete root post', () => {
/**
* When you delete a post the current behavior in displaying it and the thread replies is incorrect.
* Deleting a root post leads to a post showing `(message deleted)`, but still rendering the replies
* in the ThreadFooter. Ideally we should remove the root post without removing the replies.
*/
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: user2, message: messages.REPLY1, channelId: testChannel.id, rootId: rootPost.id});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1);
// # Delete thread root post
cy.apiDeletePost(rootPost.id);
/**
* TODO: this should not be there once the root post is deleted, so remove it once the feature is adjusted
*/
// * There should be a single thread item showing '(message deleted)'
cy.get('div.ThreadItem').should('have.lengthOf', 1).should('contain.text', '(message deleted)');
// # Refresh the page
cy.reload(true);
// * There should be no thread item anymore
cy.get('div.ThreadItem').should('have.lengthOf', 0);
});
it('MM-T4446 CRT - Delete single reply post on a thread', () => {
/**
* When you delete a reply you are still following the thread, so the thread is not being removed
* from the global threads view. The test description was written differently as it was assuming the
* thread to not be followed after that. The test will be build keeping that in mind, but should
* thread to not be followed after that. The test will be build keeping that in mind, but should
* probably be adjusted later on.
*/
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: user1, message: messages.REPLY1, channelId: testChannel.id, rootId: rootPost.id}).then((post) => {
replyPost1 = post;
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).first().click();
// * Reply should be in RHS
cy.get(`#rhsPostMessageText_${replyPost1.id}`).should('be.visible').should('contain.text', messages.REPLY1);
// # Delete thread reply post
cy.apiDeletePost(replyPost1.id);
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1);
// * The reply should be in RHS showing '(message deleted)'
cy.get(`#rhsPost_${replyPost1.id}`).should('be.visible').should('contain.text', '(message deleted)');
// # Reload to re-render UI
cy.reload(true);
// * There should be a single thread item with no reply
cy.get('div.ThreadItem').should('have.lengthOf', 0);
// * The reply post should not exist anymore
cy.get(`#rhsPost_${replyPost1.id}`).should('not.exist');
// # Cleanup for next test
cy.apiDeletePost(rootPost.id);
});
});
it('MM-T4447 CRT - Delete single reply post on a multi-reply thread', () => {
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: user2, message: messages.REPLY1, channelId: testChannel.id, rootId: rootPost.id}).then((post) => {
replyPost1 = post;
}).then(() => {
return cy.postMessageAs({sender: user3, message: messages.REPLY2, channelId: testChannel.id, rootId: rootPost.id});
}).then((post) => {
replyPost2 = post;
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in Thread Footer should say '2 replies'
cy.get('.ReplyButton').should('have.text', '2 replies');
// * 2 avatars/participants should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 2);
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).first().click().within(() => {
// * Activity section in ThreadItem should say '2 replies'
cy.get('.activity').should('have.text', '2 replies');
// * 3 avatars/participants should show in ThreadItem
cy.get('.Avatar').should('have.lengthOf', 3);
});
// * The reply should be in RHS showing '(message deleted)'
cy.get(`#rhsPost_${replyPost2.id}`).should('be.visible').should('contain.text', messages.REPLY2);
// # Delete thread reply post
cy.apiDeletePost(replyPost2.id);
// # Reload to re-render UI
cy.reload(true);
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).first().click().within(() => {
// * Activity section in ThreadItem should say '1 reply'
cy.get('.activity').should('have.text', '1 reply');
// * 2 avatars/participants should show in ThreadItem
cy.get('.Avatar').should('have.lengthOf', 2);
});
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in Thread Footer should say '1 reply'
cy.get('.ReplyButton').should('have.text', '1 reply');
// * 1 avatar/participant should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 1);
});
// # Cleanup for next test
cy.apiDeletePost(rootPost.id);
});
});
it('MM-T4448_1 CRT - L16 - Use “Mark all as read” button', () => {
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: user2, message: messages.REPLY1, channelId: testChannel.id, rootId: rootPost.id}).then((post) => {
replyPost1 = post;
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in Thread Footer should say '1 reply'
cy.get('.ReplyButton').should('have.text', '1 reply');
// * 1 avatar/participant should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 1);
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * The unreads tab button has a blue dot (unread indicator)
cy.get('#threads-list-unread-button .dot').should('have.lengthOf', 1);
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).within(() => {
// * The unread indicator (blue dot) should be present
cy.get('.dot-unreads').should('have.lengthOf', 1);
// * Activity section in ThreadItem should say '1 new reply'
cy.get('.activity').should('have.text', '1 new reply');
});
// # Get the "mark all as read" button (the selector is pretty ambiguous here, but should work for now)
cy.get('#threads-list__mark-all-as-read').click();
// * mark_all_threads_as_read_modal is open
cy.get('#mark-all-threads-as-read-modal').should('exist');
// # Click confirm button
cy.get('button.confirm').contains('Mark all as read').click();
// * mark_all_threads_as_read_modal is closed
cy.get('#mark-all-threads-as-read-modal').should('not.exist');
// * The unreads tab button does NOT have a blue dot (unread indicator)
cy.get('#threads-list-unread-button .dot').should('not.exist');
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).within(() => {
// * The unread indicator (blue dot) should NOT be present
cy.get('.dot-unreads').should('have.lengthOf', 0);
// * Activity section in ThreadItem should say '1 reply'
cy.get('.activity').should('have.text', '1 reply');
});
// * Assert that the RHS shows the correct view
cy.get('.no-results__holder').should('contain.text', 'Looks like you’re all caught up');
// # Cleanup for next test
cy.apiDeletePost(rootPost.id);
});
});
it('MM-T4448_2 CRT - L16 - Cancel “Mark all as unread” modal', () => {
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: user2, message: messages.REPLY1, channelId: testChannel.id, rootId: rootPost.id}).then((post) => {
replyPost1 = post;
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * The unreads tab button has a blue dot (unread indicator)
cy.get('#threads-list-unread-button .dot').should('have.lengthOf', 1);
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).within(() => {
// * The unread indicator (blue dot) should be present
cy.get('.dot-unreads').should('have.lengthOf', 1);
// * Activity section in ThreadItem should say '1 new reply'
cy.get('.activity').should('have.text', '1 new reply');
});
// # Get the "mark all as read" button
cy.get('#threads-list__mark-all-as-read').click();
// * Verify mark_all_threads_as_read_modal is open
cy.get('#mark-all-threads-as-read-modal').should('exist');
// # Click cancel button
cy.get('.btn-tertiary').contains('Cancel').click();
// * Verify mark_all_threads_as_read_modal is closed
cy.get('#mark-all-threads-as-read-modal').should('not.exist');
// * Verify the unreads tab button still has a blue dot
cy.get('#threads-list-unread-button .dot').should('exist');
// # Click on “Mark all as unread” button again
cy.get('#threads-list__mark-all-as-read').click();
// * Verify mark_all_threads_as_read_modal is open
cy.get('#mark-all-threads-as-read-modal').should('exist');
// # Click close button
cy.get('button.close').click();
// * Verify mark_all_threads_as_read_modal is closed
cy.get('#mark-all-threads-as-read-modal').should('not.exist');
// * Verify the unreads tab button still has a blue dot
cy.get('#threads-list-unread-button .dot').should('exist');
// # Click on “Mark all as unread” button again
cy.get('#threads-list__mark-all-as-read').click();
// # Press ESC key to close the modal
cy.get('body').type('{esc}');
// * Verify mark_all_threads_as_read_modal is closed
cy.get('#mark-all-threads-as-read-modal').should('not.exist');
// * Verify the unreads tab button still has a blue dot
cy.get('#threads-list-unread-button .dot').should('exist');
// * There should be a single thread item
cy.get('div.ThreadItem').should('have.lengthOf', 1).within(() => {
// * The unread indicator (blue dot) should be present
cy.get('.dot-unreads').should('have.lengthOf', 1);
// * Activity section in ThreadItem should say '1 new reply'
cy.get('.activity').should('have.text', '1 new reply');
});
});
// * Assert that the RHS shows the correct view
cy.get('.no-results__holder').should('not.contain.text', 'Looks like you’re all caught up');
// # Cleanup for next test
cy.apiDeletePost(rootPost.id);
});
it('CRT - Threads list keyboard navigation', () => {
// # Post a few messages as another user and then reply to each one as the current user to follow them
let firstRoot: PostMessageResp;
let firstReply: PostMessageResp;
let secondRoot: PostMessageResp;
let secondReply: PostMessageResp;
let thirdRoot: PostMessageResp;
let thirdReply: PostMessageResp;
cy.postMessageAs({sender: user1, message: messages.ROOT + '1', channelId: testChannel.id}).then((post) => {
firstRoot = post;
cy.postMessageAs({sender: user2, message: messages.REPLY1 + '1', channelId: testChannel.id, rootId: firstRoot.id}).then((reply) => {
firstReply = reply;
});
}).then(() => {
cy.postMessageAs({sender: user1, message: messages.ROOT + '1', channelId: testChannel.id}).then((post) => {
secondRoot = post;
cy.postMessageAs({sender: user2, message: messages.REPLY1 + '1', channelId: testChannel.id, rootId: secondRoot.id}).then((reply) => {
secondReply = reply;
});
});
}).then(() => {
cy.postMessageAs({sender: user1, message: messages.ROOT + '1', channelId: testChannel.id}).then((post) => {
thirdRoot = post;
cy.postMessageAs({sender: user2, message: messages.REPLY1 + '1', channelId: testChannel.id, rootId: thirdRoot.id}).then((reply) => {
thirdReply = reply;
});
});
}).then(() => {
// # Click on the Threads item in the LHS
cy.get('a').contains('Threads').click();
// * There should be a three threads in the threads list
cy.get('div.ThreadItem').should('have.lengthOf', 3);
// * No thread should be selected at first
cy.contains('Catch up on your threads').should('be.visible');
// # Press the down arrow
cy.get('body').type('{downArrow}');
// * The third thread should be visible since it's the most recent one
cy.contains('Catch up on your threads').should('not.exist');
cy.get(`#rhsPost_${thirdRoot.id}`).should('be.visible');
cy.get(`#rhsPost_${thirdReply.id}`).should('be.visible');
// * The comment box should not be focused
cy.focused().should('not.have.id', 'reply_textbox');
// # Press the down arrow again
cy.get('body').type('{downArrow}');
// * The second thread should be visible
cy.get(`#rhsPost_${secondRoot.id}`).should('be.visible');
cy.get(`#rhsPost_${secondReply.id}`).should('be.visible');
// * The comment box should still not be focused
cy.focused().should('not.have.id', 'reply_textbox');
// # Press the down arrow again
cy.get('body').type('{downArrow}');
// * The first thread should be visible
cy.get(`#rhsPost_${firstRoot.id}`).should('be.visible');
cy.get(`#rhsPost_${firstReply.id}`).should('be.visible');
// * The comment box should still not be focused
cy.focused().should('not.have.id', 'reply_textbox');
// # Press the up arrow
cy.get('body').type('{upArrow}');
// * The second thread should be visible
cy.get(`#rhsPost_${secondRoot.id}`).should('be.visible');
cy.get(`#rhsPost_${secondReply.id}`).should('be.visible');
// * The comment box should still not be focused
cy.focused().should('not.have.id', 'reply_textbox');
// # Type something
cy.get('body').type('something');
// * The comment box should be focused and have received that input
cy.focused().should('have.id', 'reply_textbox').
should('contain.text', 'something');
// # Press the up arrow again
cy.get('body').type('{upArrow}');
// * The second thread should still be visible because focus is now in the comment box
cy.get(`#rhsPost_${secondRoot.id}`).should('be.visible');
cy.get(`#rhsPost_${secondReply.id}`).should('be.visible');
});
});
});

View File

@@ -0,0 +1,119 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import * as TIMEOUTS from '@/fixtures/timeouts';
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads
describe('Collapsed Reply Threads', () => {
let userA: UserProfile; // Member of team A and B
let teamA: Team;
let teamB: Team;
let offTopicUrlA: string;
let testChannel: Channel;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_on',
},
});
cy.apiInitSetup().then(({team, user, offTopicUrl: url}) => {
userA = user;
teamA = team;
offTopicUrlA = url;
cy.apiCreateUser().then(() => {
return cy.apiCreateTeam('team', 'Team');
}).then(({team: otherTeam}) => {
teamB = otherTeam;
return cy.apiAddUserToTeam(teamB.id, userA.id);
}).then(() => {
return cy.apiCreateChannel(teamA.id, 'test', 'Test');
}).then(({channel}) => {
testChannel = channel;
return cy.apiAddUserToChannel(testChannel.id, userA.id);
});
});
});
beforeEach(() => {
// # Log in to Team A with an account that has joined multiple teams.
cy.apiLogin(userA);
// # On an account on two teams, view Team A
cy.visit(offTopicUrlA);
});
it('MM-T4887 should stay on threads view when switching teams', () => {
// # Navigate to the new teams town square
cy.visit(`/${teamA.name}/channels/town-square`);
// # Switch to Team B
cy.get(`#${teamB.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// * Verify team display name changes correctly.
cy.uiGetLHSHeader().findByText(teamB.display_name);
// # Go to the ‘Threads’ view on Team B
cy.uiGetSidebarThreadsButton().click();
cy.wait(TIMEOUTS.ONE_SEC);
cy.url().should('include', `${teamB.name}/threads`);
// # Switch back to Team A
cy.get(`#${teamA.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// Verify url is set to landing channel
cy.url().should('include', `${teamA.name}/channels/town-square`);
});
it('MM-T4843_1 should go to threads view when switching a team if that was the last view on that team', () => {
// # Go to the ‘Threads’ view on Team A
cy.uiGetSidebarThreadsButton().click();
// * Wait for the view to mount, since that is what records threads as the team's last viewed type
cy.get('.GlobalThreads').should('be.visible');
// # Switch to Team B
cy.get(`#${teamB.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// * Verify team display name changes correctly.
cy.uiGetLHSHeader().findByText(teamB.display_name);
// # Switch back to Team A
cy.get(`#${teamA.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// Verify url is set up for threads view
cy.url().should('include', `${teamA.name}/threads`);
});
it('MM-T4843_2 should go to threads view when threads view is the penultimate view and leave the current channel', () => {
// # Go to the ‘Threads’ view on Team A
cy.uiGetSidebarThreadsButton().click();
cy.wait(TIMEOUTS.ONE_SEC);
// # Switch to Test Channel
cy.uiClickSidebarItem(testChannel.name);
// # Leave the current channel
cy.uiLeaveChannel();
// Verify url is set up for threads view when thread view is the penultimate view
cy.url().should('include', `${teamA.name}/threads`);
});
});

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testUser: UserProfile;
let otherUser: UserProfile;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
EnableTutorial: false,
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, user}) => {
testTeam = team;
testUser = user;
cy.apiSaveCRTPreference(testUser.id, 'on');
cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => {
otherUser = user1;
cy.apiAddUserToTeam(testTeam.id, otherUser.id);
});
});
});
beforeEach(() => {
// # Visit the channel
cy.visit(`/${testTeam.name}/messages/@${otherUser.username}`);
});
it('should open thread when thread footer reply button is clicked in a DM/GM channel', () => {
// # Post a message
const msg = 'Root post';
cy.postMessage(msg);
cy.getLastPostId().then((rootId) => {
// # Thread with replies
cy.clickPostCommentIcon(rootId);
cy.uiGetReplyTextBox().type('reply{enter}');
cy.uiGetReplyTextBox().type('reply2{enter}');
cy.uiCloseRHS();
// * Check that the RHS is closed
cy.get('#rhsContainer').should('not.exist');
// # Get thread footer of last post and find reply button
cy.uiGetPostThreadFooter(rootId).find('button.ReplyButton').click();
// * Thread should be visible in RHS
cy.get(`#rhsPost_${rootId}`).within(() => {
cy.get(`#rhsPostMessageText_${rootId}`).should('be.visible').and('have.text', msg);
});
});
});
});

View File

@@ -0,0 +1,225 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @collapsed_reply_threads
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import type {PostMessageResp} from '@/support/task_commands';
import * as TIMEOUTS from '@/fixtures/timeouts';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testUser: UserProfile;
let otherUser: UserProfile;
let testChannel: Channel;
let rootPost: PostMessageResp;
let postForAvatar: PostMessageResp;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
testUser = user;
testChannel = channel;
cy.apiSaveCRTPreference(testUser.id, 'on');
cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => {
otherUser = user1;
cy.apiAddUserToTeam(testTeam.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, otherUser.id);
// # Post a message as other user
cy.postMessageAs({sender: otherUser, message: 'Root post', channelId: testChannel.id}).then((post) => {
rootPost = post;
});
// # Post a message as other user for clicking avatar test
cy.postMessageAs({sender: otherUser, message: 'Root post for clicking avatar', channelId: testChannel.id}).then((post) => {
postForAvatar = post;
});
});
});
});
});
beforeEach(() => {
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
});
it('MM-T4142 should show number of replies in thread', () => {
cy.uiWaitUntilMessagePostedIncludes(rootPost.data.message);
// # Thread footer should not exist
cy.uiGetPostThreadFooter(rootPost.id).should('not.exist');
// # Post a reply post as current user
cy.postMessageAs({sender: testUser, message: 'reply to root post', channelId: testChannel.id, rootId: rootPost.id});
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in Thread Footer should say '1 reply'
cy.get('.ReplyButton').should('have.text', '1 reply');
// * 1 avatar/participant should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 1);
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * The sole thread item should have text in footer saying '1 reply'
cy.get('div.ThreadItem').find('.activity').should('have.text', '1 reply');
// # Visit the channel
cy.uiClickSidebarItem(testChannel.name);
// # Post another reply as current user
cy.postMessageAs({sender: testUser, message: 'another reply!', channelId: testChannel.id, rootId: rootPost.id});
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in thread footer should say '2 replies'
cy.get('.ReplyButton').should('have.text', '2 replies');
// * 1 avatar/participant should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 1);
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * The sole thread item should have text in footer saying '2 replies'
cy.get('div.ThreadItem').find('.activity').should('have.text', '2 replies');
// # Visit the channel
cy.uiClickSidebarItem(testChannel.name);
// # Post another reply as other user
cy.postMessageAs({sender: otherUser, message: 'other reply!', channelId: testChannel.id, rootId: rootPost.id});
// # Get thread footer of last post
cy.uiGetPostThreadFooter(rootPost.id).within(() => {
// * Reply button in thread footer should say '3 replies'
cy.get('.ReplyButton').should('have.text', '3 replies');
// * 2 avatars/participants should show in Thread Footer
cy.get('.Avatar').should('have.lengthOf', 2);
});
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * The sole thread item should have text in footer saying '1 new reply'
cy.get('div.ThreadItem').find('.activity').should('have.text', '1 new reply');
});
it('MM-T4646 should open popover when avatar is clicked', () => {
cy.uiWaitUntilMessagePostedIncludes(postForAvatar.data.message);
// # Post a reply post as current user
cy.postMessageAs({sender: testUser, message: 'reply!', channelId: testChannel.id, rootId: postForAvatar.id});
// # Post another reply as other user
cy.postMessageAs({sender: otherUser, message: 'another reply!', channelId: testChannel.id, rootId: postForAvatar.id});
// # Get thread footer of last post and find avatars
cy.uiGetPostThreadFooter(postForAvatar.id).find('.Avatars').find('button').first().click();
// * Profile popover should be visible and close on ESC
cy.get('div.user-profile-popover').first().should('be.visible').find('button.btn-primary.btn-sm').type('{esc}');
// # Visit global threads
cy.uiClickSidebarItem('threads');
// * Find the first avatar and click it
cy.get('div.ThreadItem').find('.activity').find('.Avatars').find('button').first().click();
// * Profile popover should be visible and close on ESC
cy.get('div.user-profile-popover').first().should('be.visible').find('button.btn-primary.btn-sm');
});
it('MM-T4143 Emoji reaction - type +:+1:', () => {
// # Create a root post
cy.postMessage('Hello!');
cy.getLastPostId().then((postId) => {
// # Click on post to open the thread in RHS
cy.get(`#post_${postId}`).click();
// # Type "+:+1:" in comment box to react to the post with a thumbs-up and post
cy.postMessageReplyInRHS('+:+1:');
// * Thumbs-up reaction displays as reaction on post
cy.get(`#${postId}_message`).within(() => {
cy.findByLabelText('reactions').should('be.visible');
cy.findByRole('button', {name: /reacted with :\+1:/i}).should('be.visible');
});
// * Reacting to a root post should not create a thread (thread footer should not exist)
cy.uiGetPostThreadFooter(postId).should('not.exist');
// # Close RHS
cy.uiCloseRHS();
});
});
it('MM-T5413 should auto-scroll to bottom upon pasting long text in reply', () => {
// # Post a root post as current user
cy.postMessageAs({
sender: testUser,
message: 'Another interesting post,',
channelId: testChannel.id,
}).then(({id: rootId}) => {
// # Post multiple replies as other user so that the new messages line is pushed up
Cypress._.times(20, (i) => {
cy.postMessageAs({
sender: otherUser,
message: 'Reply ' + i,
channelId: testChannel.id,
rootId,
});
});
// # Click root post
cy.get(`#post_${rootId}`).click();
// # Wait for RHS to open and scroll to position
cy.wait(TIMEOUTS.ONE_SEC);
// * RHS should open and the editor's actions should be visible.
cy.get('#rhsContainer').findByTestId('SendMessageButton').should('be.visible');
// # Close RHS
cy.uiCloseRHS();
// # Click root post
cy.get(`#post_${rootId}`).click();
// # Paste a multiline string in the RHS textbox.
const text = 'word '.repeat(2000);
cy.get('#rhsContainer').findByTestId('reply_textbox').clear().invoke('val', text).trigger('input');
// * RHS should open and the editor should be visible and focused
cy.get('#rhsContainer').findByTestId('SendMessageButton').scrollIntoView().should('be.visible');
});
});
});

View File

@@ -0,0 +1,165 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @collapsed_reply_threads
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import * as TIMEOUTS from '@/fixtures/timeouts';
describe('Collapsed Reply Threads', () => {
let testTeam: Team;
let testUser: UserProfile;
let otherUser: UserProfile;
let testChannel: Channel;
before(() => {
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
CollapsedThreads: 'default_off',
},
});
// # Create new channel and other user, and add other user to channel
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel, user}) => {
testTeam = team;
testUser = user;
testChannel = channel;
cy.apiSaveCRTPreference(testUser.id, 'on');
cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => {
otherUser = user1;
cy.apiAddUserToTeam(testTeam.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, otherUser.id);
});
});
});
});
beforeEach(() => {
// # Visit channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
});
it('MM-T4144_1 should show a new messages line for an unread thread', () => {
// # Post a root post as current user
cy.postMessageAs({
sender: testUser,
message: 'Another interesting post,',
channelId: testChannel.id,
}).then(({id: rootId}) => {
// # Post multiple replies as other user so that the new messages line is pushed up
Cypress._.times(20, (i) => {
cy.postMessageAs({
sender: otherUser,
message: 'Reply ' + i,
channelId: testChannel.id,
rootId,
});
});
// # Click root post
cy.get(`#post_${rootId}`).click();
// # Wait for RHS to open and scroll to position
cy.wait(TIMEOUTS.ONE_SEC);
// * RHS should open and new messages line should be visible
cy.get('#rhsContainer').findByTestId('NotificationSeparator').scrollIntoView().should('be.visible');
// # Close RHS
cy.uiCloseRHS();
});
});
it('MM-T4144_2 should not show a new messages line after viewing the thread', () => {
// # Get last message in channel
cy.getLastPostId().then((rootId) => {
// # Click on message
cy.get(`#post_${rootId}`).click();
// * RHS should open and new messages line should NOT be visible
cy.get('#rhsContainer').findByTestId('NotificationSeparator').should('not.exist');
// # Close RHS
cy.uiCloseRHS();
});
});
it('MM-T5671 should handle mention counts correctly when marking a thread as unread and unfollowing it', () => {
// # Post a root post as current user
cy.postMessageAs({
sender: otherUser,
message: `@${testUser.username} Root post for mention test`,
channelId: testChannel.id,
}).then(({id: rootId}) => {
// # Post a reply mentioning the user
cy.postMessageAs({
sender: otherUser,
message: `Hey @${testUser.username}, check this out!`,
channelId: testChannel.id,
rootId,
}).then(({id: replyId}) => {
// # Post another reply mentioning the user
cy.postMessageAs({
sender: otherUser,
message: `Hey @${testUser.username}, check this out too!`,
channelId: testChannel.id,
rootId,
});
// # Click root post to open RHS
cy.get(`#post_${rootId}`).click();
// # Wait for RHS to open
cy.wait(TIMEOUTS.ONE_SEC);
// # Mark the thread as unread
cy.uiClickPostDropdownMenu(replyId, 'Mark as Unread', 'RHS_COMMENT');
// # Wait for unread to be marked correctly
cy.wait(TIMEOUTS.ONE_SEC);
// # Close RHS
cy.uiCloseRHS();
// # Switch to a different team
cy.apiCreateTeam('team', 'Team').then(({team: otherTeam}) => {
// # Reload the page to ensure newly created team is loaded
cy.reload();
// # Click on the other team button to switch teams
cy.get(`#${otherTeam.name}TeamButton`).click();
// * Verify mention count on the original team
cy.get(`#${testTeam.name}TeamButton`).find('.badge').should('be.visible');
// # Click on the original team button to switch back
cy.get(`#${testTeam.name}TeamButton`).click();
// # Unfollow the thread
cy.uiGetPostThreadFooter(rootId).findByText('Following').click();
// # Switch to a different team and back
cy.get(`#${otherTeam.name}TeamButton`).click();
cy.get(`#${testTeam.name}TeamButton`).click();
cy.get(`#${otherTeam.name}TeamButton`).click();
// * Verify there is no mention count on the original team
cy.get(`#${testTeam.name}TeamButton`).find('.badge').should('not.exist');
});
});
});
});
});