mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
SEC-10587 E2E/Playwright: Migrate RFQA browser tests (batch 1, 1-20) (#37352)
This commit is contained in:
-37
@@ -1,37 +0,0 @@
|
||||
// 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 @keyboard_shortcuts¨
|
||||
|
||||
describe('Keyboard Shortcuts', () => {
|
||||
before(() => {
|
||||
cy.apiInitSetup().then(({channelUrl}) => {
|
||||
cy.visit(channelUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1242 - CTRL/CMD+K - Typed characters are not lost after switching channels', () => {
|
||||
const message = 'Hello World!';
|
||||
|
||||
// # Type CTRL/CMD+K to open 'Switch Channels'
|
||||
cy.uiGetPostTextBox().cmdOrCtrlShortcut('K');
|
||||
|
||||
// * Verify that the suggestion list is visible
|
||||
cy.get('#suggestionList').should('be.visible');
|
||||
|
||||
// # Type ENTER to switch to new channel
|
||||
cy.get('#quickSwitchInput').type('{enter}');
|
||||
|
||||
// # Typing message
|
||||
cy.get('body').type(message);
|
||||
|
||||
// * Textbox should have text equal to message
|
||||
cy.uiGetPostTextBox().should('have.text', message);
|
||||
});
|
||||
});
|
||||
-100
@@ -38,106 +38,6 @@ describe('Keyboard Shortcuts', () => {
|
||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||
});
|
||||
|
||||
it('MM-T1239 - CTRL+/ and CMD+/ and /shortcuts', () => {
|
||||
// # Type CTRL/CMD+/
|
||||
cy.uiGetPostTextBox().cmdOrCtrlShortcut('/');
|
||||
|
||||
// # Verify that the 'Keyboard Shortcuts' modal is open
|
||||
modalShouldOpen();
|
||||
|
||||
// # Verify that the 'Keyboard Shortcuts' modal displays the CTRL/CMD+U shortcut
|
||||
cy.get('.section').eq(2).within(() => {
|
||||
cy.findByText('Files').should('be.visible');
|
||||
cy.get('.shortcut-line').should('be.visible').as('shortcutLine');
|
||||
if (isMac()) {
|
||||
cy.get('@shortcutLine').findByText('⌘').should('be.visible');
|
||||
} else {
|
||||
cy.get('@shortcutLine').findByText('Ctrl').should('be.visible');
|
||||
}
|
||||
cy.get('@shortcutLine').findByText('U').should('be.visible');
|
||||
});
|
||||
|
||||
// # Type CTRL/CMD+/ to close the 'Keyboard Shortcuts' modal
|
||||
cy.get('body').cmdOrCtrlShortcut('/');
|
||||
cy.get('#shortcutsModalLabel').should('not.exist');
|
||||
|
||||
// # Type /shortcuts
|
||||
cy.uiGetPostTextBox().clear().type('/shortcuts{enter}');
|
||||
modalShouldOpen();
|
||||
|
||||
// # Close the 'Keyboard Shortcuts' modal using the x button
|
||||
cy.get('.modal-header button.close').should('have.attr', 'aria-label', 'Close').click();
|
||||
cy.get('#shortcutsModalLabel').should('not.exist');
|
||||
|
||||
// # Type /shortcuts
|
||||
cy.uiGetPostTextBox().clear().type('/shortcuts{enter}');
|
||||
|
||||
// # Close the 'Keyboard Shortcuts' modal by pressing ESC key
|
||||
cy.get('body').type('{esc}');
|
||||
cy.get('#shortcutsModalLabel').should('not.exist');
|
||||
});
|
||||
|
||||
it('MM-T1254 - CTRL/CMD+UP; CTRL/CMD+DOWN', () => {
|
||||
const messagePrefix = 'hello from current user: ';
|
||||
let message;
|
||||
const count = 5;
|
||||
|
||||
// # Post messages to the center channel
|
||||
for (let index = 0; index < count; index++) {
|
||||
message = messagePrefix + index;
|
||||
cy.postMessage(message);
|
||||
}
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
// # Type CTRL/CMD+UP
|
||||
cy.uiGetPostTextBox().cmdOrCtrlShortcut('{uparrow}');
|
||||
|
||||
// # Verify that the previous message is displayed
|
||||
message = messagePrefix + (4 - index);
|
||||
cy.uiGetPostTextBox().contains(message);
|
||||
}
|
||||
|
||||
// # One extra CTRL/CMD+UP does not change the displayed message
|
||||
cy.uiGetPostTextBox().cmdOrCtrlShortcut('{uparrow}');
|
||||
message = messagePrefix + '0';
|
||||
cy.uiGetPostTextBox().contains(message);
|
||||
|
||||
for (let index = 1; index < count; index++) {
|
||||
// # Type CTRL/CMD+DOWN
|
||||
cy.uiGetPostTextBox().cmdOrCtrlShortcut('{downarrow}');
|
||||
|
||||
// # Verify that the next message is displayed
|
||||
message = messagePrefix + index;
|
||||
cy.uiGetPostTextBox().contains(message);
|
||||
}
|
||||
});
|
||||
|
||||
it('MM-T1260 - UP arrow', () => {
|
||||
const message = 'Test';
|
||||
const editMessage = 'Edit Test';
|
||||
|
||||
// # Post message text
|
||||
cy.uiGetPostTextBox().clear().type(message).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Edit previous post
|
||||
cy.getLastPostId().then(() => {
|
||||
cy.uiGetPostTextBox().type('{uparrow}');
|
||||
|
||||
// * Edit Post Input should appear
|
||||
cy.get('#edit_textbox').should('be.visible');
|
||||
|
||||
// * Edit to the post message and type ENTER
|
||||
cy.get('#edit_textbox').invoke('val', '').clear().type(editMessage).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
});
|
||||
|
||||
cy.getLastPostId().then((postId) => {
|
||||
// * Post should have "Edited"
|
||||
cy.get(`#postEdited_${postId}`).
|
||||
should('be.visible').
|
||||
should('contain', 'Edited');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1273 - @[character]+TAB', () => {
|
||||
const userName = `${testUser.username}`;
|
||||
|
||||
|
||||
@@ -56,43 +56,6 @@ describe('channel unread posts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T246 Mark Post as Unread', () => {
|
||||
// # 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');
|
||||
|
||||
// # 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);
|
||||
|
||||
// * Verify the notification separator line exists and present before the unread message
|
||||
verifyPostNextToNewMessageSeparator('hello from current user: 4');
|
||||
|
||||
// # Switch to channelB
|
||||
switchToChannel(channelB);
|
||||
|
||||
// * Verify the channelA has does not have unread in LHS
|
||||
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
|
||||
});
|
||||
|
||||
it('MM-T256 Mark unread before a page of message in Channel', () => {
|
||||
// # Login as other user
|
||||
cy.apiLogin(otherUser);
|
||||
|
||||
@@ -403,58 +403,6 @@ describe('Mark as Unread', () => {
|
||||
// The New Messages line should appear above the selected post
|
||||
verifyPostNextToNewMessageSeparator('post3');
|
||||
});
|
||||
|
||||
it('MM-T250 Mark as unread in the RHS', () => {
|
||||
switchToChannel(channelA);
|
||||
|
||||
// # 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);
|
||||
|
||||
// * Verify the RHS does not have the NotificationSeparator line
|
||||
cy.get('#rhsContainer').find('.NotificationSeparator').should('not.exist');
|
||||
|
||||
// # Switch to channelB
|
||||
switchToChannel(channelB);
|
||||
|
||||
// # Switch to channelA
|
||||
switchToChannel(channelA);
|
||||
|
||||
// * Verify the channelA does not have unread in LHS
|
||||
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
|
||||
|
||||
// * 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 holding alt
|
||||
cy.get(`#post_${post2.id}`).type('{alt}', {release: false}).click();
|
||||
|
||||
// * Verify the post is marked as unread
|
||||
verifyPostNextToNewMessageSeparator('post2');
|
||||
|
||||
// * Verify the channelA has unread in LHS
|
||||
cy.get(`#sidebarItem_${channelA.name}`).should(beUnread);
|
||||
|
||||
// * Verify the RHS does not have the NotificationSeparator line
|
||||
cy.get('#rhsContainer').find('.NotificationSeparator').should('not.exist');
|
||||
|
||||
// # Switch to channelB
|
||||
switchToChannel(channelB);
|
||||
|
||||
// # Switch to channelA
|
||||
switchToChannel(channelA);
|
||||
|
||||
// * Verify the channelA does not have unread in LHS
|
||||
cy.get(`#sidebarItem_${channelA.name}`).should(beRead);
|
||||
});
|
||||
});
|
||||
|
||||
function markAsUnreadFromPost(post, rhs = false) {
|
||||
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
// 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 @messaging
|
||||
|
||||
import {verifyPostNextToNewMessageSeparator} from './helpers';
|
||||
|
||||
describe('Mark DM post as Unread ', () => {
|
||||
beforeEach(function() {
|
||||
cy.apiAdminLogin().
|
||||
apiCreateUser().
|
||||
its('user').as('userA').
|
||||
apiInitSetup().
|
||||
then(({user, team}) => cy.
|
||||
apiAddUserToTeam(team.id, this.userA.id).
|
||||
apiCreateDirectChannel([user.id, this.userA.id]).its('channel').as('dmChannel').
|
||||
wrap(user).as('mainUser').
|
||||
wrap(team).as('team').
|
||||
wrap(`/${team.name}/messages/@${this.userA.username}`).as('link'),
|
||||
);
|
||||
});
|
||||
|
||||
it('MM-T248_1 Mark DM post as Unread', function() {
|
||||
const NUMBER_OF_USER_A_UNREAD_MESSAGES = 4;
|
||||
|
||||
// # Post initial message from main user
|
||||
cy.postMessageAs({
|
||||
sender: this.mainUser,
|
||||
message: 'Initial message',
|
||||
channelId: this.dmChannel.id,
|
||||
});
|
||||
|
||||
// # Post several messages from User A
|
||||
cy.postListOfMessages({
|
||||
numberOfMessages: 3,
|
||||
sender: this.userA,
|
||||
channelId: this.dmChannel.id,
|
||||
});
|
||||
|
||||
// # Post messages from User A meant to be marked as unread
|
||||
cy.postMessageAs({
|
||||
sender: this.userA,
|
||||
message: 'Unread from here',
|
||||
channelId: this.dmChannel.id,
|
||||
}).as('unreadFromHere');
|
||||
cy.postListOfMessages({
|
||||
numberOfMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES - 1,
|
||||
sender: this.userA,
|
||||
channelId: this.dmChannel.id,
|
||||
});
|
||||
|
||||
// # Post more messages from main user
|
||||
cy.postListOfMessages({
|
||||
numberOfMessages: 3,
|
||||
sender: this.mainUser,
|
||||
channelId: this.dmChannel.id,
|
||||
});
|
||||
|
||||
// # Visit the DM channel and open the thread in RHS
|
||||
cy.apiLogin(this.mainUser).visit(this.link);
|
||||
|
||||
// # Мark the message from user A as unread
|
||||
cy.then(() => cy.uiClickPostDropdownMenu(this.unreadFromHere.id, 'Mark as Unread'));
|
||||
|
||||
// * Verify that new message separator exists above the unread messages
|
||||
cy.then(() => verifyPostNextToNewMessageSeparator(this.unreadFromHere.data.message));
|
||||
|
||||
// * Verify that DM-channel is marked as unread
|
||||
cy.then(() => verifyChannelIsMarkedUnreadInLHS(this.userA.username, {
|
||||
numberOfUnreadMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES,
|
||||
}));
|
||||
|
||||
// # Leave DM-channel
|
||||
cy.get('.SidebarChannel:contains(Off-Topic)').click();
|
||||
|
||||
// * Verify that DM-channel is still marked as unread, with the same "mention bubble"
|
||||
cy.then(() => verifyChannelIsMarkedUnreadInLHS(this.userA.username, {
|
||||
numberOfUnreadMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES,
|
||||
}));
|
||||
|
||||
// # Return to DM-channel
|
||||
cy.get(`.SidebarChannel:contains(${this.userA.username})`).click();
|
||||
|
||||
// * Verify that DM-channel is marked as read
|
||||
cy.then(() => verifyChannelIsMarkedReadInLHS(this.userA.username));
|
||||
|
||||
// * Verify that new message separator exists above the unread messages
|
||||
cy.then(() => verifyPostNextToNewMessageSeparator(this.unreadFromHere.data.message));
|
||||
});
|
||||
|
||||
it('MM-T248_2 Mark DM post as Unread in a reply thread', function() {
|
||||
const NUMBER_OF_USER_A_UNREAD_MESSAGES = 4;
|
||||
|
||||
// # Post initial message from main user
|
||||
cy.postMessageAs({
|
||||
sender: this.mainUser,
|
||||
message: 'Initial message',
|
||||
channelId: this.dmChannel.id,
|
||||
}).as('root');
|
||||
|
||||
// # Post several messages from User A
|
||||
cy.then(() => cy.postListOfMessages({
|
||||
numberOfMessages: 3,
|
||||
sender: this.userA,
|
||||
channelId: this.dmChannel.id,
|
||||
rootId: this.root.id,
|
||||
}));
|
||||
|
||||
// # Post messages from User A meant to be marked as unread
|
||||
cy.then(() => cy.postMessageAs({
|
||||
sender: this.userA,
|
||||
message: 'Unread from here',
|
||||
channelId: this.dmChannel.id,
|
||||
rootId: this.root.id,
|
||||
})).as('unreadFromHere');
|
||||
cy.then(() => cy.postListOfMessages({
|
||||
numberOfMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES - 1,
|
||||
sender: this.userA,
|
||||
channelId: this.dmChannel.id,
|
||||
rootId: this.root.id,
|
||||
}));
|
||||
|
||||
// # Post more messages from main user
|
||||
cy.then(() => cy.postListOfMessages({
|
||||
numberOfMessages: 3,
|
||||
sender: this.mainUser,
|
||||
channelId: this.dmChannel.id,
|
||||
rootId: this.root.id,
|
||||
}));
|
||||
|
||||
// # Visit the DM channel and open the thread in RHS
|
||||
cy.apiLogin(this.mainUser).visit(this.link);
|
||||
cy.get('@root').its('id').then(cy.clickPostCommentIcon);
|
||||
|
||||
// # Мark the message from user A as unread
|
||||
cy.then(() => cy.uiClickPostDropdownMenu(this.unreadFromHere.id, 'Mark as Unread', 'RHS_COMMENT'));
|
||||
|
||||
// * Verify that new message separator exists above the unread messages
|
||||
cy.then(() => verifyPostNextToNewMessageSeparator(this.unreadFromHere.data.message));
|
||||
|
||||
// * Verify that DM-channel is marked as unread
|
||||
cy.then(() => verifyChannelIsMarkedUnreadInLHS(this.userA.username, {
|
||||
numberOfUnreadMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES,
|
||||
}));
|
||||
|
||||
// # Leave DM-channel
|
||||
cy.get('.SidebarChannel:contains(Off-Topic)').click();
|
||||
|
||||
// * Verify that DM-channel is still marked as unread, with the same "mention bubble"
|
||||
cy.then(() => verifyChannelIsMarkedUnreadInLHS(this.userA.username, {
|
||||
numberOfUnreadMessages: NUMBER_OF_USER_A_UNREAD_MESSAGES,
|
||||
}));
|
||||
|
||||
// # Return to DM-channel
|
||||
cy.get(`.SidebarChannel:contains(${this.userA.username})`).click();
|
||||
|
||||
// * Verify that DM-channel is marked as read
|
||||
cy.then(() => verifyChannelIsMarkedReadInLHS(this.userA.username));
|
||||
|
||||
// * Verify that new message separator exists above the unread messages
|
||||
cy.then(() => verifyPostNextToNewMessageSeparator(this.unreadFromHere.data.message));
|
||||
});
|
||||
});
|
||||
|
||||
function verifyChannelIsMarkedUnreadInLHS(channelName, {numberOfUnreadMessages}) {
|
||||
cy.
|
||||
|
||||
// * Verify that DM-channel is marked as unread
|
||||
get('.SidebarChannelGroup_content').
|
||||
contains(channelName).
|
||||
parent().
|
||||
should('have.class', 'unread').
|
||||
|
||||
// * Verify that DM channel "mention bubble" contains number of unread messages except mainUser's ones
|
||||
find('.badge').
|
||||
should('contain.text', numberOfUnreadMessages);
|
||||
}
|
||||
|
||||
function verifyChannelIsMarkedReadInLHS(channelName) {
|
||||
cy.
|
||||
|
||||
// * Verify that DM-channel is marked as read
|
||||
get('.SidebarChannelGroup_content').
|
||||
contains(channelName).
|
||||
parent().
|
||||
should('not.have.class', 'unread').
|
||||
|
||||
// * Verify that DM channel "mention bubble" contains number of unread messages except mainUser's ones
|
||||
find('.badge').
|
||||
should('not.exist');
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
// 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 @messaging
|
||||
|
||||
function emojiVerification(postId) {
|
||||
// # set the postMessageTextId var
|
||||
const postMessageTextId = `#postMessageText_${postId}`;
|
||||
|
||||
// * Check for the emoji attr of :) is exists
|
||||
cy.get(`${postMessageTextId} p span span.emoticon`).should('have.attr', 'alt', ':slightly_smiling_face:');
|
||||
|
||||
// * Check for the punctuation('=') is exists without space
|
||||
cy.get(`${postMessageTextId} p`).should('same.text', ':slightly_smiling_face:=');
|
||||
}
|
||||
|
||||
describe('Messaging', () => {
|
||||
before(() => {
|
||||
// # Login as test user and visit off-topic
|
||||
cy.apiInitSetup({loginAfter: true}).then(({offTopicUrl}) => {
|
||||
cy.visit(offTopicUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T222 Emoji characters followed by punctuation', () => {
|
||||
// # Post a message
|
||||
const messageText = ':)=';
|
||||
cy.postMessage(messageText);
|
||||
|
||||
// # Get Last Post ID
|
||||
cy.getLastPostId().then((postId) => {
|
||||
emojiVerification(postId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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 @messaging
|
||||
|
||||
describe('Messaging', () => {
|
||||
before(() => {
|
||||
// # Login as test user and visit off-topic
|
||||
cy.apiInitSetup({loginAfter: true}).then(({offTopicUrl}) => {
|
||||
cy.visit(offTopicUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T95 Selecting an emoji from emoji picker should insert it at the cursor position', () => {
|
||||
// # Write some text in the send box.
|
||||
cy.uiGetPostTextBox().type('HelloWorld!');
|
||||
|
||||
// # Move the cursor to the middle of the text.
|
||||
cy.uiGetPostTextBox().type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}');
|
||||
|
||||
// # Open emoji picker
|
||||
cy.uiOpenEmojiPicker();
|
||||
|
||||
// # Select the grinning emoji from the emoji picker.
|
||||
cy.clickEmojiInEmojiPicker('grinning');
|
||||
|
||||
// * The emoji should be inserted as a Unicode character where the cursor is at the time of selection.
|
||||
cy.uiGetPostTextBox().should('have.value', 'Hello\uD83D\uDE00World!');
|
||||
cy.uiGetPostTextBox().type('{enter}');
|
||||
|
||||
// * The emoji should be displayed in the post at the position inserted.
|
||||
cy.getLastPost().find('p').should('contain', 'Hello').and('contain', 'World!');
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,6 @@
|
||||
|
||||
// Group: @channels @messaging
|
||||
|
||||
import * as TIMEOUTS from '@/fixtures/timeouts';
|
||||
import {spyNotificationAs} from '@/support/notification';
|
||||
|
||||
describe('Group Message', () => {
|
||||
let testTeam;
|
||||
let testUser;
|
||||
@@ -90,193 +87,4 @@ describe('Group Message', () => {
|
||||
// * Verify that the user is found and is part of the GM together with the other user
|
||||
cy.get('#moreDmModal .more-modal__row').should('be.visible').and('contain', otherUser2.username).and('contain', otherUser1.username);
|
||||
});
|
||||
|
||||
it('MM-T460 - Add and Remove users whilst creating a group message', () => {
|
||||
// # Create a group message with two other users
|
||||
createGroupMessageWith(users.slice(0, 2));
|
||||
|
||||
// # Open channel menu and click Add Members
|
||||
cy.uiOpenChannelMenu('Add Members');
|
||||
|
||||
// # Filter user by username
|
||||
cy.get('#selectItems input').typeWithForce(users[2].username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the first user on a filtered list
|
||||
cy.get('#multiSelectList .clickable').first().click();
|
||||
|
||||
// * Assert that member info updates to reflect the new addition
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', 'You can add 4 more people');
|
||||
|
||||
// # Click the first user on an unfiltered list
|
||||
cy.get('#multiSelectList .clickable').first().click();
|
||||
|
||||
// * Assert that member info updates to reflect the new addition
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', 'You can add 3 more people');
|
||||
|
||||
// # Remove user by clicking the remove(x) button
|
||||
cy.get('#selectItems .react-select__multi-value__remove').first().click();
|
||||
|
||||
// * Assert that member info updates to reflect the new addition
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', 'You can add 4 more people');
|
||||
|
||||
// # Remove last user on the list by typing backspace
|
||||
cy.get('#selectItems input').typeWithForce('{backspace}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Assert that member info updates to reflect the new addition
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', 'You can add 5 more people');
|
||||
});
|
||||
|
||||
it('MM-T465 - Assert that group message participant sees', () => {
|
||||
// # Create a group message with two other users
|
||||
const participants = users.slice(0, 2);
|
||||
createGroupMessageWith(participants);
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
const sortedParticipants = participants.sort((a, b) => {
|
||||
return a.username > b.username ? 1 : -1;
|
||||
});
|
||||
|
||||
// * Assert that intro message includes the right copy
|
||||
const expectedChannelInfo = 'This is the start of your group message history with these teammates.';
|
||||
cy.get('#channelIntro p.channel-intro__text').first().should('contain', expectedChannelInfo);
|
||||
cy.get('#channelIntro .profile-icon').should('have.length', '2');
|
||||
|
||||
cy.location().then((loc) => {
|
||||
const channelId = loc.pathname.split('/').slice(-1)[0];
|
||||
|
||||
// * Assert that sidebar displays the group channel
|
||||
cy.get(`#sidebarItem_${channelId}`).should('contain', `${sortedParticipants[0].username}, ${sortedParticipants[1].username}`);
|
||||
|
||||
// * Assert that member count shows next to the channel name
|
||||
cy.get(`#sidebarItem_${channelId} .status`).eq(0).should('contain', '2');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T469 - Post an @mention on a group channel', () => {
|
||||
spyNotificationAs('withNotification', 'granted');
|
||||
|
||||
// # Create a group message with two other users
|
||||
const participants = users.slice(0, 2);
|
||||
createGroupMessageWith(participants);
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Post a message as a different user
|
||||
cy.getCurrentChannelId().then((channelId) => {
|
||||
cy.postMessageAs({sender: participants[0], message: `@${testUser.username} Hello!!!`, channelId});
|
||||
|
||||
// * Assert that user receives notification
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('@withNotification').should('have.been.called');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T475 - Channel preferences, mute channel', () => {
|
||||
spyNotificationAs('withNotification', 'granted');
|
||||
|
||||
// # Create a group message with two other users
|
||||
const participants = users.slice(0, 2);
|
||||
createGroupMessageWith(participants);
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Clicks on Mute Channel through Notification Preferences
|
||||
cy.uiOpenChannelMenu().within(() => {
|
||||
// # Set Mute Channel to On
|
||||
cy.get('#markUnreadEdit').click();
|
||||
cy.get('#channelNotificationUnmute').click();
|
||||
cy.get('#saveSetting').click();
|
||||
|
||||
// * Assert that channel is muted
|
||||
cy.get('#toggleMute').should('be.visible');
|
||||
});
|
||||
|
||||
// # Post a message as a different user
|
||||
cy.getCurrentChannelId().then((channelId) => {
|
||||
let channelName;
|
||||
|
||||
cy.location().then((loc) => {
|
||||
channelName = loc.pathname.split('/').slice(-1)[0];
|
||||
});
|
||||
|
||||
cy.postMessageAs({sender: participants[0], message: 'Hello all', channelId}).then(() => {
|
||||
cy.visit(townsquareLink);
|
||||
|
||||
// * Assert that user does not receives a notification
|
||||
cy.get('@withNotification').should('not.have.been.called');
|
||||
|
||||
// * Should not have unread mentions indicator.
|
||||
|
||||
cy.get(`#sidebarItem_${channelName}`).
|
||||
scrollIntoView().
|
||||
find('#unreadMentions').
|
||||
should('not.exist');
|
||||
});
|
||||
|
||||
cy.postMessageAs({sender: participants[0], message: `@${testUser.username} Hello!!!`, channelId}).then(() => {
|
||||
cy.apiLogin(testUser);
|
||||
cy.visit(townsquareLink);
|
||||
|
||||
// * Assert that user does not receives a notification
|
||||
cy.get('@withNotification').should('not.have.been.called');
|
||||
|
||||
// * Should have unread mentions indicator.
|
||||
|
||||
cy.get(`#sidebarItem_${channelName}`).
|
||||
scrollIntoView().
|
||||
get('#unreadMentions').
|
||||
should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T478 - Open existing group message from More... section', () => {
|
||||
// # Create a group message with two other users
|
||||
const participants = users.slice(0, 2);
|
||||
const sortedParticipants = participants.sort((a, b) => {
|
||||
return a.username > b.username ? 1 : -1;
|
||||
});
|
||||
|
||||
createGroupMessageWith(participants);
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
cy.location().then((loc) => {
|
||||
const channelName = loc.pathname.split('/').slice(-1)[0];
|
||||
|
||||
// # Remove GM from the LHS
|
||||
cy.uiGetChannelSidebarMenu(channelName).within(() => {
|
||||
cy.findByText('Close Conversation').click();
|
||||
});
|
||||
|
||||
// # Open DM modal
|
||||
cy.uiAddDirectMessage().click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open previously closed group message
|
||||
cy.get('#selectItems input').typeWithForce(participants[0].username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#multiSelectList .suggestion-list__item').last().click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify that participants are listed in the input field
|
||||
cy.get('#selectItems').should('contain', `${sortedParticipants[0].username}${sortedParticipants[1].username}`);
|
||||
|
||||
// # Open group message
|
||||
cy.get('#saveItems').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify that page renders with the right information
|
||||
cy.get('#channelHeaderTitle').should('contain', `${sortedParticipants[0].username}, ${sortedParticipants[1].username}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createGroupMessageWith = (users) => {
|
||||
const defaultUserLimit = 7;
|
||||
cy.uiAddDirectMessage().click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', 'You can add 7 more people');
|
||||
|
||||
users.forEach((user, index) => {
|
||||
cy.get('#selectItems input').typeWithForce(user.username).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Assert that member info updates whilst adding new members
|
||||
cy.get('#multiSelectHelpMemberInfo').should('contain', `You can add ${defaultUserLimit - (index + 1)} more people`);
|
||||
});
|
||||
|
||||
// # Save group message member changes
|
||||
cy.get('#saveItems').click().wait(TIMEOUTS.HALF_SEC);
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// 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 @messaging
|
||||
|
||||
describe('Message', () => {
|
||||
let testChannel;
|
||||
let testTeam;
|
||||
|
||||
before(() => {
|
||||
// # Login as test user and go to off-topic
|
||||
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
|
||||
testChannel = channel;
|
||||
testTeam = team;
|
||||
|
||||
cy.visit(`/${testTeam.name}/channels/off-topic`);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T175 Channel shortlinking still works when placed in brackets', () => {
|
||||
// # Post a shortlink of channel
|
||||
const shortLink = `(~${testChannel.name})`;
|
||||
const longLink = `~${testChannel.display_name}`;
|
||||
|
||||
cy.postMessage('hello');
|
||||
cy.uiGetPostTextBox().type(shortLink).type('{enter}');
|
||||
|
||||
cy.getLastPostId().then((postId) => {
|
||||
// # Grab last message with the long link url and go to the link
|
||||
const divPostId = `#postMessageText_${postId}`;
|
||||
cy.get(divPostId).contains(longLink).click();
|
||||
|
||||
// * verify that the url is the same as what was just clicked on
|
||||
cy.location('pathname').should('contain', `${testTeam.name}/channels/${testChannel.name}`);
|
||||
|
||||
// * verify that the channel title represents the same channel that was clicked on
|
||||
cy.get('#channelHeaderTitle').should('contain', testChannel.display_name);
|
||||
});
|
||||
});
|
||||
});
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
// 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 @messaging
|
||||
|
||||
import * as TIMEOUTS from '@/fixtures/timeouts';
|
||||
|
||||
describe('Messaging', () => {
|
||||
let testTeam;
|
||||
let testChannel;
|
||||
let testUser;
|
||||
let userOne;
|
||||
|
||||
before(() => {
|
||||
// # Wrap websocket to be able to connect and close connections on demand
|
||||
cy.mockWebsockets();
|
||||
|
||||
// # Update config to enable "EnableReliableWebSockets"
|
||||
cy.apiUpdateConfig({ServiceSettings: {EnableReliableWebSockets: true}});
|
||||
|
||||
// # Login as test user and go to town-square
|
||||
cy.apiInitSetup().then(({team, channel, user}) => {
|
||||
testUser = user;
|
||||
testTeam = team;
|
||||
testChannel = channel;
|
||||
|
||||
cy.apiCreateUser().then(({user: user1}) => {
|
||||
userOne = user1;
|
||||
cy.apiAddUserToTeam(testTeam.id, userOne.id).then(() => {
|
||||
cy.apiAddUserToChannel(testChannel.id, userOne.id);
|
||||
});
|
||||
});
|
||||
|
||||
cy.apiLogin(testUser);
|
||||
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
|
||||
|
||||
// # Post several messages to establish websocket connection
|
||||
Cypress._.times(5, (i) => {
|
||||
cy.postMessage(i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T94 RHS fetches messages on socket reconnect when a different channel is in center', () => {
|
||||
// # Connect all sockets
|
||||
window.mockWebsockets.forEach((value) => {
|
||||
value.connect();
|
||||
});
|
||||
|
||||
// # Post a message as another user
|
||||
cy.postMessageAs({sender: userOne, message: 'abc', channelId: testChannel.id}).wait(TIMEOUTS.FIVE_SEC);
|
||||
|
||||
// # Click "Reply"
|
||||
cy.getLastPostId().then((rootPostId) => {
|
||||
cy.clickPostCommentIcon(rootPostId);
|
||||
|
||||
// # Post a message
|
||||
cy.postMessageReplyInRHS('def');
|
||||
|
||||
// # Change channel
|
||||
cy.uiGetLhsSection('CHANNELS').findByText('Town Square').click().then(() => {
|
||||
// # Close all sockets
|
||||
window.mockWebsockets.forEach((value) => {
|
||||
if (value.close) {
|
||||
value.close();
|
||||
}
|
||||
});
|
||||
|
||||
// # Post message as a different user
|
||||
cy.postMessageAs({sender: userOne, message: 'ghi', channelId: testChannel.id, rootId: rootPostId});
|
||||
|
||||
// # Wait a short time to check whether the message appears or not
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
|
||||
// * Verify that only "def" is posted and not "ghi"
|
||||
cy.get('#rhsContainer .post-right-comments-container').should('be.visible').children().should('have.length', 1);
|
||||
cy.get('#rhsContainer .post-right-comments-container').within(() => {
|
||||
cy.findByText('def').should('be.visible');
|
||||
cy.findByText('ghi').should('not.exist');
|
||||
}).then(() => {
|
||||
// * Connect all sockets one more time
|
||||
window.mockWebsockets.forEach((value) => {
|
||||
value.connect();
|
||||
});
|
||||
|
||||
// # Wait for sockets to be connected
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
cy.uiGetLhsSection('CHANNELS').findByText('Off-Topic').click();
|
||||
cy.postMessage('any');
|
||||
cy.uiGetLhsSection('CHANNELS').findByText('Town Square').click();
|
||||
cy.postMessage('any');
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// * Verify that both "def" and "ghi" are posted on websocket reconnect
|
||||
cy.get('#rhsContainer .post-right-comments-container').should('be.visible').children().should('have.length', 2);
|
||||
cy.get('#rhsContainer .post-right-comments-container').within(() => {
|
||||
cy.findByText('def').should('be.visible');
|
||||
cy.findByText('ghi').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-19
@@ -67,25 +67,6 @@ describe('Notifications', () => {
|
||||
cy.get(`#sidebarItem_${channelA.name}`).wait(TIMEOUTS.ONE_SEC).should('have.class', 'unread-title');
|
||||
cy.get(`#sidebarItem_${channelA.name} > #unreadMentions`).should('not.exist');
|
||||
});
|
||||
|
||||
it('MM-T568 - Channel Notifications - Turn off Ignore mentions for @channel, @here and @all', () => {
|
||||
cy.visit(`/${testTeam.name}/channels/${channelA.name}`);
|
||||
|
||||
// # Unset ignore mentions
|
||||
setIgnoreMentions(false);
|
||||
|
||||
// # Go to a different channel
|
||||
cy.visit(`/${testTeam.name}/channels/${channelB.name}`);
|
||||
|
||||
// # Post messages as another user on the first channel
|
||||
cy.postMessageAs({sender: userB, message: '@all test', channelId: channelA.id});
|
||||
cy.postMessageAs({sender: userB, message: '@channel test', channelId: channelA.id});
|
||||
cy.postMessageAs({sender: userB, message: '@here test', channelId: channelA.id});
|
||||
|
||||
// * Assert the channel is unread with 3 mentions
|
||||
cy.get(`#sidebarItem_${channelA.name}`).should('have.class', 'unread-title');
|
||||
cy.get(`#sidebarItem_${channelA.name} > #unreadMentions`).should('exist').wait(TIMEOUTS.ONE_SEC).should('contain', '3');
|
||||
});
|
||||
});
|
||||
|
||||
function addNumberOfUsersToChannel(num = 1) {
|
||||
|
||||
-38
@@ -29,44 +29,6 @@ describe('Search', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T350 - Searching displays results in RHS', () => {
|
||||
const testSearch = '/test url test-search';
|
||||
|
||||
// # Post a message
|
||||
cy.postMessage(testSearch);
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Search the world 'hello' that is generated by /test command
|
||||
cy.uiGetSearchBox().type('hello{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#search-items-container span').should('contain.text', 'hello');
|
||||
|
||||
// # RHS should be visible with search results
|
||||
cy.get('#search-items-container').should('be.visible');
|
||||
|
||||
// * Verify different display formatting of the search results
|
||||
cy.get('[data-testid="search-item-container"]').first().then(($result) => {
|
||||
cy.wrap($result).contains('Basic word search: Hello world!');
|
||||
cy.wrap($result).get('h5.markdown__heading').contains('Hello');
|
||||
cy.wrap($result).get('.post-code.post-code--wrap code').contains('Hello');
|
||||
cy.wrap($result).contains('#hello');
|
||||
|
||||
// # Jump to conversation
|
||||
cy.wrap($result).get('a.search-item__jump').first().click();
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Hover search query clear icon
|
||||
cy.get('.input-clear-x').first().trigger('mouseover', {force: true}).then(($span) => {
|
||||
// # Click the clear query icon
|
||||
cy.wrap($span).click({force: true});
|
||||
|
||||
// * Assert search results are intact
|
||||
cy.get('[data-testid="search-item-container"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T2286 - Clicking a hashtag from a message opens messages with that hashtag on RHS', () => {
|
||||
const testSearch = '/test url test-search';
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
// 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 @search_date_filter
|
||||
|
||||
import {
|
||||
getMsAndQueryForDate,
|
||||
getTestMessages,
|
||||
searchAndValidate,
|
||||
setupTestData,
|
||||
} from './helpers';
|
||||
|
||||
import {getAdminAccount} from '@/support/env';
|
||||
|
||||
describe('Search Date Filter', () => {
|
||||
const testData = getTestMessages();
|
||||
const admin = getAdminAccount();
|
||||
let anotherAdmin;
|
||||
let channelUrl;
|
||||
let channelId;
|
||||
|
||||
before(() => {
|
||||
cy.apiInitSetup({userPrefix: 'other-admin'}).
|
||||
then(({team, channel, user, channelUrl: url}) => {
|
||||
anotherAdmin = user;
|
||||
channelUrl = url;
|
||||
channelId = channel.id;
|
||||
|
||||
// # Visit test channel
|
||||
cy.visit(channelUrl);
|
||||
|
||||
setupTestData(testData, {team, channel, admin, anotherAdmin});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.reload();
|
||||
cy.postMessage(Date.now());
|
||||
});
|
||||
|
||||
it('MM-T599 Edit date and search again', () => {
|
||||
// # Create expected data
|
||||
const targetMessage = 'calendarUpdate' + Date.now();
|
||||
const targetDate = getMsAndQueryForDate(Date.UTC(2019, 0, 15, 9, 30));
|
||||
|
||||
// # Post message with unique text
|
||||
cy.postMessageAs({sender: admin, message: targetMessage, channelId, createAt: targetDate.ms});
|
||||
|
||||
// # Set clock to custom date and visit test channel like reloading a page to take effect
|
||||
cy.clock(targetDate.ms, ['Date']);
|
||||
cy.reload();
|
||||
|
||||
// # Type on: into search field
|
||||
cy.uiGetSearchBox().click().clear().type('on:');
|
||||
|
||||
// * Day picker should appear
|
||||
cy.get('.DayPicker').
|
||||
as('dayPicker').
|
||||
should('be.visible');
|
||||
|
||||
// # Click on today's date
|
||||
cy.get('@dayPicker').
|
||||
find('.DayPicker-Day--today').click();
|
||||
|
||||
// * Search field should populate with the correct date, then send rest of query
|
||||
cy.uiGetSearchBox().
|
||||
should('have.value', 'on:2019-01-15 ').
|
||||
click().
|
||||
type(`${targetMessage}{enter}`).
|
||||
should('be.empty');
|
||||
|
||||
cy.get('#loadingSpinner').should('not.exist');
|
||||
|
||||
// * Verify we see our single result
|
||||
cy.uiGetRHSSearchContainer().
|
||||
findAllByTestId('search-item-container').
|
||||
should('be.visible').
|
||||
and('have.length', 1).
|
||||
find('.post-message').
|
||||
should('have.text', targetMessage);
|
||||
|
||||
// # Visit test channel to reload a page
|
||||
cy.visit(channelUrl);
|
||||
cy.postMessage(Date.now());
|
||||
|
||||
// # Back space right after the date to bring up date picker again
|
||||
cy.uiGetSearchBox().click().clear().
|
||||
type(`on:2019-01-15 ${targetMessage}`).
|
||||
type('{leftarrow}'.repeat(targetMessage.length + 1)).
|
||||
type('{backspace}');
|
||||
|
||||
// * Day picker should be visible
|
||||
cy.get('@dayPicker').should('be.visible');
|
||||
|
||||
// # Click on tomorrow's day
|
||||
cy.get('@dayPicker').
|
||||
find('.DayPicker-Day--today').
|
||||
next('.DayPicker-Day').click();
|
||||
|
||||
// # Add message to search for, and hit enter
|
||||
cy.uiGetSearchBox().
|
||||
should('have.value', `on:2019-01-16 ${targetMessage}`).
|
||||
click().
|
||||
type('{enter}').
|
||||
should('be.empty');
|
||||
|
||||
cy.get('#loadingSpinner').should('not.exist');
|
||||
|
||||
// * There should be no results
|
||||
cy.uiGetRHSSearchContainer().
|
||||
findAllByTestId('search-item-container').
|
||||
should('have.length', 0);
|
||||
});
|
||||
|
||||
it('MM-T595 Changing timezone changes day search results appears', () => {
|
||||
const identifier = 'timezone' + Date.now();
|
||||
|
||||
const target = getMsAndQueryForDate(Date.UTC(2018, 9, 31, 23, 59));
|
||||
|
||||
const targetMessage = 'targetAM ' + identifier;
|
||||
|
||||
// # Post message with unique text
|
||||
cy.postMessageAs({sender: admin, message: targetMessage, channelId, createAt: target.ms});
|
||||
|
||||
// * Verify result appears in current timezone
|
||||
searchAndValidate(`on:${target.query} ${identifier}`, [targetMessage]);
|
||||
|
||||
// # Change timezone to alter what posts appear in results
|
||||
changeTimezone('Europe/Brussels');
|
||||
|
||||
// * With same query, no results should appear
|
||||
searchAndValidate(`on:${target.query} ${identifier}`);
|
||||
});
|
||||
});
|
||||
|
||||
function changeTimezone(timezone) {
|
||||
cy.apiPatchMe({timezone: {automaticTimezone: '', manualTimezone: timezone, useAutomaticTimezone: 'false'}});
|
||||
}
|
||||
@@ -40,6 +40,8 @@ cd e2e-tests && TEST=playwright make
|
||||
|
||||
This approach uses the server's Docker image to create a consistent testing environment. It automatically configures the server with the necessary settings for Playwright tests and handles dependencies.
|
||||
|
||||
`ENABLED_DOCKER_SERVICES` defaults to the base `postgres inbucket` set (plus the auto-added `playwright` and `webhook-interactions` services). Expand it only when a test needs an extra dependency (for example `minio` for file storage or `openldap` for LDAP).
|
||||
|
||||
#### 2. Install dependencies and run the test.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
getOnPremServerConfig,
|
||||
isWebhookTestServerReachable,
|
||||
setupWebhookTestServer,
|
||||
PlaywrightClient4,
|
||||
} from './server';
|
||||
|
||||
export {
|
||||
|
||||
@@ -5,12 +5,25 @@ import type {Page} from '@playwright/test';
|
||||
|
||||
type NotificationData = {title: string} & NotificationOptions;
|
||||
|
||||
type MockWebSocket = {
|
||||
wrappedSocket: WebSocket | null;
|
||||
onopen: ((ev: Event) => void) | null;
|
||||
onmessage: ((ev: MessageEvent) => void) | null;
|
||||
onerror: ((ev: Event) => void) | null;
|
||||
onclose: ((ev: CloseEvent) => void) | null;
|
||||
readyState: number;
|
||||
send(data: string | ArrayBuffer): void;
|
||||
close(): void;
|
||||
connect(): void;
|
||||
};
|
||||
|
||||
// Extend the Window interface to add custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
originalNotification: typeof Notification;
|
||||
capturedNotifications: NotificationData[];
|
||||
getNotifications: () => NotificationData[];
|
||||
mockWebsockets: MockWebSocket[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,3 +101,100 @@ export async function waitForNotification(
|
||||
console.error(`Notification not received within the timeout period of ${timeout}ms`);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* `mockWebsockets` wraps `window.WebSocket` so the test can close and reopen the underlying
|
||||
* socket connection(s) on demand, to simulate a socket disconnect/reconnect without navigating
|
||||
* away from the page. Mocked sockets do not auto-connect on construction — call
|
||||
* `connectWebsockets` to open them.
|
||||
*
|
||||
* Must be called on a `page` that has not yet navigated (e.g. right after `pw.testBrowser.login()`,
|
||||
* before `channelsPage.goto(...)`), since it installs the override via `addInitScript` so it's in
|
||||
* place before the app's own bundle constructs its WebSocket client.
|
||||
*
|
||||
* @param page Page object, not yet navigated
|
||||
*/
|
||||
export async function mockWebsockets(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
const RealWebSocket = window.WebSocket;
|
||||
window.mockWebsockets = [];
|
||||
|
||||
class MockWebSocketImpl {
|
||||
// Match the standard WebSocket.readyState values so client code comparing
|
||||
// against `WebSocket.OPEN` (now this class, since it replaces window.WebSocket)
|
||||
// sees real state transitions instead of coincidental `undefined === undefined`.
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSING = 2;
|
||||
static readonly CLOSED = 3;
|
||||
|
||||
wrappedSocket: WebSocket | null = null;
|
||||
onopen: ((ev: Event) => void) | null = null;
|
||||
onmessage: ((ev: MessageEvent) => void) | null = null;
|
||||
onerror: ((ev: Event) => void) | null = null;
|
||||
onclose: ((ev: CloseEvent) => void) | null = null;
|
||||
readyState: number = MockWebSocketImpl.CONNECTING;
|
||||
private readonly args: [string | URL, (string | string[])?];
|
||||
|
||||
constructor(...args: [string | URL, (string | string[])?]) {
|
||||
this.args = args;
|
||||
window.mockWebsockets.push(this);
|
||||
}
|
||||
|
||||
send(data: string | ArrayBuffer) {
|
||||
if (this.wrappedSocket) {
|
||||
this.wrappedSocket.send(data);
|
||||
} else if (this.onerror) {
|
||||
this.onerror(new Event('error'));
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.wrappedSocket) {
|
||||
this.readyState = MockWebSocketImpl.CLOSING;
|
||||
this.wrappedSocket.close(1000);
|
||||
} else {
|
||||
this.readyState = MockWebSocketImpl.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
const socket = new RealWebSocket(...this.args);
|
||||
this.readyState = MockWebSocketImpl.CONNECTING;
|
||||
socket.onopen = (ev) => {
|
||||
this.readyState = MockWebSocketImpl.OPEN;
|
||||
this.onopen?.(ev);
|
||||
};
|
||||
socket.onmessage = (ev) => this.onmessage?.(ev);
|
||||
socket.onerror = (ev) => this.onerror?.(ev);
|
||||
socket.onclose = (ev) => {
|
||||
this.readyState = MockWebSocketImpl.CLOSED;
|
||||
this.onclose?.(ev);
|
||||
};
|
||||
this.wrappedSocket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
window.WebSocket = MockWebSocketImpl as unknown as typeof WebSocket;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens (or reopens) every mocked WebSocket tracked by `mockWebsockets`, simulating a reconnect.
|
||||
* @param page Page object
|
||||
*/
|
||||
export async function connectWebsockets(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
window.mockWebsockets.forEach((ws) => ws.connect());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes every mocked WebSocket tracked by `mockWebsockets`, simulating a disconnect.
|
||||
* @param page Page object
|
||||
*/
|
||||
export async function closeWebsockets(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
window.mockWebsockets.forEach((ws) => ws.close());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {PlaywrightClient4} from './playwright_client';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
// Variable to hold cache
|
||||
@@ -13,7 +14,7 @@ export async function makeClient(
|
||||
userRequest?: UserRequest,
|
||||
opts: {useCache?: boolean; skipLog?: boolean} = {useCache: true, skipLog: false},
|
||||
): Promise<ClientCache> {
|
||||
const client = new Client4();
|
||||
const client = new PlaywrightClient4();
|
||||
client.setUrl(testConfig.baseURL);
|
||||
|
||||
try {
|
||||
@@ -53,6 +54,6 @@ type UserRequest = {
|
||||
};
|
||||
|
||||
type ClientCache = {
|
||||
client: Client4;
|
||||
client: PlaywrightClient4;
|
||||
user: UserProfile | null;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {makeClient} from './client';
|
||||
export {PlaywrightClient4} from './playwright_client';
|
||||
export {createRandomChannel} from './channel';
|
||||
export {getOnPremServerConfig, mergeWithOnPremServerConfig} from './default_config';
|
||||
export {initSetup, getAdminClient} from './init';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {createRandomChannel} from './channel';
|
||||
import {createNewUserProfile} from './user';
|
||||
|
||||
/**
|
||||
* Client4 extended with Playwright test-setup helpers only.
|
||||
* These are not part of the Mattermost server API — do not add real API wrappers here.
|
||||
*/
|
||||
export class PlaywrightClient4 extends Client4 {
|
||||
async createPublicChannel(teamId: string, displayName = 'Public', name?: string): Promise<Channel> {
|
||||
return this.createChannel(
|
||||
createRandomChannel({
|
||||
teamId,
|
||||
name: name ?? displayName.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
displayName,
|
||||
unique: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async createUsers(teamId: string, count: number, prefix = 'user'): Promise<UserProfile[]> {
|
||||
const users: UserProfile[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const user = await createNewUserProfile(this, {prefix});
|
||||
await this.addToTeam(teamId, user.id);
|
||||
users.push(user);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,13 @@ import {
|
||||
} from './test_action';
|
||||
import {pages} from './ui/pages';
|
||||
import {matchSnapshot} from './visual';
|
||||
import {stubNotification, waitForNotification} from './mock_browser_api';
|
||||
import {
|
||||
closeWebsockets,
|
||||
connectWebsockets,
|
||||
mockWebsockets,
|
||||
stubNotification,
|
||||
waitForNotification,
|
||||
} from './mock_browser_api';
|
||||
import {duration, getRandomId, newTestPassword, simpleEmailRe, wait} from './util';
|
||||
|
||||
export {expect} from '@playwright/test';
|
||||
@@ -120,6 +126,9 @@ export class PlaywrightExtended {
|
||||
// ./mock_browser_api
|
||||
readonly stubNotification;
|
||||
readonly waitForNotification;
|
||||
readonly mockWebsockets;
|
||||
readonly connectWebsockets;
|
||||
readonly closeWebsockets;
|
||||
|
||||
// ./server
|
||||
readonly createNewUserProfile;
|
||||
@@ -196,6 +205,9 @@ export class PlaywrightExtended {
|
||||
// ./mock_browser_api
|
||||
this.stubNotification = stubNotification;
|
||||
this.waitForNotification = waitForNotification;
|
||||
this.mockWebsockets = mockWebsockets;
|
||||
this.connectWebsockets = connectWebsockets;
|
||||
this.closeWebsockets = closeWebsockets;
|
||||
|
||||
// ./server
|
||||
this.createNewUserProfile = createNewUserProfile;
|
||||
|
||||
@@ -27,6 +27,9 @@ export default class ChannelsCenterView {
|
||||
readonly channelBanner;
|
||||
readonly autotranslationBadge;
|
||||
readonly flagPostConfirmationDialog;
|
||||
readonly notificationSeparator;
|
||||
readonly postViews;
|
||||
readonly channelIntro;
|
||||
|
||||
constructor(container: Locator, page: Page) {
|
||||
this.container = container;
|
||||
@@ -44,6 +47,9 @@ export default class ChannelsCenterView {
|
||||
page.getByRole('dialog', {name: 'Quarantine for Review'}),
|
||||
page,
|
||||
);
|
||||
this.notificationSeparator = container.locator('.NotificationSeparator');
|
||||
this.postViews = container.getByTestId('postView');
|
||||
this.channelIntro = container.locator('#channelIntro');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Locator} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelNotificationPreferencesModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly muteChannelCheckbox;
|
||||
readonly ignoreMentionsCheckbox;
|
||||
readonly saveButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.muteChannelCheckbox = container.getByRole('checkbox', {name: /Mute channel/i});
|
||||
this.ignoreMentionsCheckbox = container.getByRole('checkbox', {
|
||||
name: 'Ignore mentions for @channel, @here and @all',
|
||||
});
|
||||
this.saveButton = container.getByRole('button', {name: 'Save'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await this.container.getByText('Mute or ignore').waitFor();
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default class DirectChannelsModal {
|
||||
readonly goButton;
|
||||
readonly results;
|
||||
readonly searchInput;
|
||||
readonly memberLimitHelpText;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
@@ -18,6 +19,7 @@ export default class DirectChannelsModal {
|
||||
this.goButton = container.getByRole('button', {name: 'Go'});
|
||||
this.results = container.getByTestId('more-modal-list');
|
||||
this.searchInput = container.getByRole('combobox', {name: 'Search for people'});
|
||||
this.memberLimitHelpText = container.locator('#multiSelectHelpMemberInfo');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
@@ -32,7 +34,19 @@ export default class DirectChannelsModal {
|
||||
|
||||
await row.click();
|
||||
|
||||
await expect(this.container.getByRole('button', {name: `Remove ${user.username}`})).toBeVisible();
|
||||
await expect(this.getRemoveButton(user.username)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the "Remove {username}" button for a selected user.
|
||||
* @param username
|
||||
*/
|
||||
getRemoveButton(username: string): Locator {
|
||||
return this.container.getByRole('button', {name: `Remove ${username}`});
|
||||
}
|
||||
|
||||
async removeUser(username: string) {
|
||||
await this.getRemoveButton(username).click();
|
||||
}
|
||||
|
||||
async toHaveNUsersSelected(count: number) {
|
||||
|
||||
@@ -15,6 +15,8 @@ export default class ChannelsPost {
|
||||
|
||||
readonly body;
|
||||
readonly profileIcon;
|
||||
readonly emoticon;
|
||||
readonly messageText;
|
||||
|
||||
readonly removePostButton;
|
||||
|
||||
@@ -32,6 +34,8 @@ export default class ChannelsPost {
|
||||
this.body = container.getByTestId('post-body');
|
||||
|
||||
this.profileIcon = container.getByTestId('profile-icon');
|
||||
this.emoticon = container.locator('.emoticon');
|
||||
this.messageText = container.locator('.post-message__text p');
|
||||
|
||||
this.removePostButton = container.getByTestId('post-remove-button');
|
||||
|
||||
@@ -70,6 +74,14 @@ export default class ChannelsPost {
|
||||
return this.profileIcon.getByAltText(`${username} profile image`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a rendered link with the given accessible name inside the post body.
|
||||
* @param name
|
||||
*/
|
||||
getLink(name: string): Locator {
|
||||
return this.container.getByRole('link', {name});
|
||||
}
|
||||
|
||||
async openAThread() {
|
||||
await this.container.hover();
|
||||
await this.postMenu.toBeVisible();
|
||||
|
||||
@@ -44,4 +44,14 @@ export default class SearchBox {
|
||||
getSelectedSuggestion() {
|
||||
return this.searchHints.getByTestId('suggestion-selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a day cell in the "on:" date-filter day picker by day-of-month.
|
||||
* Matches on the leading day number in the accessible name (e.g. "15th January (Tuesday)"),
|
||||
* so callers don't need to compute the ordinal suffix or day-of-week.
|
||||
* @param dayOfMonth
|
||||
*/
|
||||
getDayPickerDay(dayOfMonth: number): Locator {
|
||||
return this.container.getByRole('button', {name: new RegExp(`^${dayOfMonth}\\D`)});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,60 @@ export default class ChannelsSidebarLeft {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the sidebar channel link with the given name.
|
||||
* Locates the sidebar link with the given name.
|
||||
* It can be any sidebar item name including channels, direct messages, or group messages, threads, etc.
|
||||
* Falls back to matching by visible text since some sidebar items (e.g. DMs) may not resolve by the plain ID.
|
||||
* @param name
|
||||
*/
|
||||
item(name: string): Locator {
|
||||
return this.container
|
||||
.locator(`#sidebarItem_${name}`)
|
||||
.or(this.container.locator('.SidebarLink').filter({hasText: name}))
|
||||
.first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the sidebar channel link with the given name.
|
||||
* @param channelName
|
||||
*/
|
||||
async goToItem(channelName: string) {
|
||||
const channel = this.container.locator(`#sidebarItem_${channelName}`);
|
||||
const channel = this.item(channelName);
|
||||
await channel.waitFor();
|
||||
await channel.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the sidebar item with the given name is in the unread state.
|
||||
* @param name
|
||||
*/
|
||||
async assertItemUnread(name: string) {
|
||||
await expect(this.item(name)).toHaveClass(/unread|unread-title/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the sidebar item with the given name is in the read (not unread) state.
|
||||
* @param name
|
||||
*/
|
||||
async assertItemRead(name: string) {
|
||||
await expect(this.item(name)).not.toHaveClass(/unread|unread-title/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the unread-mentions count badge nested inside the sidebar item with the given name.
|
||||
* @param name
|
||||
*/
|
||||
unreadMentionsBadge(name: string): Locator {
|
||||
return this.item(name).locator('#unreadMentions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the group message member-count badge nested inside the sidebar item with the given name.
|
||||
* @param name
|
||||
*/
|
||||
memberCountBadge(name: string): Locator {
|
||||
return this.item(name).locator('.status--group');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies 'Drafts' as a sidebar link exists in LHS.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ export default class ChannelsSidebarRight {
|
||||
readonly currentVersionEditedPosttext;
|
||||
readonly restorePreviousPostVersionIcon;
|
||||
readonly channelBanner;
|
||||
readonly notificationSeparator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
@@ -49,6 +50,7 @@ export default class ChannelsSidebarRight {
|
||||
'button[aria-label="Select to restore an old message."]',
|
||||
);
|
||||
this.channelBanner = container.getByTestId('channel_banner_container');
|
||||
this.notificationSeparator = container.locator('.NotificationSeparator');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import ChannelsHeader from './channels/header';
|
||||
import ChannelsPost from './channels/post';
|
||||
import ChannelsPostCreate from './channels/post_create';
|
||||
import ChannelsPostEdit from './channels/post_edit';
|
||||
import ChannelNotificationPreferencesModal from './channels/channel_notification_preferences_modal';
|
||||
import ChannelSettingsModal from './channels/channel_settings/channel_settings_modal';
|
||||
import ChannelsSidebarLeft from './channels/sidebar_left';
|
||||
import ChannelsSidebarRight from './channels/sidebar_right';
|
||||
@@ -86,6 +87,7 @@ const components = {
|
||||
ChannelsAppBar,
|
||||
ChannelsCenterView,
|
||||
CreateTeamForm,
|
||||
ChannelNotificationPreferencesModal,
|
||||
ChannelsHeader,
|
||||
ChannelsPost,
|
||||
ChannelsPostCreate,
|
||||
@@ -165,6 +167,7 @@ export {
|
||||
ChannelsAppBar,
|
||||
ChannelsCenterView,
|
||||
CreateTeamForm,
|
||||
ChannelNotificationPreferencesModal,
|
||||
ChannelsHeader,
|
||||
ChannelsPost,
|
||||
ChannelsPostCreate,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {expect} from '@playwright/test';
|
||||
import {waitUntil} from 'async-wait-until';
|
||||
|
||||
import type {
|
||||
ChannelNotificationPreferencesModal,
|
||||
ChannelsPost,
|
||||
SettingsModal,
|
||||
TeamSettingsModal,
|
||||
@@ -30,12 +31,14 @@ export default class ChannelsPage {
|
||||
readonly messagePriority;
|
||||
|
||||
readonly channelSettingsModal;
|
||||
readonly channelNotificationPreferencesModal;
|
||||
readonly createTeamForm;
|
||||
readonly deletePostModal;
|
||||
readonly findChannelsModal;
|
||||
readonly newChannelModal;
|
||||
readonly browseChannelsModal;
|
||||
readonly directChannelsModal;
|
||||
readonly keyboardShortcutsModal;
|
||||
public invitePeopleModal: InvitePeopleModal | undefined;
|
||||
public membersInvitedModal: MembersInvitedModal | undefined;
|
||||
readonly profileModal;
|
||||
@@ -55,6 +58,9 @@ export default class ChannelsPage {
|
||||
readonly emojiGifPickerPopup;
|
||||
readonly scheduleMessageMenu;
|
||||
|
||||
readonly searchResultsContainer;
|
||||
readonly searchResultItems;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
@@ -70,6 +76,10 @@ export default class ChannelsPage {
|
||||
|
||||
// Modals
|
||||
this.channelSettingsModal = new ChannelSettingsModal(page.getByRole('dialog', {name: 'Channel Settings'}));
|
||||
this.channelNotificationPreferencesModal = new components.ChannelNotificationPreferencesModal(
|
||||
page.getByRole('dialog', {name: 'Notification Preferences'}),
|
||||
);
|
||||
this.keyboardShortcutsModal = page.getByRole('dialog', {name: /Keyboard shortcuts/});
|
||||
this.createTeamForm = new CreateTeamForm(page.getByTestId('create-team-form'));
|
||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
@@ -104,7 +114,17 @@ export default class ChannelsPage {
|
||||
this.postContainer = page.getByTestId('post-message-text');
|
||||
this.archivedChannelMessage = page.locator('#channelArchivedMessage');
|
||||
|
||||
page.locator('#channelHeaderDropdownMenu');
|
||||
// Search results
|
||||
this.searchResultsContainer = page.locator('#search-items-container');
|
||||
this.searchResultItems = page.getByTestId('search-item-container');
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a search result item containing the given text.
|
||||
* @param text
|
||||
*/
|
||||
getSearchResultItem(text: string) {
|
||||
return this.page.getByTestId('search-item-container').filter({hasText: text});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
@@ -221,6 +241,19 @@ export default class ChannelsPage {
|
||||
return this.channelSettingsModal;
|
||||
}
|
||||
|
||||
async openChannelNotificationPreferences(): Promise<ChannelNotificationPreferencesModal> {
|
||||
await this.centerView.header.openChannelMenu();
|
||||
await this.page.getByRole('menuitem', {name: 'Notification Preferences'}).click();
|
||||
await this.channelNotificationPreferencesModal.toBeVisible();
|
||||
|
||||
return this.channelNotificationPreferencesModal;
|
||||
}
|
||||
|
||||
async closeGroupMessage() {
|
||||
await this.centerView.header.openChannelMenu();
|
||||
await this.page.getByRole('menuitem', {name: 'Close Group Message'}).click();
|
||||
}
|
||||
|
||||
async openSettings(): Promise<SettingsModal> {
|
||||
await this.globalHeader.openSettings();
|
||||
await this.settingsModal.toBeVisible();
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {PlaywrightExtended} from '@mattermost/playwright-lib';
|
||||
import type {PlaywrightClient4, PlaywrightExtended} from '@mattermost/playwright-lib';
|
||||
|
||||
export const BOR_TAG = '@burn_on_read';
|
||||
|
||||
export type BorSetup = {
|
||||
adminClient: Client4;
|
||||
adminClient: PlaywrightClient4;
|
||||
adminUser: UserProfile;
|
||||
user: UserProfile;
|
||||
userClient: Client4;
|
||||
userClient: PlaywrightClient4;
|
||||
team: Team;
|
||||
offTopicUrl: string;
|
||||
townSquareUrl: string;
|
||||
@@ -64,40 +63,13 @@ export async function setupBorTest(
|
||||
*/
|
||||
export async function createSecondUser(
|
||||
pw: PlaywrightExtended,
|
||||
adminClient: Client4,
|
||||
adminClient: PlaywrightClient4,
|
||||
team: Team,
|
||||
): Promise<UserProfile & {password: string}> {
|
||||
const randomUser = await pw.random.user();
|
||||
const user = await adminClient.createUser(randomUser, '', '');
|
||||
(user as any).password = randomUser.password;
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
const [user] = await adminClient.createUsers(team.id, 1);
|
||||
return user as UserProfile & {password: string};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple users and add to team
|
||||
* @param pw Playwright extended fixture
|
||||
* @param adminClient Admin client for user creation
|
||||
* @param team Team to add users to
|
||||
* @param count Number of users to create
|
||||
* @returns Array of created users with passwords
|
||||
*/
|
||||
export async function createMultipleUsers(
|
||||
pw: PlaywrightExtended,
|
||||
adminClient: Client4,
|
||||
team: Team,
|
||||
count: number,
|
||||
): Promise<Array<UserProfile & {password: string}>> {
|
||||
const users: Array<UserProfile & {password: string}> = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const user = await createSecondUser(pw, adminClient, team);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse recipient count from tooltip text
|
||||
* @param tooltipText Tooltip text containing recipient info
|
||||
|
||||
+1
-2
@@ -14,7 +14,6 @@ import {
|
||||
createParentPolicy,
|
||||
assignChannelsToPolicy,
|
||||
createPrivateChannel,
|
||||
createPublicChannel,
|
||||
createGroupConstrainedPrivateChannel,
|
||||
setUserAttribute,
|
||||
addAttributeRule,
|
||||
@@ -86,7 +85,7 @@ test.describe('Channel Settings Modal - Access Control Tab', () => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
const channel = await createPublicChannel(adminClient, team.id);
|
||||
const channel = await adminClient.createPublicChannel(team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import type {Client4} from '@mattermost/client';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {newTestPassword, getRandomId} from '@mattermost/playwright-lib';
|
||||
import {newTestPassword, getRandomId, type PlaywrightClient4} from '@mattermost/playwright-lib';
|
||||
|
||||
import {assignChannelsToPolicy, deletePolicy, unassignChannelsFromPolicy} from '../team_settings/helpers';
|
||||
|
||||
@@ -134,14 +134,8 @@ export async function setUserAttributeById(client: Client4, userId: string, fiel
|
||||
/**
|
||||
* Create a public channel and register cleanup for it.
|
||||
*/
|
||||
export async function createTrackedPublicChannel(client: Client4, teamId: string, ledger: CleanupLedger) {
|
||||
const id = getRandomId();
|
||||
const channel = await client.createChannel({
|
||||
team_id: teamId,
|
||||
name: `pub-${id}`,
|
||||
display_name: `PUB-${id}`,
|
||||
type: 'O',
|
||||
} as any);
|
||||
export async function createTrackedPublicChannel(client: PlaywrightClient4, teamId: string, ledger: CleanupLedger) {
|
||||
const channel = await client.createPublicChannel(teamId);
|
||||
ledger.add(() => client.deleteChannel(channel.id));
|
||||
return channel;
|
||||
}
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify users can be added and removed while creating a group message.
|
||||
*/
|
||||
test('MM-T460 Add and remove users while creating new Group Message', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const participants = await adminClient.createUsers(team.id, 3, 'gm-edit');
|
||||
|
||||
// # Open the Direct Messages modal and select two users
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const modal = await channelsPage.openDirectChannelsModal();
|
||||
await modal.selectUser(participants[0]);
|
||||
await modal.selectUser(participants[1]);
|
||||
|
||||
// * Verify two users are selected, and the remaining-members counter reflects the selection
|
||||
// (a group message allows up to 8 members total, 7 besides the current user)
|
||||
await expect(modal.getRemoveButton(participants[0].username)).toBeVisible();
|
||||
await expect(modal.getRemoveButton(participants[1].username)).toBeVisible();
|
||||
await expect(modal.memberLimitHelpText).toContainText('You can add 5 more people');
|
||||
|
||||
// # Remove one user via its remove button
|
||||
await modal.removeUser(participants[1].username);
|
||||
|
||||
// * Verify the counter reflects the removal
|
||||
await expect(modal.memberLimitHelpText).toContainText('You can add 6 more people');
|
||||
|
||||
// # Add a different user
|
||||
await modal.selectUser(participants[2]);
|
||||
|
||||
// * Verify the selected list reflects the removal and addition, and the counter updates again
|
||||
await expect(modal.getRemoveButton(participants[1].username)).toHaveCount(0);
|
||||
await expect(modal.getRemoveButton(participants[2].username)).toBeVisible();
|
||||
await expect(modal.memberLimitHelpText).toContainText('You can add 5 more people');
|
||||
|
||||
// # Remove the last selected user via backspace in the empty search input
|
||||
await modal.searchInput.press('Backspace');
|
||||
|
||||
// * Verify the selected list and counter reflect the backspace removal
|
||||
await expect(modal.getRemoveButton(participants[2].username)).toHaveCount(0);
|
||||
await expect(modal.memberLimitHelpText).toContainText('You can add 6 more people');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify the group message intro, sidebar label, and member count render for participants.
|
||||
*/
|
||||
test('MM-T465 Create a group message and show participant details', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const participants = await adminClient.createUsers(team.id, 2, 'gm-intro');
|
||||
|
||||
// # Create a group message from the Direct Messages modal
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const modal = await channelsPage.openDirectChannelsModal();
|
||||
for (const participant of participants) {
|
||||
await modal.selectUser(participant);
|
||||
}
|
||||
await modal.goToChannel();
|
||||
|
||||
// * Verify the group message intro, participant avatars, sidebar label, and member count. The
|
||||
// sidebar label sorts the other participants' usernames with numeric-aware locale comparison
|
||||
// (digit runs compare by value, not by character code), so match that here rather than a plain
|
||||
// string sort.
|
||||
const sortedParticipants = [...participants].sort((a, b) =>
|
||||
a.username.localeCompare(b.username, undefined, {numeric: true}),
|
||||
);
|
||||
await expect(channelsPage.centerView.channelIntro).toContainText('This is the start of your group message history');
|
||||
await expect(channelsPage.centerView.channelIntro.locator('.profile-icon')).toHaveCount(2);
|
||||
await channelsPage.centerView.header.toHaveTitle(participants[0].username);
|
||||
await channelsPage.centerView.header.toHaveTitle(participants[1].username);
|
||||
await expect(channelsPage.sidebarLeft.item(participants[0].username)).toContainText(
|
||||
`${sortedParticipants[0].username}, ${sortedParticipants[1].username}`,
|
||||
);
|
||||
await expect(channelsPage.sidebarLeft.memberCountBadge(participants[0].username)).toContainText('2');
|
||||
|
||||
// # Post a message so the group message persists in the sidebar
|
||||
await channelsPage.postMessage('Hi group');
|
||||
|
||||
// * Verify the message appears in the group message
|
||||
await (await channelsPage.getLastPost()).toContainText('Hi group');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a mention posted in a group message creates an unread mention for the mentioned participant.
|
||||
*/
|
||||
test('MM-T469 Create a group message and post a mention for another user', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [sender, secondParticipant] = await adminClient.createUsers(team.id, 2, 'gm-mention');
|
||||
const gmChannel = await adminClient.createGroupChannel([user.id, sender.id, secondParticipant.id]);
|
||||
|
||||
// # View another channel, then have a participant mention the test user in the GM
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
await adminClient.createPost({
|
||||
channel_id: gmChannel.id,
|
||||
user_id: sender.id,
|
||||
message: `@${user.username} Hello from GM`,
|
||||
});
|
||||
|
||||
// * Verify a desktop notification fires for the mention
|
||||
const notifications = await pw.waitForNotification(page, 1);
|
||||
expect(notifications.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// * Verify the group message becomes unread with a mention badge
|
||||
await channelsPage.sidebarLeft.assertItemUnread(sender.username);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify muting a group message suppresses normal unread notification styling but keeps mention counts.
|
||||
*/
|
||||
test('MM-T475 Group Message Channel Preferences Mute channel', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [sender, secondParticipant] = await adminClient.createUsers(team.id, 2, 'gm-mute');
|
||||
const gmChannel = await adminClient.createGroupChannel([user.id, sender.id, secondParticipant.id]);
|
||||
|
||||
// # Open the GM and mute it through notification preferences
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.gotoMessage(team.name, gmChannel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const notificationPreferences = await channelsPage.openChannelNotificationPreferences();
|
||||
await notificationPreferences.muteChannelCheckbox.check();
|
||||
await notificationPreferences.save();
|
||||
|
||||
// # Post a normal message from another participant while away
|
||||
// `goto` is a full page navigation, so (re-)stub notifications only once settled on the away channel
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await pw.stubNotification(page, 'granted');
|
||||
await adminClient.createPost({channel_id: gmChannel.id, user_id: sender.id, message: 'Muted normal GM message'});
|
||||
|
||||
// * Verify no notification fires for the normal message while muted. Every GM post implicitly
|
||||
// mentions all members server-side (server/channels/app/notification.go), so the mention badge
|
||||
// still shows here even for a non-@mention message — that's current, correct behavior, not a bug.
|
||||
await pw.wait(duration.two_sec);
|
||||
expect(await page.evaluate(() => window.getNotifications())).toHaveLength(0);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
|
||||
|
||||
// # Post a message that mentions the user while away
|
||||
await adminClient.createPost({
|
||||
channel_id: gmChannel.id,
|
||||
user_id: sender.id,
|
||||
message: `@${user.username} Muted mention GM message`,
|
||||
});
|
||||
|
||||
// * Verify muting still suppresses the notification, but the mention indicator appears
|
||||
await pw.wait(duration.two_sec);
|
||||
expect(await page.evaluate(() => window.getNotifications())).toHaveLength(0);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(sender.username)).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a closed group message can be reopened from the Direct Messages modal.
|
||||
*/
|
||||
test('MM-T478 Closing group message channels and re-opening via Direct Messages modal', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const participants = await adminClient.createUsers(team.id, 2, 'gm-reopen');
|
||||
const gmChannel = await adminClient.createGroupChannel([user.id, participants[0].id, participants[1].id]);
|
||||
|
||||
// # Open then close the group message conversation
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.gotoMessage(team.name, gmChannel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.closeGroupMessage();
|
||||
await expect(channelsPage.sidebarLeft.item(participants[0].username)).toHaveCount(0);
|
||||
|
||||
// # Reopen the same GM from the Direct Messages modal. Searching by a single member's username
|
||||
// only surfaces an option to start a new conversation with that one person, not the existing
|
||||
// group message, so reselect both participants to resurface the existing conversation.
|
||||
const modal = await channelsPage.openDirectChannelsModal();
|
||||
await modal.selectUser(participants[0]);
|
||||
await modal.selectUser(participants[1]);
|
||||
await modal.goToChannel();
|
||||
|
||||
// * Verify the existing group message opens again
|
||||
await channelsPage.centerView.header.toHaveTitle(participants[0].username);
|
||||
await channelsPage.centerView.header.toHaveTitle(participants[1].username);
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify the keyboard shortcuts modal opens from Ctrl/Cmd+/ and /shortcuts, displays the
|
||||
* platform-specific upload shortcut, and can be closed by the shortcut, its close button, or Escape.
|
||||
*/
|
||||
test('MM-T1239 CTRL/CMD+/ and /shortcuts open keyboard shortcuts', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Open the keyboard shortcuts modal with the shortcut
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ControlOrMeta+/');
|
||||
|
||||
// * Verify the shortcuts modal opens and shows the platform-specific "Upload files" shortcut
|
||||
const modal = channelsPage.keyboardShortcutsModal;
|
||||
await expect(modal).toBeVisible();
|
||||
const filesSection = modal.locator('.subsection').filter({hasText: 'Files'});
|
||||
await expect(filesSection).toBeVisible();
|
||||
await expect(filesSection.getByText(process.platform === 'darwin' ? '⌘' : 'Ctrl')).toBeVisible();
|
||||
await expect(filesSection.getByText('U', {exact: true})).toBeVisible();
|
||||
|
||||
// # Close the modal by pressing the same shortcut again
|
||||
await page.keyboard.press('ControlOrMeta+/');
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
// # Reopen via the slash command and close using the modal's close button
|
||||
await channelsPage.postMessage('/shortcuts');
|
||||
await expect(modal).toBeVisible();
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
// # Reopen via the slash command and close by pressing Escape
|
||||
await channelsPage.postMessage('/shortcuts');
|
||||
await expect(modal).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify Ctrl/Cmd+K channel switch keeps focus so typed characters are not lost.
|
||||
*/
|
||||
test('MM-T1242 CTRL/CMD+K typed characters are not lost after switching channels', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const message = 'Hello World!';
|
||||
|
||||
// # Open quick switcher, select the current channel, and type into the focused page
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await expect(channelsPage.findChannelsModal.input).toBeVisible();
|
||||
await channelsPage.findChannelsModal.input.fill('off');
|
||||
await channelsPage.findChannelsModal.selectChannel('off-topic');
|
||||
await channelsPage.centerView.header.toHaveTitle('Off-Topic');
|
||||
await page.keyboard.type(message);
|
||||
|
||||
// * Verify typed characters land in the post textbox
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue(message);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify Ctrl/Cmd+Up and Ctrl/Cmd+Down cycle through previous messages in the post textbox.
|
||||
*/
|
||||
test('MM-T1254 CTRL/CMD+UP and CTRL/CMD+DOWN cycle previous messages', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const messages = ['post 1', 'post 2', 'post 3', 'post 4', 'post 5'];
|
||||
|
||||
// # Post several messages and focus the textbox
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
for (const message of messages) {
|
||||
await channelsPage.postMessage(message);
|
||||
}
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
|
||||
// * Verify Ctrl/Cmd+Up cycles backward through message history
|
||||
for (const message of [...messages].reverse()) {
|
||||
await page.keyboard.press('ControlOrMeta+ArrowUp');
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue(message);
|
||||
}
|
||||
|
||||
// * Verify one extra Ctrl/Cmd+Up past the oldest message does not change the displayed message
|
||||
await page.keyboard.press('ControlOrMeta+ArrowUp');
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue(messages[0]);
|
||||
|
||||
// * Verify Ctrl/Cmd+Down cycles forward through message history
|
||||
for (const message of messages.slice(1)) {
|
||||
await page.keyboard.press('ControlOrMeta+ArrowDown');
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue(message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify Up arrow opens inline edit for the previous message and saving marks the post as edited.
|
||||
*/
|
||||
test('MM-T1260 UP arrow edits the previous post', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Post a message and press Up from the center textbox
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.postMessage('Test');
|
||||
const postId = await channelsPage.centerView.getLastPostID();
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
|
||||
// # Edit and save the previous message
|
||||
await channelsPage.centerView.postEdit.toBeVisible();
|
||||
await channelsPage.centerView.postEdit.writeMessage('Edit Test');
|
||||
await channelsPage.centerView.postEdit.sendMessage();
|
||||
|
||||
// * Verify the post was edited and has the edited marker
|
||||
const editedPost = await channelsPage.getLastPost();
|
||||
await editedPost.toContainText('Edit Test');
|
||||
await expect(channelsPage.centerView.editedPostIcon(postId)).toContainText('Edited');
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test, type ChannelsPage} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify a public channel post can be marked unread from the post menu.
|
||||
*/
|
||||
test('MM-T246 Mark Post as Unread', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [author] = await adminClient.createUsers(team.id, 1, 'unread-author');
|
||||
const channelA = await adminClient.createPublicChannel(team.id, 'Unread A');
|
||||
const channelB = await adminClient.createPublicChannel(team.id, 'Unread B');
|
||||
await adminClient.addToChannel(user.id, channelA.id);
|
||||
await adminClient.addToChannel(user.id, channelB.id);
|
||||
await adminClient.addToChannel(author.id, channelA.id);
|
||||
|
||||
// # Create messages and mark the last one as unread
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: author.id, message: 'hello from current user: 1'});
|
||||
const unreadPost = await adminClient.createPost({
|
||||
channel_id: channelA.id,
|
||||
user_id: author.id,
|
||||
message: 'hello from current user: 4',
|
||||
});
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelA.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await markPostAsUnread(channelsPage, unreadPost.id);
|
||||
|
||||
// * Verify the unread separator appears at the selected post
|
||||
await expectUnreadSeparator(channelsPage, 'hello from current user: 4');
|
||||
|
||||
// # Switch away and back to the marked channel
|
||||
await channelsPage.sidebarLeft.goToItem(channelB.name);
|
||||
|
||||
// * Verify the original channel is unread while away
|
||||
await channelsPage.sidebarLeft.assertItemUnread(channelA.name);
|
||||
await channelsPage.sidebarLeft.goToItem(channelA.name);
|
||||
|
||||
// * Verify opening the channel marks it read while preserving the unread separator
|
||||
await channelsPage.sidebarLeft.assertItemRead(channelA.name);
|
||||
await expectUnreadSeparator(channelsPage, 'hello from current user: 4');
|
||||
|
||||
// # Switch away again
|
||||
await channelsPage.sidebarLeft.goToItem(channelB.name);
|
||||
|
||||
// * Verify the channel remains read, not reverted to unread by leaving again
|
||||
await channelsPage.sidebarLeft.assertItemRead(channelA.name);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a direct-message post can be marked unread and clears when revisited, preserving the
|
||||
* mention count across own trailing messages.
|
||||
*/
|
||||
test('MM-T248 Mark Direct Message post as Unread', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [otherUser] = await adminClient.createUsers(team.id, 1, 'dm-unread');
|
||||
const dmChannel = await adminClient.createDirectChannel([user.id, otherUser.id]);
|
||||
|
||||
// # Post an initial message, some messages from the other user, the message to mark unread, more
|
||||
// messages from the other user, then trailing messages from the main user (which should not count
|
||||
// toward the unread mention badge)
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: user.id, message: 'Initial message'});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: otherUser.id, message: `Before unread ${i}`});
|
||||
}
|
||||
const unreadPost = await adminClient.createPost({
|
||||
channel_id: dmChannel.id,
|
||||
user_id: otherUser.id,
|
||||
message: 'Unread from here',
|
||||
});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: otherUser.id, message: `After unread ${i}`});
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: user.id, message: `Own message ${i}`});
|
||||
}
|
||||
|
||||
// # Open the DM channel and mark a post as unread
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
||||
await channelsPage.toBeVisible();
|
||||
await markPostAsUnread(channelsPage, unreadPost.id);
|
||||
|
||||
// * Verify unread state appears for the DM, with the mention badge counting all 7 of the other
|
||||
// user's messages, excluding the main user's own 4 messages (1 initial + 3 trailing)
|
||||
await expectUnreadSeparator(channelsPage, 'Unread from here');
|
||||
await channelsPage.sidebarLeft.assertItemUnread(otherUser.username);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(otherUser.username)).toHaveText('7');
|
||||
|
||||
// # Leave and return to the DM channel
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
await channelsPage.sidebarLeft.assertItemUnread(otherUser.username);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(otherUser.username)).toHaveText('7');
|
||||
await channelsPage.sidebarLeft.goToItem(otherUser.username);
|
||||
|
||||
// * Verify the DM is marked read after revisiting
|
||||
await channelsPage.sidebarLeft.assertItemRead(otherUser.username);
|
||||
await expectUnreadSeparator(channelsPage, 'Unread from here');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a reply post can be marked unread from within an open RHS thread.
|
||||
*/
|
||||
test('MM-T248_2 Mark Direct Message post as Unread in a reply thread', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [otherUser] = await adminClient.createUsers(team.id, 1, 'dm-thread-unread');
|
||||
const dmChannel = await adminClient.createDirectChannel([user.id, otherUser.id]);
|
||||
|
||||
// # Post a root message, replies from the other user, the reply to mark unread, more replies from
|
||||
// the other user, then trailing replies from the main user
|
||||
const root = await adminClient.createPost({channel_id: dmChannel.id, user_id: user.id, message: 'Initial message'});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({
|
||||
channel_id: dmChannel.id,
|
||||
user_id: otherUser.id,
|
||||
message: `Before unread ${i}`,
|
||||
root_id: root.id,
|
||||
});
|
||||
}
|
||||
const unreadPost = await adminClient.createPost({
|
||||
channel_id: dmChannel.id,
|
||||
user_id: otherUser.id,
|
||||
message: 'Unread from here',
|
||||
root_id: root.id,
|
||||
});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({
|
||||
channel_id: dmChannel.id,
|
||||
user_id: otherUser.id,
|
||||
message: `After unread ${i}`,
|
||||
root_id: root.id,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await adminClient.createPost({
|
||||
channel_id: dmChannel.id,
|
||||
user_id: user.id,
|
||||
message: `Own message ${i}`,
|
||||
root_id: root.id,
|
||||
});
|
||||
}
|
||||
|
||||
// # Open the DM channel, open the thread in the RHS, and mark the reply as unread
|
||||
// (marking unread from within an open thread only affects thread-level state under CRT, whether
|
||||
// the channel is a public channel or a DM, so the LHS/mention badge are unaffected by this action)
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
||||
await channelsPage.toBeVisible();
|
||||
await (await channelsPage.centerView.getPostById(root.id)).reply();
|
||||
await markPostAsUnread(channelsPage, unreadPost.id, true);
|
||||
|
||||
// * Verify the RHS shows the unread separator for the reply
|
||||
await expect(channelsPage.sidebarRight.notificationSeparator).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a thread post can be marked unread from the RHS.
|
||||
*/
|
||||
test('MM-T250 Mark as unread in the RHS', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [author] = await adminClient.createUsers(team.id, 1, 'rhs-unread');
|
||||
const channelA = await adminClient.createPublicChannel(team.id, 'RHS Unread A');
|
||||
const channelB = await adminClient.createPublicChannel(team.id, 'RHS Unread B');
|
||||
await adminClient.addToChannel(user.id, channelA.id);
|
||||
await adminClient.addToChannel(user.id, channelB.id);
|
||||
await adminClient.addToChannel(author.id, channelA.id);
|
||||
|
||||
// # Create a thread and mark the root post unread from the RHS
|
||||
const root = await adminClient.createPost({channel_id: channelA.id, user_id: author.id, message: 'post1'});
|
||||
const reply = await adminClient.createPost({
|
||||
channel_id: channelA.id,
|
||||
user_id: author.id,
|
||||
message: 'post2',
|
||||
root_id: root.id,
|
||||
});
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelA.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await (await channelsPage.centerView.getPostById(root.id)).reply();
|
||||
await markPostAsUnread(channelsPage, root.id, true);
|
||||
|
||||
// * Verify the RHS shows the unread separator (marking unread from the RHS only affects
|
||||
// thread-level read state under CRT, so the center channel view is unaffected)
|
||||
await expect(channelsPage.sidebarRight.notificationSeparator).toBeVisible();
|
||||
|
||||
// # Switch away and back
|
||||
await channelsPage.sidebarLeft.goToItem(channelB.name);
|
||||
await channelsPage.sidebarLeft.goToItem(channelA.name);
|
||||
|
||||
// * Verify the channel returns to read state
|
||||
await channelsPage.sidebarLeft.assertItemRead(channelA.name);
|
||||
|
||||
// # Reopen the thread and mark the reply unread too
|
||||
await (await channelsPage.centerView.getPostById(root.id)).reply();
|
||||
await markPostAsUnread(channelsPage, reply.id, true);
|
||||
|
||||
// * Verify the RHS shows the unread separator for the reply as well (thread-level state only,
|
||||
// consistent with marking the root unread above — LHS channel state is unaffected either way)
|
||||
await expect(channelsPage.sidebarRight.notificationSeparator).toBeVisible();
|
||||
});
|
||||
|
||||
// Alt+click is a real shortcut for "Mark as Unread", equivalent to the post menu's own
|
||||
// "Mark as Unread" item — used here since the menu item click is flaky against the virtualized post list.
|
||||
async function markPostAsUnread(channelsPage: ChannelsPage, postId: string, rhs = false) {
|
||||
const post = rhs
|
||||
? await channelsPage.sidebarRight.getPostById(postId)
|
||||
: await channelsPage.centerView.getPostById(postId);
|
||||
|
||||
await post.toBeVisible();
|
||||
await post.container.scrollIntoViewIfNeeded();
|
||||
await post.container.click({modifiers: ['Alt']});
|
||||
}
|
||||
|
||||
async function expectUnreadSeparator(channelsPage: ChannelsPage, message: string) {
|
||||
await expect(channelsPage.centerView.notificationSeparator).toBeVisible();
|
||||
await expect(channelsPage.centerView.postViews.filter({hasText: message})).toBeVisible();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify the RHS thread refreshes replies when the center channel has changed.
|
||||
*/
|
||||
test('MM-T94 RHS fetches messages on reconnect while a different channel is in center', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [author] = await adminClient.createUsers(team.id, 1, 'reconnect-author');
|
||||
const threadChannel = await adminClient.createPublicChannel(team.id, 'RHS Reconnect');
|
||||
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
await adminClient.addToChannel(user.id, threadChannel.id);
|
||||
await adminClient.addToChannel(author.id, threadChannel.id);
|
||||
|
||||
// # Open a thread in the RHS and add an initial reply
|
||||
const root = await adminClient.createPost({
|
||||
channel_id: threadChannel.id,
|
||||
user_id: author.id,
|
||||
message: 'reconnect root',
|
||||
});
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await pw.mockWebsockets(page);
|
||||
await channelsPage.goto(team.name, threadChannel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.connectWebsockets(page);
|
||||
await (await channelsPage.centerView.getPostById(root.id)).reply();
|
||||
await channelsPage.sidebarRight.postMessage('def');
|
||||
|
||||
// # Change the center channel, then go offline and have another user reply while disconnected
|
||||
await channelsPage.sidebarLeft.goToItem(offTopic.name);
|
||||
await pw.closeWebsockets(page);
|
||||
await adminClient.createPost({channel_id: threadChannel.id, user_id: author.id, message: 'ghi', root_id: root.id});
|
||||
await pw.wait(duration.four_sec);
|
||||
|
||||
// * Verify the reply posted while disconnected has not reached the RHS
|
||||
await expect(channelsPage.sidebarRight.container).toContainText('def');
|
||||
await expect(channelsPage.sidebarRight.container).not.toContainText('ghi');
|
||||
|
||||
// # Reconnect and nudge the client with some activity so it resyncs missed messages
|
||||
await pw.connectWebsockets(page);
|
||||
await channelsPage.sidebarLeft.goToItem('town-square');
|
||||
await channelsPage.postMessage('nudge 1');
|
||||
await channelsPage.sidebarLeft.goToItem(offTopic.name);
|
||||
await channelsPage.postMessage('nudge 2');
|
||||
await pw.wait(duration.two_sec);
|
||||
|
||||
// * Verify the RHS fetches both replies after reconnecting
|
||||
await channelsPage.sidebarRight.toContainText('def');
|
||||
await channelsPage.sidebarRight.toContainText('ghi');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify selecting an emoji from the picker inserts it at the current caret position.
|
||||
*/
|
||||
test('MM-T95 Selecting an emoji from emoji picker should insert it at the cursor position', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Log in and place the caret between "Hello" and "World"
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.centerView.postCreate.writeMessage('HelloWorld!');
|
||||
for (let i = 0; i < 'World!'.length; i++) {
|
||||
await channelsPage.centerView.postCreate.input.press('ArrowLeft');
|
||||
}
|
||||
|
||||
// # Select the grinning emoji from the picker
|
||||
await channelsPage.centerView.postCreate.openEmojiPicker();
|
||||
await channelsPage.emojiGifPickerPopup.toBeVisible();
|
||||
await channelsPage.emojiGifPickerPopup.clickEmoji('grinning');
|
||||
|
||||
// * Verify the emoji was inserted at the caret and can be posted
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue('Hello 😀 World!');
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText('Hello 😀 World!');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify channel short-linking still works when the channel reference is surrounded by brackets.
|
||||
*/
|
||||
test('MM-T175 Channel short-linking still works when placed in brackets', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const linkedChannel = await adminClient.createPublicChannel(team.id, 'Shortlink Target');
|
||||
await adminClient.addToChannel(user.id, linkedChannel.id);
|
||||
|
||||
// # Post a bracketed channel shortlink from a different channel
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.postMessage(`(~${linkedChannel.name})`);
|
||||
|
||||
// # Click the rendered channel link
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.getLink(linkedChannel.display_name).click();
|
||||
|
||||
// * Verify the linked channel opens
|
||||
await expect(page).toHaveURL(`/${team.name}/channels/${linkedChannel.name}`);
|
||||
await channelsPage.centerView.header.toHaveTitle(linkedChannel.display_name);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify an emoji followed by punctuation renders as an emoji without separating the punctuation.
|
||||
*/
|
||||
test('MM-T222 Emoji characters followed by punctuation', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Post an emoticon followed by punctuation
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.postMessage(':)=');
|
||||
|
||||
// * Verify the emoticon renders and the punctuation remains immediately adjacent, with no stray
|
||||
// whitespace inserted between the rendered emoji and the punctuation
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.emoticon.first()).toHaveAttribute('aria-label', ':slightly_smiling_face:');
|
||||
await expect(lastPost.messageText.first()).toHaveText(':)=');
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify disabling "Ignore mentions" counts @channel, @here, and @all as mentions.
|
||||
*/
|
||||
test('MM-T568 Channel Notifications turn off Ignore mentions for @channel, @here and @all', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [sender] = await adminClient.createUsers(team.id, 1, 'channel-mentions');
|
||||
const channelA = await adminClient.createPublicChannel(team.id, 'Mention Source');
|
||||
const channelB = await adminClient.createPublicChannel(team.id, 'Mention Away');
|
||||
await adminClient.addToChannel(user.id, channelA.id);
|
||||
await adminClient.addToChannel(user.id, channelB.id);
|
||||
await adminClient.addToChannel(sender.id, channelA.id);
|
||||
|
||||
// # Open channel notification preferences and turn off ignore mentions
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channelA.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const notificationPreferences = await channelsPage.openChannelNotificationPreferences();
|
||||
if (await notificationPreferences.ignoreMentionsCheckbox.isChecked()) {
|
||||
await notificationPreferences.ignoreMentionsCheckbox.uncheck();
|
||||
}
|
||||
await notificationPreferences.save();
|
||||
|
||||
// # Move away and post each channel-wide mention from another user. @here only mentions users who
|
||||
// are currently online, so force the receiving user's status online first to avoid a race against
|
||||
// their websocket connection setting it asynchronously. Space the posts apart since firing them
|
||||
// back-to-back races the per-post async mention-count update and can under-count the total.
|
||||
await adminClient.updateStatus({user_id: user.id, status: 'online'});
|
||||
await channelsPage.goto(team.name, channelB.name);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@all test'});
|
||||
await pw.wait(duration.one_sec);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@channel test'});
|
||||
await pw.wait(duration.one_sec);
|
||||
await adminClient.createPost({channel_id: channelA.id, user_id: sender.id, message: '@here test'});
|
||||
|
||||
// * Verify the channel is unread and counts all 3 channel-wide mentions exactly
|
||||
await channelsPage.sidebarLeft.assertItemUnread(channelA.name);
|
||||
await expect(channelsPage.sidebarLeft.unreadMentionsBadge(channelA.name)).toHaveText('3');
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test, type ChannelsPage} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify message search displays matching results in the right-hand side, rendering markdown
|
||||
* content correctly, and that jumping to the conversation and clearing the query keep results intact.
|
||||
*/
|
||||
test('MM-T350 Searching displays results in the RHS', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
const identifier = pw.random.id();
|
||||
const message = [
|
||||
`Basic word search: Hello world! #hello ${identifier}`,
|
||||
'',
|
||||
'##### Hello',
|
||||
'',
|
||||
'```',
|
||||
'Hello',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
// # Create and search for a message with markdown content: a heading, a code block, and a hashtag
|
||||
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message});
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const query = `hello ${identifier}`;
|
||||
await submitSearch(channelsPage, query);
|
||||
|
||||
// * Verify the matching result appears in the RHS with each markdown element rendered distinctly
|
||||
await expectSearchResult(channelsPage, 'Hello world', query);
|
||||
const result = channelsPage.getSearchResultItem('Hello world');
|
||||
await expect(result.locator('h5.markdown__heading')).toContainText('Hello');
|
||||
await expect(result.locator('.post-code.post-code--wrap code')).toContainText('Hello');
|
||||
await expect(result).toContainText('#hello');
|
||||
|
||||
// # Jump to the conversation from the search result
|
||||
await result.getByRole('link', {name: 'Jump'}).click();
|
||||
|
||||
// # Reopen search and clear the query text
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.clearIfPossible();
|
||||
|
||||
// * Verify the search results remain visible after clearing the query text
|
||||
await expect(channelsPage.searchResultsContainer).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify changing timezone changes which posts match an on: date filter.
|
||||
*/
|
||||
test('MM-T595 Changing timezone changes day search results appears', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
const identifier = `timezone-${pw.random.id()}`;
|
||||
const targetMessage = `targetAM ${identifier}`;
|
||||
const targetTimestamp = Date.UTC(2018, 9, 31, 23, 59);
|
||||
|
||||
// # Create a post close to a day boundary and search in UTC
|
||||
await adminClient.patchUser({
|
||||
id: user.id,
|
||||
timezone: {automaticTimezone: '', manualTimezone: 'UTC', useAutomaticTimezone: 'false'},
|
||||
});
|
||||
await adminClient.createPost({
|
||||
channel_id: channel.id,
|
||||
user_id: user.id,
|
||||
message: targetMessage,
|
||||
create_at: targetTimestamp,
|
||||
});
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const utcQuery = `on:2018-10-31 ${identifier}`;
|
||||
await submitSearch(channelsPage, utcQuery);
|
||||
|
||||
// * Verify the result appears for the UTC date
|
||||
await expectSearchResult(channelsPage, targetMessage, utcQuery);
|
||||
|
||||
// # Change timezone and run the same date-filtered search
|
||||
await adminClient.patchUser({
|
||||
id: user.id,
|
||||
timezone: {automaticTimezone: '', manualTimezone: 'Europe/Brussels', useAutomaticTimezone: 'false'},
|
||||
});
|
||||
await channelsPage.page.reload();
|
||||
await submitSearch(channelsPage, utcQuery);
|
||||
|
||||
// * Verify the post no longer matches the previous day in the new timezone
|
||||
await expectNoSearchResult(channelsPage, targetMessage);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify editing a date-filtered search query, via the interactive day picker, updates the search results.
|
||||
*/
|
||||
test('MM-T599 Edit date and search again', async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
const targetMessage = `calendarUpdate-${pw.random.id()}`;
|
||||
const targetTimestamp = Date.UTC(2019, 0, 15, 9, 30);
|
||||
|
||||
// # Create a dated post and pin the client's clock to that date, so "today" in the day picker is Jan 15, 2019
|
||||
await adminClient.createPost({
|
||||
channel_id: channel.id,
|
||||
user_id: user.id,
|
||||
message: targetMessage,
|
||||
create_at: targetTimestamp,
|
||||
});
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await page.clock.setFixedTime(targetTimestamp);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Type "on:" to open the day picker and click today's date
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.clearIfPossible();
|
||||
await channelsPage.searchBox.searchInput.fill('on:');
|
||||
await channelsPage.searchBox.getDayPickerDay(15).click();
|
||||
|
||||
// * Verify the search box auto-populates with the selected date
|
||||
await expect(channelsPage.searchBox.searchInput).toHaveValue('on:2019-01-15 ');
|
||||
|
||||
// # Complete and submit the query
|
||||
await channelsPage.searchBox.searchInput.press('End');
|
||||
await channelsPage.searchBox.searchInput.pressSequentially(targetMessage);
|
||||
await channelsPage.searchBox.searchInput.press('Enter');
|
||||
await expect(channelsPage.searchResultsContainer).toBeVisible();
|
||||
|
||||
// * Verify exactly one matching result for the original date
|
||||
await expect(channelsPage.searchResultItems).toHaveCount(1);
|
||||
await expect(channelsPage.getSearchResultItem(targetMessage)).toBeVisible();
|
||||
|
||||
// # Reopen the channel, then reopen the day picker by backspacing right after the date
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.clearIfPossible();
|
||||
const originalDateQuery = `on:2019-01-15 ${targetMessage}`;
|
||||
await channelsPage.searchBox.searchInput.fill(originalDateQuery);
|
||||
for (let i = 0; i < targetMessage.length + 1; i++) {
|
||||
await channelsPage.searchBox.searchInput.press('ArrowLeft');
|
||||
}
|
||||
await channelsPage.searchBox.searchInput.press('Backspace');
|
||||
|
||||
// # Click the day after the pinned date
|
||||
await channelsPage.searchBox.getDayPickerDay(16).click();
|
||||
|
||||
// * Verify the search box updates to the edited date, then submit
|
||||
await expect(channelsPage.searchBox.searchInput).toHaveValue(`on:2019-01-16 ${targetMessage}`);
|
||||
await channelsPage.searchBox.searchInput.press('Enter');
|
||||
|
||||
// * Verify the original post is not returned for the edited date
|
||||
await expect(channelsPage.searchResultItems).toHaveCount(0);
|
||||
});
|
||||
|
||||
async function submitSearch(channelsPage: ChannelsPage, query: string) {
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.clearIfPossible();
|
||||
await channelsPage.searchBox.searchInput.fill(query);
|
||||
await channelsPage.searchBox.searchInput.press('Enter');
|
||||
await expect(channelsPage.searchResultsContainer).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectSearchResult(
|
||||
channelsPage: ChannelsPage,
|
||||
text: string,
|
||||
query?: string,
|
||||
timeout = duration.half_min,
|
||||
) {
|
||||
const result = channelsPage.getSearchResultItem(text);
|
||||
|
||||
await expect(async () => {
|
||||
if (await result.isVisible({timeout: duration.one_sec}).catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
await submitSearch(channelsPage, query);
|
||||
} else {
|
||||
await channelsPage.searchBox.searchInput.press('Enter');
|
||||
}
|
||||
await expect(result).toBeVisible({timeout: duration.one_sec * 5});
|
||||
}).toPass({timeout});
|
||||
}
|
||||
|
||||
async function expectNoSearchResult(channelsPage: ChannelsPage, text: string) {
|
||||
await expect(channelsPage.getSearchResultItem(text)).toHaveCount(0);
|
||||
}
|
||||
@@ -211,11 +211,6 @@ export async function createGroupConstrainedPrivateChannel(client: Client4, team
|
||||
} as any);
|
||||
}
|
||||
|
||||
export async function createPublicChannel(client: Client4, teamId: string) {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
return client.createChannel({team_id: teamId, name: `pub-${id}`, display_name: `PUB-${id}`, type: 'O'} as any);
|
||||
}
|
||||
|
||||
export async function createTeamAdmin(adminClient: Client4, teamId: string) {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
const user = await adminClient.createUser(
|
||||
|
||||
+1
-2
@@ -15,7 +15,6 @@ import {
|
||||
assignChannelsToPolicy,
|
||||
unassignChannelsFromPolicy,
|
||||
createPrivateChannel,
|
||||
createPublicChannel,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
addAttributeRule,
|
||||
@@ -784,7 +783,7 @@ test.describe('Team Settings Modal - Policy Editor', () => {
|
||||
|
||||
// # Create 55 public channels — more than the 50-result autocomplete cap
|
||||
for (let i = 0; i < 55; i++) {
|
||||
const pub = await createPublicChannel(adminClient, team.id);
|
||||
const pub = await adminClient.createPublicChannel(team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, pub.id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user