snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s

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

View File

@@ -0,0 +1,75 @@
// 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 @mark_as_unread
import {markAsUnreadShouldBeAbsent} from './helpers';
describe('Channels', () => {
let testUser;
let testChannel;
let testTeam;
let post1;
before(() => {
cy.apiInitSetup().then(({team, user}) => {
testUser = user;
testTeam = team;
// # Create a test channel
cy.apiCreateChannel(team.id, 'channel-test', 'Channel').then(({channel}) => {
testChannel = channel;
// # Create second user and add him to the team
cy.apiCreateUser().then(({user: user2}) => {
const otherUser = user2;
cy.apiAddUserToTeam(team.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, otherUser.id);
// Another user creates posts in the channel since you can't mark your own posts unread currently
cy.postMessageAs({sender: otherUser, message: 'post1', channelId: testChannel.id}).then((p1) => {
post1 = p1;
});
});
});
});
});
});
it('MM-T263 Mark as Unread post menu option should not be available for archived channels', () => {
// # Login as testUser
cy.apiLogin(testUser);
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Click channel header to open channel menu
cy.get('#channelHeaderTitle').should('contain', testChannel.display_name).click();
// * Verify that the menu is opened
cy.get('#channelHeaderDropdownMenu').should('be.visible').within(() => {
// # Archive the channel
cy.findByText('Archive Channel').should('be.visible').click();
});
// * Verify that the delete/archive channel modal is opened
cy.get('#deleteChannelModal').should('be.visible').within(() => {
// # Confirm archive
cy.findByText('Archive').should('be.visible').click();
});
// * Verify the "Mark as Unread" option is absent in post menu
markAsUnreadShouldBeAbsent(post1.id);
// * Verify the post is not marked as unread
cy.get('.NotificationSeparator').should('not.exist');
});
});

View File

@@ -0,0 +1,61 @@
// 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 @mark_as_unread
import {markAsUnreadFromPost, verifyPostNextToNewMessageSeparator} from './helpers';
import {beUnread} from '@/support/assertions';
describe('Bot post unread message', () => {
let newChannel;
let botPost;
let testTeam;
before(() => {
// # Create and visit new channel
cy.apiInitSetup({
promoteNewUserAsAdmin: true,
loginAfter: true,
}).then(({team, channel}) => {
testTeam = team;
newChannel = channel;
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
});
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
cy.apiPatchUserRoles(botUserId, ['system_user system_post_all system_admin']);
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {
//# Add bot to team
cy.apiAddUserToTeam(newChannel.team_id, botUserId);
// # Post message as bot to the new channel
cy.postBotMessage({token, channelId: newChannel.id, message: 'this is bot message'}).then((res) => {
botPost = res.data;
});
});
});
});
it('MM-T252 bot post unread', () => {
// # Mark the bot post as unread
markAsUnreadFromPost(botPost);
// * Verify the channel is unread in LHS
cy.visit(`/${testTeam.name}/channels/town-square`);
cy.get(`#sidebarItem_${newChannel.name}`).should(beUnread).click();
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator('this is bot message');
});
});

View File

@@ -0,0 +1,132 @@
// 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 @channel
import {verifyPostNextToNewMessageSeparator, switchToChannel} from './helpers';
import {beRead, beUnread} from '@/support/assertions';
describe('channel unread posts', () => {
let testUser;
let otherUser;
let channelA;
let channelB;
beforeEach(() => {
cy.apiAdminLogin();
// # Create testUser added to channel
cy.apiInitSetup().then(({team, channel, user}) => {
testUser = user;
channelA = channel;
// # Create second channel and add testUser
cy.apiCreateChannel(team.id, 'channel-b', 'Channel B').then((out) => {
channelB = out.channel;
cy.apiAddUserToChannel(channelB.id, testUser.id);
});
// # Create otherUser, add to team, and add to both channelA and channelB
cy.apiCreateUser().then(({user: user2}) => {
otherUser = user2;
cy.apiAddUserToTeam(team.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(channelA.id, otherUser.id);
cy.apiAddUserToChannel(channelB.id, otherUser.id);
});
for (let index = 0; index < 5; index++) {
// # Post Message as Current user
const message = `hello from current user: ${index}`;
cy.postMessageAs({sender: testUser, message, channelId: channelA.id});
}
});
cy.apiLogin(testUser);
cy.visit(`/${team.name}/channels/town-square`);
});
});
it('MM-T256 Mark unread before a page of message in Channel', () => {
// # Login as other user
cy.apiLogin(otherUser);
// # Switch to channelA
switchToChannel(channelA);
for (let index = 5; index < 40; index++) {
// # Post Message as Current user
const message = `hello from current user: ${index}`;
cy.postMessageAs({sender: testUser, message, channelId: channelA.id});
}
// # Mark the post which is one page above from bottom as unread
cy.getNthPostId(6).then((postId) => {
cy.get(`#post_${postId}`).scrollIntoView().should('be.visible');
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator('hello from current user: 5');
// # Switch to channelB
switchToChannel(channelB);
// # Switch to channelA
switchToChannel(channelA);
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator('hello from current user: 5');
});
it('MM-T259 Mark as Unread channel remains unread when receiving new message', () => {
// # Login as other user
cy.apiLogin(otherUser);
// # Switch to channelA
switchToChannel(channelA);
// # Mark the last post as unread
cy.getLastPostId().then((postId) => {
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator('hello from current user: 4');
// * Verify the channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
for (let index = 5; index < 10; index++) {
// # Post Message as Current user
const message = `hello from current user: ${index}`;
cy.postMessageAs({sender: testUser, message, channelId: channelA.id});
// * Verify the channelA has unread in LHS even when the messages are coming through
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
}
// # Switch to channelB
switchToChannel(channelB);
// * Verify the channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// # Switch to channelA
switchToChannel(channelA);
// * Verify the channelA has does not have unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
});
});

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '@/fixtures/timeouts';
export function markAsUnreadFromPost(post, rhs = false) {
const prefix = rhs ? 'rhsPost' : 'post';
cy.get(`#${prefix}_${post.id}`).scrollIntoView().should('be.visible');
cy.get('body').type('{alt}', {release: false});
cy.get(`#${prefix}_${post.id}`).click({force: true});
cy.get('body').type('{alt}', {release: true});
}
export function markAsUnreadShouldBeAbsent(postId, prefix = 'post', location = 'CENTER') {
cy.get(`#${prefix}_${postId}`).trigger('mouseover');
cy.clickPostDotMenu(postId, location);
cy.get(`#CENTER_dropdown_${postId}`).
should('be.visible').
within(() => {
cy.findByText('Mark as Unread').should('not.exist');
});
cy.get('body').type('esc');
}
export function switchToChannel(channel) {
cy.get(`#sidebarItem_${channel.name}`).click();
cy.get('#channelHeaderTitle', {timeout: TIMEOUTS.ONE_MIN}).should('contain', channel.display_name);
// # Wait some time for the channel to set state
cy.wait(TIMEOUTS.HALF_SEC);
}
export function verifyPostNextToNewMessageSeparator(message) {
cy.get('.NotificationSeparator').
should('exist').
parent().
parent().
parent().
next().
should('contain', message);
}
export function verifyTopSpaceForNewMessage(message) {
cy.get('.post-row__padding.top').
should('be.visible').
should('contain', message);
}
export function verifyBottomSpaceForNewMessage(message) {
cy.get('.post-row__padding.bottom').
should('be.visible').
should('contain', message);
}
export function showCursor(items) {
cy.expect(items).to.have.length(1);
expect(items[0].className).to.match(/cursor--pointer/);
}
export function notShowCursor(items) {
cy.expect(items).to.have.length(1);
expect(items[0].className).to.not.match(/cursor--pointer/);
}

View File

@@ -0,0 +1,81 @@
// 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 @mark_as_unread
import {markAsUnreadFromPost, switchToChannel} from './helpers';
import {beRead, beUnread} from '@/support/assertions';
describe('Leaving channel', () => {
let testUser;
let otherUser;
let channelA;
let channelB;
let post1;
beforeEach(() => {
cy.visit('/');
cy.apiAdminLogin();
cy.apiInitSetup().then(({team, channel, user}) => {
testUser = user;
channelA = channel;
cy.apiCreateChannel(team.id, 'channel-b', 'Channel B').then((out) => {
channelB = out.channel;
cy.apiAddUserToChannel(channelB.id, testUser.id);
});
cy.apiCreateUser().then(({user: user2}) => {
otherUser = user2;
cy.apiAddUserToTeam(team.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(channelA.id, otherUser.id);
cy.postMessageAs({
sender: otherUser,
message: 'post1',
channelId: channelA.id,
}).then((p1) => {
post1 = p1;
});
});
});
cy.apiLogin(testUser);
cy.visit(`/${team.name}/channels/town-square`);
});
});
it('MM-T2924_1 Channel is marked as read as soon as user leaves', () => {
switchToChannel(channelA);
cy.postMessageAs({sender: otherUser, message: 'post2', channelId: channelA.id});
switchToChannel(channelB);
// * Verify that channelA does not have unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
});
it('MM-T2924_2 Channel is left unread if post is manually marked as unread and user leaves', () => {
switchToChannel(channelA);
markAsUnreadFromPost(post1);
switchToChannel(channelB);
// * Verify that channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
});
});

View File

@@ -0,0 +1,422 @@
// 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 @mark_as_unread
import {verifyPostNextToNewMessageSeparator, verifyTopSpaceForNewMessage, verifyBottomSpaceForNewMessage, switchToChannel, showCursor, notShowCursor} from './helpers';
import {beRead, beUnread} from '@/support/assertions';
describe('Mark as Unread', () => {
let testUser;
let team1;
let channelA;
let channelB;
let post1;
let post2;
let post3;
beforeEach(() => {
cy.apiAdminLogin();
cy.apiInitSetup().then(({team, channel, user}) => {
team1 = team;
testUser = user;
channelA = channel;
cy.apiCreateChannel(team1.id, 'channel-b', 'Channel B').then((out) => {
channelB = out.channel;
cy.apiAddUserToChannel(channelB.id, testUser.id);
});
cy.apiCreateUser().then(({user: user2}) => {
const otherUser = user2;
cy.apiAddUserToTeam(team1.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(channelA.id, otherUser.id);
// Another user creates posts in the channel since you can't mark your own posts unread currently
cy.postMessageAs({
sender: otherUser,
message: 'post1',
channelId: channelA.id,
}).then((p1) => {
post1 = p1;
cy.postMessageAs({
sender: otherUser,
message: 'post2',
channelId: channelA.id,
}).then((p2) => {
post2 = p2;
cy.postMessageAs({
sender: otherUser,
message: 'post3',
channelId: channelA.id,
rootId: post1.id,
}).then((post) => {
post3 = post;
});
});
});
});
});
cy.apiLogin(testUser);
cy.visit(`/${team1.name}/channels/town-square`);
});
});
it('Channel should appear unread after switching away from channel and be read after switching back', () => {
// Starts unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
// Then becomes read when you view the channel
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
markAsUnreadFromPost(post2);
// Then becomes unread when the channel is marked as unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelB);
// Then stays unread when switching away
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
// And becomes read when switching back
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
});
it('MM-T5223 The latest post should appear unread after marking the channel as unread', () => {
// channelA starts unread, then becomes read after you viewed
// and switched to channelB
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
switchToChannel(channelB);
// After mark channelA with the LHS mark as unread option, channelA
// should appear unread
cy.get(`#sidebarItem_${channelA.name}`).find('.SidebarMenu').click({force: true});
cy.get(`#markAsUnread-${channelA.id}`).click();
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// After switching back to channelA,
// the New Messages line should appear above the last post (post3)
cy.get(`#sidebarItem_${channelA.name}`).click();
verifyPostNextToNewMessageSeparator('post3');
});
it('MM-T5224 The latest post should appear unread after marking the channel as unread with alt/option+left-click on channel sidebar item', () => {
// channelA starts unread, then becomes read after you viewed
// and switched to channelB
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
switchToChannel(channelB);
// After mark channelA with alt/option+left-click, channelA
// should appear unread
cy.get(`#sidebarItem_${channelA.name}`).type('{alt}', {release: false}).click();
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// After switching back to channelA,
// the New Messages line should appear above the last post (post3)
cy.get(`#sidebarItem_${channelA.name}`).click();
verifyPostNextToNewMessageSeparator('post3');
});
it('MM-T257 Mark as Unread when bringing window into focus', () => {
// * Verify channels are unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
cy.get(`#sidebarItem_${channelB.name}`).should(beUnread);
// # Navigate to integration screen (away from chat/main screen)
cy.visit(`/${team1.name}/integrations/`);
// # Navigate back to chat/main screen
cy.visit(`/${team1.name}/channels/town-square`);
// * Verify channels are unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
cy.get(`#sidebarItem_${channelB.name}`).should(beUnread);
switchToChannel(channelA);
switchToChannel(channelB);
// * Verify channel are read
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
cy.get(`#sidebarItem_${channelB.name}`).should(beRead);
});
it('New messages line should remain after switching back to channel', () => {
switchToChannel(channelA);
// Starts not visible
cy.get('.NotificationSeparator').should('not.exist');
markAsUnreadFromPost(post2);
// Then becomes visible
cy.get('.NotificationSeparator').should('exist');
switchToChannel(channelB);
switchToChannel(channelA);
// Then stays visible when switching back to channel
cy.get('.NotificationSeparator').should('exist');
switchToChannel(channelB);
switchToChannel(channelA);
// Then finally disappears when switching back a second time
cy.get('.NotificationSeparator').should('not.exist');
});
it('MM-T260 Mark as Unread New Messages line extra space moves with it', () => {
switchToChannel(channelA);
markAsUnreadFromPost(post2);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post2');
// Top separator space should appear above the selected post
verifyTopSpaceForNewMessage('post2');
// Bottom separator space should appear below the post
verifyBottomSpaceForNewMessage('post1');
markAsUnreadFromPost(post1);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post1');
// Top separator space should appear above the selected post
verifyTopSpaceForNewMessage('post1');
// Bottom separator space should appear below the post by user
verifyBottomSpaceForNewMessage('System');
markAsUnreadFromPost(post3);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post3');
// Top separator space should appear above the selected post
verifyTopSpaceForNewMessage('post3');
// Bottom separator space should appear below the post
verifyBottomSpaceForNewMessage('post2');
});
it('Should be able to mark channel as unread by alt-clicking on RHS', () => {
switchToChannel(channelA);
// Show the RHS
cy.get(`#CENTER_commentIcon_${post3.id}`).click({force: true});
markAsUnreadFromPost(post1, true);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post1');
markAsUnreadFromPost(post3, true);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post3');
});
it('Should show cursor pointer when holding down alt', () => {
const componentIds = [
`#post_${post1.id}`,
`#post_${post2.id}`,
`#post_${post3.id}`,
`#rhsPost_${post1.id}`,
`#rhsPost_${post3.id}`,
];
switchToChannel(channelA);
// Show the RHS
cy.get(`#CENTER_commentIcon_${post3.id}`).click({force: true});
// Pretend that we start hovering over each and then hold alt afterwards
for (const componentId of componentIds) {
// * Verify that we don't show the pointer on mouseover
cy.get(componentId).trigger('mouseover').should(notShowCursor);
// * Verify that we show the pointer after pressing alt
cy.get(componentId).trigger('keydown', {altKey: true}).should(showCursor);
// * Verify that we stop showing the pointer after releasing alt
cy.get(componentId).trigger('keydown', {altKey: false}).should(notShowCursor);
// # Move the mouse away from the post
cy.get(componentId).trigger('mouseout');
}
// Pretend that we hold down alt and then hover over each post
for (const componentId of componentIds) {
// * Verify that we don't show the pointer on mouseover
cy.get(componentId).trigger('mouseover', {altKey: true}).should(showCursor);
// # Move the mouse away from the post
cy.get(componentId).trigger('mouseout', {altKey: true}).should(notShowCursor);
}
});
it('Marking a channel as unread from another session while viewing channel', () => {
switchToChannel(channelA);
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
markAsUnreadFromAnotherSession(post2, testUser);
// The channel should now be unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post2');
switchToChannel(channelB);
// Then stays unread when switching away
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
// And becomes read when switching back
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post2');
});
it('Marking a channel as unread from another session while viewing another channel', () => {
switchToChannel(channelA);
switchToChannel(channelB);
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
markAsUnreadFromAnotherSession(post2, testUser);
// The channel should now be unread
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
switchToChannel(channelA);
// And becomes read when switching back
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post2');
});
it('MM-T244 Webapp: Post menu item `Mark as Unread` appearance', () => {
switchToChannel(channelA);
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
postMessage(post1.message);
cy.getLastPostId().then((postId) => {
cy.clickPostDotMenu(postId);
cy.get('ul.Menu__content.dropdown-menu').
as('menuOptions').
should('be.visible').
scrollIntoView();
// # Post menu item `Mark as Unread` should be visible
cy.get('@menuOptions').
find('[aria-label="Mark as Unread"]').
as('markAsReadElement').
should('be.visible');
// # Shrink the window and verify post menu options in the mobile view
cy.viewport('iphone-5');
cy.get('@markAsReadElement').should('be.visible');
// * Verify there are no extra divider lines at the bottom of the menu for the non-admin user
cy.findByText('Edit').should('not.exist');
cy.findByText('Delete').should('not.exist');
// * Verify there are extra divider lines at the bottom
cy.get('ul.Menu__content').find('li.MenuItem').each(($listElement) => {
cy.wrap($listElement).find('button').then(($buttonElement) => {
cy.wrap($buttonElement).invoke('attr', 'aria-label').then((ariaLabel) => {
if (ariaLabel !== 'Copy Text') {
cy.wrap($buttonElement).should('have.css', 'border-color', 'rgba(63, 67, 80, 0.12)');
}
});
});
});
});
});
it('Should be able to mark channel as unread from post menu', () => {
switchToChannel(channelA);
// # Mark post2 as unread
cy.uiClickPostDropdownMenu(post2.id, 'Mark as Unread');
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post2');
// # Mark post1 as unread
cy.uiClickPostDropdownMenu(post1.id, 'Mark as Unread');
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post1');
// # Mark post3 as unread
cy.uiClickPostDropdownMenu(post3.id, 'Mark as Unread');
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post3');
});
it('Should be able to mark channel as unread from RHS post menu', () => {
switchToChannel(channelA);
// Show the RHS
cy.get(`#CENTER_commentIcon_${post3.id}`).click({force: true});
// # Mark post1 as unread in RHS as root thread
cy.uiClickPostDropdownMenu(post1.id, 'Mark as Unread', 'RHS_ROOT');
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post1');
// # Mark post3 as unread in RHS as comment
cy.uiClickPostDropdownMenu(post3.id, 'Mark as Unread', 'RHS_COMMENT');
// The New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post3');
});
});
function markAsUnreadFromPost(post, rhs = false) {
const prefix = rhs ? 'rhsPost' : 'post';
cy.get('body').type('{alt}', {release: false});
cy.get(`#${prefix}_${post.id}`).click({force: true});
cy.get('body').type('{alt}', {release: true});
}
function markAsUnreadFromAnotherSession(post, user) {
cy.externalRequest({
user,
method: 'post',
path: `users/${user.id}/posts/${post.id}/set_unread`,
});
}

View File

@@ -0,0 +1,94 @@
// 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 @mark_as_unread
import {verifyPostNextToNewMessageSeparator, switchToChannel, showCursor} from './helpers';
import {beRead, beUnread} from '@/support/assertions';
describe('Mark as Unread', () => {
let testUser;
let channelA;
let channelB;
let post1;
let post2;
before(() => {
cy.apiInitSetup().then(({team, channel, user}) => {
testUser = user;
channelA = channel;
// # Create second channel and add testUser
cy.apiCreateChannel(team.id, 'channel-b', 'Channel B').then((out) => {
channelB = out.channel;
cy.apiAddUserToChannel(channelB.id, testUser.id);
});
// # Create second user and add him to the team
cy.apiCreateUser().then(({user: user2}) => {
const otherUser = user2;
cy.apiAddUserToTeam(team.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(channelA.id, otherUser.id);
// Another user creates posts in the channel since you can't mark your own posts unread currently
cy.postMessageAs({sender: otherUser, message: 'post1', channelId: channelA.id}).then((p1) => {
post1 = p1;
cy.postMessageAs({sender: otherUser, message: 'post2', channelId: channelA.id}).then((p2) => {
post2 = p2;
});
});
});
});
cy.apiLogin(testUser);
cy.visit(`/${team.name}/channels/${channelA.name}`);
});
});
it('MM-T251 using shortcuts to make post unread', () => {
// * Hover on the post with holding alt should show cursor
cy.get(`#post_${post2.id}`).trigger('mouseover').type('{alt}', {release: false}).should(showCursor);
// # Mouse click on the post
cy.get(`#post_${post2.id}`).click();
// * Verify the post is marked as unread
verifyPostNextToNewMessageSeparator('post2');
// * Verify the channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// # Switch to channelB
switchToChannel(channelB);
// # Switch to channelA
switchToChannel(channelA);
// * Verify the channelB does not have unread in LHS
cy.get(`#sidebarItem_${channelB.name}`).should(beRead);
// # Open RHS (reply thread)
cy.clickPostCommentIcon(post1.id);
// # Mark the post as unread from RHS
cy.uiClickPostDropdownMenu(post1.id, 'Mark as Unread', 'RHS_ROOT');
// * Verify the New Messages line should appear above the selected post
verifyPostNextToNewMessageSeparator('post1');
// * Verify the channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
});
});

View File

@@ -0,0 +1,92 @@
// 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 @mark_as_unread
import {verifyPostNextToNewMessageSeparator} from './helpers';
import * as TIMEOUTS from '@/fixtures/timeouts';
describe('Mark as Unread', () => {
let testUser;
let otherUser1;
let otherUser2;
before(() => {
// # Create testUser added to channel
cy.apiInitSetup().then(({team, user}) => {
testUser = user;
// # Create second user and add to the team
cy.apiCreateUser({prefix: 'otherA'}).then(({user: newUser}) => {
otherUser1 = newUser;
cy.apiAddUserToTeam(team.id, newUser.id);
});
// # Create third user and add to the team
cy.apiCreateUser({prefix: 'otherB'}).then(({user: newUser}) => {
otherUser2 = newUser;
cy.apiAddUserToTeam(team.id, newUser.id);
});
// # Login as test user and go to town square
cy.apiLogin(testUser);
cy.visit(`/${team.name}/channels/town-square`);
});
});
it('MM-T249 Mark GM post as unread', () => {
const userGroupIds = [testUser.id, otherUser1.id, otherUser2.id];
// # Create a group channel for 3 users
cy.apiCreateGroupChannel(userGroupIds).then(({channel: gmChannel}) => {
// # Visit the channel using the name using the channels route
for (let index = 0; index < 8; index++) {
// # Post Message as otherUser1
cy.postMessageAs({sender: otherUser1, message: `this is from user: ${otherUser1.id}, ${index}`, channelId: gmChannel.id});
// # Post Message as otherUser2
cy.postMessageAs({sender: otherUser2, message: `this is from user: ${otherUser2.id}, ${index}`, channelId: gmChannel.id});
}
// # Go to the group message channel
cy.get(`#sidebarItem_${gmChannel.name}`).click();
cy.reload();
// # Mark the post to be unread
cy.getNthPostId(-2).then((postId) => {
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator(`this is from user: ${otherUser1.id}, 7`);
// * Verify the group message in LHS is unread
cy.get(`#sidebarItem_${gmChannel.name}`).should('have.attr', 'aria-label', `${otherUser1.username}, ${otherUser2.username} 2 mentions`);
// # Leave the group message channel
cy.get('#sidebarItem_town-square').click();
// * Verify the group message in LHS is unread
cy.get(`#sidebarItem_${gmChannel.name}`).should('have.attr', 'aria-label', `${otherUser1.username}, ${otherUser2.username} 2 mentions`);
// # Go to the group message channel
cy.get(`#sidebarItem_${gmChannel.name}`).click().wait(TIMEOUTS.ONE_SEC);
// * Verify the group message in LHS is read
cy.get(`#sidebarItem_${gmChannel.name}`).should('exist').should('not.have.attr', 'aria-label', `${otherUser1.username}, ${otherUser2.username} 2 mentions`);
// * Verify the notification separator line exists and present before the unread message
verifyPostNextToNewMessageSeparator(`this is from user: ${otherUser1.id}, 7`);
});
});
});

View File

@@ -0,0 +1,186 @@
// 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 @mark_as_unread
import {verifyPostNextToNewMessageSeparator, switchToChannel} from './helpers';
import {beUnread} from '@/support/assertions';
describe('Mark post with mentions as unread', () => {
let userA;
let userB;
let channelA;
let channelB;
let testTeam;
before(() => {
cy.apiInitSetup().then(({team, user, channel}) => {
userA = user;
channelA = channel;
testTeam = team;
// # Create second channel and add userA
cy.apiCreateChannel(team.id, 'channel-b', 'Channel B').then((out) => {
channelB = out.channel;
cy.apiAddUserToChannel(channelB.id, userA.id);
});
// # Create a second user
cy.apiCreateUser().then(({user: user2}) => {
userB = user2;
// # Add userB to channel team and channels
cy.apiAddUserToTeam(testTeam.id, userB.id).then(() => {
cy.apiAddUserToChannel(channelA.id, userB.id);
cy.apiAddUserToChannel(channelB.id, userB.id);
});
cy.visit(`/${testTeam.name}/channels/town-square`);
});
});
});
it('MM-T247 Marks posts with mentions as unread', () => {
// # Login as userB
cy.apiLogin(userB);
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Navigate to both channels, so the new messages line appears above posts
switchToChannel(channelA);
switchToChannel(channelB);
// # Mention userB in channel A as userA
cy.postMessageAs({
sender: userA,
message: `@${userB.username} : hello1`,
channelId: channelA.id,
});
// * Verify that channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// * Verify that ChannelA has unread mention in LHS
cy.get(`#sidebarItem_${channelA.name}`).children('#unreadMentions').should('have.text', '1');
// # Navigate to channel A
switchToChannel(channelA);
// * Verify that ChannelA no longer has unread mention in LHS
cy.get(`#sidebarItem_${channelA.name}`).children('#unreadMentions').should('not.exist');
// * Verify that new message separator exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello1`);
// # Navigate to channel B
switchToChannel(channelB);
// # Navigate back to channel A
switchToChannel(channelA);
// * Verify that new message separator is gone
cy.get('.NotificationSeparator').should('not.exist');
// # Mention userB in channel B as userA
cy.postMessageAs({
sender: userA,
message: `@${userB.username} : hello2`,
channelId: channelB.id,
});
// # Navigate to channel B
switchToChannel(channelB);
// * Verify that new message separator exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello2`);
// # Refresh the page
cy.reload();
// * Verify the new message separator is gone
cy.get('.NotificationSeparator').should('not.exist');
// # Mention userB in channelA as userA
cy.postMessageAs({
sender: userA,
message: `@${userB.username} : hello3`,
channelId: channelA.id,
});
// # Navigate back to channel A
switchToChannel(channelA);
// * Verify the new message separator exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello3`);
// # Get the ID of the last post
cy.getLastPostId().then((postId) => {
// # Mark last post as unread from menu
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Verify the new message separator still exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello3`);
// * Verify that channelA has unread in LHS
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
// * Verify that ChannelA has unread mention in LHS
cy.get(`#sidebarItem_${channelA.name}`).children('#unreadMentions').should('have.text', '1');
// # Navigate to channel B
switchToChannel(channelB);
// # Navigate back to channel A
switchToChannel(channelA);
// * Verify the new message separator still exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello3`);
// * Verify that ChannelA no longer has unread mention in LHS
cy.get(`#sidebarItem_${channelA.name}`).children('#unreadMentions').should('not.exist');
// # Mention userB in channelB as userA
cy.postMessageAs({
sender: userA,
message: `@${userB.username} : hello4`,
channelId: channelB.id,
});
// # Navigate to channel B
switchToChannel(channelB);
// # Get the ID of the last post
cy.getLastPostId().then((postId) => {
// # Mark last post as unread from menu
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Verify the new message separator exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello4`);
// * Verify that ChannelB has unread mention in LHS
cy.get(`#sidebarItem_${channelB.name}`).children('#unreadMentions').should('have.text', '1');
// # Refresh the page
cy.reload();
// * Verify the new message separator still exists above the unread message
verifyPostNextToNewMessageSeparator(`@${userB.username} : hello4`);
// # Navigate to channel A
switchToChannel(channelA);
// * Verify that ChannelB no longer has unread mention in LHS
cy.get(`#sidebarItem_${channelB.name}`).children('#unreadMentions').should('not.exist');
});
});

View File

@@ -0,0 +1,112 @@
// 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 @mark_as_unread
import {markAsUnreadFromPost, switchToChannel} from './helpers';
import * as TIMEOUTS from '@/fixtures/timeouts';
describe('Verify unread toast appears after repeated manual marking post as unread', () => {
let firstPost;
let secondPost;
const offTopicChannel = {name: 'off-topic', display_name: 'Off-Topic'};
let testChannel;
before(() => {
cy.apiInitSetup().then(({team, user, channel}) => {
testChannel = channel;
cy.apiCreateUser({prefix: 'other'}).then(({user: otherUser}) => {
cy.apiAddUserToTeam(team.id, otherUser.id).then(() => {
cy.apiAddUserToChannel(testChannel.id, otherUser.id);
// Toast only seems to appear after first visiting the channel
// So we need to visit the channel then navigate away
cy.apiLogin(user);
cy.visit(`/${team.name}/channels/${testChannel.name}`);
switchToChannel(offTopicChannel);
// Ensure that the Off-Topic channel has loaded successfully, before posting messages in the other channel
cy.get('#channelHeaderTitle').should('be.visible').and('contain.text', offTopicChannel.display_name);
cy.wait(TIMEOUTS.ONE_SEC).postMessageAs({
sender: otherUser,
message: 'First message',
channelId: testChannel.id,
}).then((post) => {
firstPost = post;
cy.postMessageAs({
sender: otherUser,
message: 'Second message',
channelId: testChannel.id,
}).then((post2) => {
secondPost = post2;
// Add posts so that scroll is available
Cypress._.times(30, (index) => {
cy.postMessageAs({
sender: otherUser,
message: `${index.toString()}\nsecond line\nthird line\nfourth line`,
channelId: testChannel.id,
});
});
});
});
});
});
});
});
it('MM-T1429 Toast when navigating to channel with unread messages and after repeated marking as unread', () => {
// # Switch to town square channel that has unread messages
switchToChannel(testChannel);
// * Check that the toast is visible
cy.get('div.toast').should('be.visible');
// # Scroll to the bottom of the posts
cy.get('.post-list__dynamic').scrollTo('bottom');
// * Check that the toast is not visible
cy.get('div.toast').should('not.exist');
// # Mark the first post as unread
markAsUnreadFromPost(firstPost);
// * Check that the toast is now visible
cy.get('div.toast').should('be.visible');
// # Scroll to the bottom of the posts
cy.get('.post-list__dynamic').scrollTo('bottom');
// * Check that the toast is not visible
cy.get('div.toast').should('not.exist');
// # Mark the second post as unread
markAsUnreadFromPost(secondPost);
// * Check that the toast is now visible
cy.get('div.toast').should('be.visible');
// # Switch channels
switchToChannel(offTopicChannel);
// * Check that the toast is not visible
cy.get('div.toast').should('not.exist');
// # Switch channels back
switchToChannel(testChannel);
// * Check that the toast is now visible
cy.get('div.toast').should('be.visible');
});
});

View File

@@ -0,0 +1,75 @@
// 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 @mark_as_unread
describe('Verify unread toast appears after repeated manual marking post as unread', () => {
let currentChannel;
before(() => {
cy.apiInitSetup().then(({team, channel, user}) => {
currentChannel = channel;
cy.apiCreateUser().then(({user: user2}) => {
cy.apiAddUserToTeam(team.id, user2.id).then(() => {
cy.apiAddUserToChannel(channel.id, user2.id);
// # Create a page full of messages
Cypress._.times(30, (i) => {
cy.postMessageAs({
sender: user2,
message: `post${i}`,
channelId: channel.id,
});
});
// # Make a mentioned message
cy.postMessageAs({
sender: user2,
message: `hi @${user.username}`,
channelId: channel.id,
});
// # Login as user
cy.apiLogin(user);
// # Visit the channel
cy.visit(`/${team.name}/channels/${channel.name}`);
// # Post a reply on RHS
cy.getLastPostId().then((postId) => {
cy.clickPostCommentIcon(postId);
cy.get('#rhsContainer').should('be.visible');
const replyMessage = 'A reply to an older post';
cy.postMessageReplyInRHS(replyMessage);
});
});
});
});
});
it('MM-T254 Rehydrate mention badge after post is marked as Unread', () => {
// # Mark the first post as unread
cy.getNthPostId(1).then((postId) => {
cy.get(`#post_${postId}`).scrollIntoView().should('be.visible');
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
});
// * Check that the toast is visible
cy.get('div.toast').should('be.visible').then(() => {
// * Check that the message is correct and all the mentions and replies are counted in toast
cy.get('div.toast__message>span').should('be.visible').contains('30 new messages');
cy.get(`#sidebarItem_${currentChannel.name}`).
scrollIntoView().
find('#unreadMentions').
should('be.visible').
and('have.text', '1');
});
});
});