mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
E2E/Playwright: Migrate RFQA browser tests (batch 5, 81-100) (#37417)
* E2E/Playwright: Migrate RFQA browser tests (batch 4, 61-80) Migrate the fourth batch of Rainforest QA browser tests into Playwright (migration plan None-core #34-53, keys MM-T480..MM-T1257): 17 new/covered specs across notifications, search, channel_settings, sidebar_left, keyboard_shortcuts, and direct_messages_modal, plus 3 duplicate keys folded into existing tests (MM-T480, MM-T1256, MM-T1257). POM/infra: add a ChannelMenu page object for the channel header dropdown, an EditChannelHeaderModal, a header field on the channel-settings info tab, a channel-wide-mentions checkbox on NotificationsSettings, a getConfirmModal helper, and a clickNotification browser-API mock helper. Give the channel header menu and the confirm modal proper accessible names so they can be targeted semantically. Notification specs run on all browsers now that the Notification API stub works headless on Chromium and Firefox. * E2E/Playwright: Migrate RFQA browser tests (batch 5, 81-100) Migrate the fifth batch of Rainforest QA browser tests into Playwright across keyboard_shortcuts, search, archived_channels, channel_settings, plugin_marketplace, messaging, and emoji_picker (keys MM-T1247..MM-T2365), folding duplicate coverage into existing keyboard-shortcut specs and documenting skipped cases inline. POM/infra: add AddPeopleToChannelModal and MarketplaceModal page objects, manage/add member buttons on the channel members RHS, and an App Marketplace entry on the global header; target the search "Jump" link through the existing SearchResultsPanel POM. Also fix a webapp bug where a post body disappeared when an inline edit was cancelled mid-animation (found via the up-arrow edit shortcut spec). * E2E/Playwright: Fail on missing scroll bounding boxes * E2E/Playwright: Group related batch 5 specs * E2E/Playwright: Avoid duplicate creator membership * E2E/Playwright: Group Rainforest batches 1 through 4 --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -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 AddPeopleToChannelModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly closeButton;
|
||||
readonly alreadyInChannelLabel;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||
this.alreadyInChannelLabel = container.getByText('Already in channel');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Types into the auto-focused react-select search input. The input is not a
|
||||
* standard textbox, so type via the keyboard once the modal is visible.
|
||||
*/
|
||||
async search(text: string) {
|
||||
await this.toBeVisible();
|
||||
await this.container.page().keyboard.type(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 MarketplaceModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly closeButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async notToBeVisible() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.closeButton.click();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ export default class ChannelsSidebarRight {
|
||||
readonly closeButton;
|
||||
readonly expandButton;
|
||||
readonly collapseButton;
|
||||
readonly manageMembersButton;
|
||||
readonly addMembersButton;
|
||||
readonly postCreate;
|
||||
readonly rhsPostBody;
|
||||
readonly scheduledPostIndicator;
|
||||
@@ -47,6 +49,10 @@ export default class ChannelsSidebarRight {
|
||||
this.expandButton = container.getByRole('button', {name: 'Expand Sidebar Icon'});
|
||||
this.collapseButton = container.getByRole('button', {name: 'Collapse Sidebar Icon'});
|
||||
|
||||
// Member-management controls shown in the channel members list (RHS).
|
||||
this.manageMembersButton = container.getByRole('button', {name: 'Manage'});
|
||||
this.addMembersButton = container.getByRole('button', {name: 'Add'});
|
||||
|
||||
this.editTextbox = container.locator('#edit_textbox');
|
||||
this.postEdit = new ChannelsPostEdit(container.getByTestId('post-edit-container'));
|
||||
this.currentVersionEditedPosttext = (postID: any) => container.locator(`#rhsPostMessageText_${postID} p`);
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class GlobalHeader {
|
||||
readonly helpButton;
|
||||
readonly searchBox;
|
||||
readonly userProfileMenu;
|
||||
readonly appMarketplaceMenuItem;
|
||||
|
||||
constructor(channelsPage: ChannelsPage, container: Locator) {
|
||||
this.channelsPage = channelsPage;
|
||||
@@ -31,6 +32,9 @@ export default class GlobalHeader {
|
||||
this.helpButton = container.getByRole('button', {name: 'Help'});
|
||||
this.searchBox = container.locator('#searchFormContainer');
|
||||
this.userProfileMenu = container.locator('#userAccountMenuButton');
|
||||
|
||||
// Rendered in a portal at the page level once the product switch menu is open.
|
||||
this.appMarketplaceMenuItem = container.page().getByRole('menuitem', {name: 'App Marketplace'});
|
||||
}
|
||||
|
||||
async toBeVisible(name: string) {
|
||||
@@ -42,6 +46,14 @@ export default class GlobalHeader {
|
||||
await this.container.getByRole('link', {name}).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the product switch menu and selects the "App Marketplace" item.
|
||||
*/
|
||||
async openAppMarketplace() {
|
||||
await this.productSwitchMenu.click();
|
||||
await this.appMarketplaceMenuItem.click();
|
||||
}
|
||||
|
||||
async openSettings() {
|
||||
await expect(this.settingsButton).toBeVisible();
|
||||
await this.settingsButton.click();
|
||||
|
||||
@@ -19,6 +19,8 @@ import ChannelNotificationPreferencesModal from './channels/channel_notification
|
||||
import ChannelSettingsModal from './channels/channel_settings/channel_settings_modal';
|
||||
import ChannelsSidebarLeft from './channels/sidebar_left';
|
||||
import ChannelsSidebarRight from './channels/sidebar_right';
|
||||
import AddPeopleToChannelModal from './channels/add_people_to_channel_modal';
|
||||
import MarketplaceModal from './channels/marketplace_modal';
|
||||
import DeletePostConfirmationDialog from './channels/delete_post_confirmation_dialog';
|
||||
import DeletePostModal from './channels/delete_post_modal';
|
||||
import DeleteScheduledPostModal from './channels/delete_scheduled_post_modal';
|
||||
@@ -98,6 +100,8 @@ const components = {
|
||||
ChannelSettingsModal,
|
||||
ChannelsSidebarLeft,
|
||||
ChannelsSidebarRight,
|
||||
AddPeopleToChannelModal,
|
||||
MarketplaceModal,
|
||||
DeletePostConfirmationDialog,
|
||||
DeletePostModal,
|
||||
DeleteScheduledPostModal,
|
||||
@@ -181,6 +185,8 @@ export {
|
||||
ChannelSettingsModal,
|
||||
ChannelsSidebarLeft,
|
||||
ChannelsSidebarRight,
|
||||
AddPeopleToChannelModal,
|
||||
MarketplaceModal,
|
||||
DeletePostConfirmationDialog,
|
||||
DeletePostModal,
|
||||
DeleteScheduledPostModal,
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class ChannelsPage {
|
||||
readonly scheduleMessageModal;
|
||||
readonly burnOnReadConfirmationModal;
|
||||
readonly searchResultsPanel;
|
||||
readonly marketplaceModal;
|
||||
readonly archivedChannelMessage;
|
||||
|
||||
readonly postContainer;
|
||||
@@ -102,6 +103,7 @@ export default class ChannelsPage {
|
||||
page.getByRole('dialog').filter({hasText: /burn|delete/i}),
|
||||
);
|
||||
this.searchResultsPanel = new components.SearchResultsPanel(page.locator('#searchContainer'));
|
||||
this.marketplaceModal = new components.MarketplaceModal(page.getByRole('dialog', {name: 'App Marketplace'}));
|
||||
|
||||
// Menus
|
||||
// The channel header dropdown menu's accessible name is "<channel> Channel Menu".
|
||||
@@ -161,6 +163,10 @@ export default class ChannelsPage {
|
||||
return this.invitePeopleModal;
|
||||
}
|
||||
|
||||
getAddPeopleToChannelModal() {
|
||||
return new components.AddPeopleToChannelModal(this.page.getByRole('dialog', {name: /Add people to/}));
|
||||
}
|
||||
|
||||
async getMembersInvitedModal(teamDisplayName: string) {
|
||||
this.membersInvitedModal = new components.MembersInvitedModal(
|
||||
this.page.getByRole('dialog', {name: `invited to ${teamDisplayName}`}),
|
||||
|
||||
+38
@@ -43,3 +43,41 @@ test('MM-T1716 hides center and RHS message boxes in an archived channel', {tag:
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that opening the reply thread of a saved post from an archived channel does not offer a reply box.
|
||||
*/
|
||||
test('MM-T1722 opens a saved post from an archived channel without a reply box', {tag: '@channels'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and prepare a channel they belong to
|
||||
const {user, team, adminClient, userClient} = await pw.initSetup();
|
||||
const message = `Archived saved post ${pw.random.id()}`;
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Archive Saved');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
// # Post a message in the channel, then archive the channel
|
||||
const post = await adminClient.createPost({channel_id: channel.id, message});
|
||||
await adminClient.deleteChannel(channel.id);
|
||||
|
||||
// # Save the post for the test user
|
||||
await userClient.savePreferences(user.id, [
|
||||
{user_id: user.id, category: 'flagged_post', name: post.id, value: 'true'},
|
||||
]);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Saved messages panel
|
||||
await channelsPage.globalHeader.openSavedMessages();
|
||||
|
||||
// * Verify the saved post from the archived channel is listed
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(message);
|
||||
|
||||
// # Click the reply arrow on the saved post
|
||||
await channelsPage.searchResultsPanel.replyToResultWithText(message);
|
||||
|
||||
// * Verify the thread opens without a reply box because the channel is archived
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).not.toBeVisible();
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// 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 member list of an archived channel is viewable but read-only (no member management).
|
||||
*/
|
||||
test('MM-T1671 shows a read-only member list for an archived channel', {tag: '@channels'}, async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Archive ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Archive the channel
|
||||
await channelsPage.archiveChannel();
|
||||
|
||||
// # Open the channel members list
|
||||
const channelMenu = await channelsPage.openChannelMenu();
|
||||
await channelMenu.members.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the member list is shown with the channel members
|
||||
await expect(channelsPage.sidebarRight.container.getByText(adminUser.username).first()).toBeVisible();
|
||||
|
||||
// * Verify member management controls are not available for the archived channel
|
||||
await expect(channelsPage.sidebarRight.manageMembersButton).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarRight.addMembersButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a post from an archived channel can be found in search and its Jump link opens the
|
||||
* archived channel in read-only mode.
|
||||
*/
|
||||
test('MM-T1679 opens an archived channel by jumping from search results', {tag: '@channels'}, async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Archived ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
const message = `archived post ${pw.random.id()}`;
|
||||
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();
|
||||
|
||||
// # Archive the channel, then move to another channel
|
||||
await channelsPage.archiveChannel();
|
||||
await channelsPage.sidebarLeft.goToItem('town-square');
|
||||
await channelsPage.centerView.header.toHaveTitle('Town Square');
|
||||
|
||||
// # Search for the message posted in the archived channel
|
||||
await channelsPage.searchFor(message);
|
||||
|
||||
// # Jump to the post from the search results
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(message)).toBeVisible();
|
||||
await channelsPage.searchResultsPanel.jumpToResultWithText(message);
|
||||
|
||||
// * Verify the archived channel is opened in read-only mode
|
||||
await channelsPage.centerView.header.toHaveTitle(channel.display_name);
|
||||
await expect(channelsPage.archivedChannelMessage).toBeVisible();
|
||||
});
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that opening the reply thread of a saved post from an archived channel does not offer a reply box.
|
||||
*/
|
||||
test('MM-T1722 opens a saved post from an archived channel without a reply box', {tag: '@channels'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and prepare a channel they belong to
|
||||
const {user, team, adminClient, userClient} = await pw.initSetup();
|
||||
const message = `Archived saved post ${pw.random.id()}`;
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Archive Saved');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
// # Post a message in the channel, then archive the channel
|
||||
const post = await adminClient.createPost({channel_id: channel.id, message});
|
||||
await adminClient.deleteChannel(channel.id);
|
||||
|
||||
// # Save the post for the test user
|
||||
await userClient.savePreferences(user.id, [
|
||||
{user_id: user.id, category: 'flagged_post', name: post.id, value: 'true'},
|
||||
]);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Saved messages panel
|
||||
await channelsPage.globalHeader.openSavedMessages();
|
||||
|
||||
// * Verify the saved post from the archived channel is listed
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(message);
|
||||
|
||||
// # Click the reply arrow on the saved post
|
||||
await channelsPage.searchResultsPanel.replyToResultWithText(message);
|
||||
|
||||
// * Verify the thread opens without a reply box because the channel is archived
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).not.toBeVisible();
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user already in a channel is shown as "Already in channel" (not addable) in the
|
||||
* add-people modal.
|
||||
*/
|
||||
test(
|
||||
'MM-T1809 marks an existing channel member as already in channel in the add people modal',
|
||||
{tag: '@channel_settings'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [member] = await adminClient.createUsers(team.id, 1, 'existing');
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Members ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
await adminClient.addToChannel(member.id, channel.id);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the channel members list and the add-people modal
|
||||
const channelMenu = await channelsPage.openChannelMenu();
|
||||
await channelMenu.members.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.addMembersButton.click();
|
||||
|
||||
const addModal = channelsPage.getAddPeopleToChannelModal();
|
||||
await addModal.toBeVisible();
|
||||
|
||||
// # Search for the member who is already in the channel (the react-select input is auto-focused)
|
||||
await addModal.search(member.username);
|
||||
|
||||
// * Verify the member is marked as already in the channel
|
||||
await expect(addModal.alreadyInChannelLabel).toBeVisible();
|
||||
},
|
||||
);
|
||||
+38
@@ -51,3 +51,41 @@ test(
|
||||
await expect(popover).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a channel header can be added from the "Set header" link in a new channel's intro,
|
||||
* that cancelling the modal makes no change, and that the saved header appears in the channel info.
|
||||
*/
|
||||
test('MM-T880 adds a channel header from the intro Set header link', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a new public channel so its intro (with the "Set header" link) is shown
|
||||
const channelName = `channel header ${pw.random.id()}`;
|
||||
await channelsPage.newChannel(channelName, 'O');
|
||||
await channelsPage.centerView.header.toHaveTitle(channelName);
|
||||
|
||||
// # Open the Edit Header modal from the intro and cancel without entering text
|
||||
await channelsPage.centerView.channelIntro.getByRole('button', {name: 'Set header'}).click();
|
||||
await channelsPage.editChannelHeaderModal.toBeVisible();
|
||||
await channelsPage.editChannelHeaderModal.cancel();
|
||||
|
||||
// # Reopen the Edit Header modal and save a header
|
||||
const header = 'this is the channel header';
|
||||
await channelsPage.centerView.channelIntro.getByRole('button', {name: 'Set header'}).click();
|
||||
await channelsPage.editChannelHeaderModal.toBeVisible();
|
||||
await channelsPage.editChannelHeaderModal.setHeader(header);
|
||||
|
||||
// * Verify the header-change system message is posted
|
||||
await channelsPage.centerView.waitUntilLastPostContains(`updated the channel header to: ${header}`);
|
||||
|
||||
// # Open the channel info panel from the channel header menu
|
||||
const channelMenu = await channelsPage.openChannelMenu();
|
||||
await channelMenu.viewInfo.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the saved header appears in the channel info panel
|
||||
await expect(channelsPage.sidebarRight.container.getByText(header)).toBeVisible();
|
||||
});
|
||||
+34
@@ -3,6 +3,40 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that creating and archiving a public channel in one browser session is synced in
|
||||
* real time to another session of the same user.
|
||||
*/
|
||||
test('MM-T837 syncs public channel create and archive across sessions', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Log in as the same user in two separate browser sessions
|
||||
const {channelsPage: firstSession} = await pw.testBrowser.login(user);
|
||||
await firstSession.goto(team.name, 'town-square');
|
||||
await firstSession.toBeVisible();
|
||||
|
||||
const {channelsPage: secondSession} = await pw.testBrowser.login(user);
|
||||
await secondSession.goto(team.name, 'town-square');
|
||||
await secondSession.toBeVisible();
|
||||
|
||||
// # Create a public channel in the first session
|
||||
const channelName = `RF ${pw.random.id()}`;
|
||||
await firstSession.newChannel(channelName, 'O');
|
||||
await firstSession.centerView.header.toHaveTitle(channelName);
|
||||
|
||||
// * Verify the new channel appears in the sidebar of both sessions
|
||||
await expect(firstSession.sidebarLeft.container.getByText(channelName)).toBeVisible();
|
||||
await expect(secondSession.sidebarLeft.container.getByText(channelName)).toBeVisible();
|
||||
|
||||
// # Archive the channel in the first session, then navigate away from the archived channel
|
||||
await firstSession.archiveChannel();
|
||||
await firstSession.sidebarLeft.goToItem('town-square');
|
||||
|
||||
// * Verify the channel is removed from the sidebar of both sessions
|
||||
await expect(firstSession.sidebarLeft.container.getByText(channelName)).not.toBeVisible();
|
||||
await expect(secondSession.sidebarLeft.container.getByText(channelName)).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that leaving a channel in one browser session removes it from the sidebar of another
|
||||
* session that is viewing a different channel, without disrupting that other session's current view.
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that creating and archiving a public channel in one browser session is synced in
|
||||
* real time to another session of the same user.
|
||||
*/
|
||||
test('MM-T837 syncs public channel create and archive across sessions', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Log in as the same user in two separate browser sessions
|
||||
const {channelsPage: firstSession} = await pw.testBrowser.login(user);
|
||||
await firstSession.goto(team.name, 'town-square');
|
||||
await firstSession.toBeVisible();
|
||||
|
||||
const {channelsPage: secondSession} = await pw.testBrowser.login(user);
|
||||
await secondSession.goto(team.name, 'town-square');
|
||||
await secondSession.toBeVisible();
|
||||
|
||||
// # Create a public channel in the first session
|
||||
const channelName = `RF ${pw.random.id()}`;
|
||||
await firstSession.newChannel(channelName, 'O');
|
||||
await firstSession.centerView.header.toHaveTitle(channelName);
|
||||
|
||||
// * Verify the new channel appears in the sidebar of both sessions
|
||||
await expect(firstSession.sidebarLeft.container.getByText(channelName)).toBeVisible();
|
||||
await expect(secondSession.sidebarLeft.container.getByText(channelName)).toBeVisible();
|
||||
|
||||
// # Archive the channel in the first session, then navigate away from the archived channel
|
||||
await firstSession.archiveChannel();
|
||||
await firstSession.sidebarLeft.goToItem('town-square');
|
||||
|
||||
// * Verify the channel is removed from the sidebar of both sessions
|
||||
await expect(firstSession.sidebarLeft.container.getByText(channelName)).not.toBeVisible();
|
||||
await expect(secondSession.sidebarLeft.container.getByText(channelName)).not.toBeVisible();
|
||||
});
|
||||
+41
-1
@@ -1,7 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user viewing a thread in the right-hand side can no longer reply once they are
|
||||
* removed from the channel.
|
||||
*/
|
||||
test(
|
||||
'MM-T843 removes the reply box in the right-hand side after the user is removed from the channel',
|
||||
{tag: '@channel_settings'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a channel, add the user, and post a root message as the admin
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Remove ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
const rootMessage = `root ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: channel.id, user_id: adminUser.id, message: rootMessage});
|
||||
|
||||
// # Log in as the user and open the thread in the right-hand side
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const rootPost = await channelsPage.getLastPost();
|
||||
await rootPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// # Post a reply in the right-hand side
|
||||
const reply = `reply ${pw.random.id()}`;
|
||||
await channelsPage.sidebarRight.postMessage(reply);
|
||||
await channelsPage.sidebarRight.toContainText(reply);
|
||||
|
||||
// * Verify the reply box is available before removal
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).toBeVisible();
|
||||
|
||||
// # Remove the user from the channel
|
||||
await adminClient.removeFromChannel(user.id, channel.id);
|
||||
|
||||
// * Verify the reply box in the right-hand side is no longer available
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).not.toBeVisible({timeout: pw.duration.ten_sec});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that users added to a public channel in one batch are combined into a single system
|
||||
+34
@@ -3,6 +3,40 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that releasing a mouse drag that started inside a modal but ends outside it does not
|
||||
* close the modal, while a genuine click outside the modal does close it.
|
||||
*/
|
||||
test('MM-T841 keeps a modal open when a drag is released outside it', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Channel Settings modal
|
||||
const channelSettings = await channelsPage.openChannelSettings();
|
||||
const modal = channelSettings.container;
|
||||
|
||||
// # Press the mouse down inside the modal, drag to outside the modal, then release
|
||||
const box = await modal.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error('Channel Settings modal has no bounding box');
|
||||
}
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(5, 5);
|
||||
await page.mouse.up();
|
||||
|
||||
// * Verify the modal remains open because the press started inside it
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// # Click outside the modal (both press and release on the backdrop)
|
||||
await page.mouse.click(5, 5);
|
||||
|
||||
// * Verify the modal now closes
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that cancelling the create-channel modal does not create or join a channel, and that
|
||||
* leaving a channel removes it from the sidebar.
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that releasing a mouse drag that started inside a modal but ends outside it does not
|
||||
* close the modal, while a genuine click outside the modal does close it.
|
||||
*/
|
||||
test('MM-T841 keeps a modal open when a drag is released outside it', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Channel Settings modal
|
||||
const channelSettings = await channelsPage.openChannelSettings();
|
||||
const modal = channelSettings.container;
|
||||
|
||||
// # Press the mouse down inside the modal, drag to outside the modal, then release
|
||||
const box = await modal.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error('Channel Settings modal has no bounding box');
|
||||
}
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(5, 5);
|
||||
await page.mouse.up();
|
||||
|
||||
// * Verify the modal remains open because the press started inside it
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// # Click outside the modal (both press and release on the backdrop)
|
||||
await page.mouse.click(5, 5);
|
||||
|
||||
// * Verify the modal now closes
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user viewing a thread in the right-hand side can no longer reply once they are
|
||||
* removed from the channel.
|
||||
*/
|
||||
test(
|
||||
'MM-T843 removes the reply box in the right-hand side after the user is removed from the channel',
|
||||
{tag: '@channel_settings'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a channel, add the user, and post a root message as the admin
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Remove ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
const rootMessage = `root ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: channel.id, user_id: adminUser.id, message: rootMessage});
|
||||
|
||||
// # Log in as the user and open the thread in the right-hand side
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const rootPost = await channelsPage.getLastPost();
|
||||
await rootPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// # Post a reply in the right-hand side
|
||||
const reply = `reply ${pw.random.id()}`;
|
||||
await channelsPage.sidebarRight.postMessage(reply);
|
||||
await channelsPage.sidebarRight.toContainText(reply);
|
||||
|
||||
// * Verify the reply box is available before removal
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).toBeVisible();
|
||||
|
||||
// # Remove the user from the channel
|
||||
await adminClient.removeFromChannel(user.id, channel.id);
|
||||
|
||||
// * Verify the reply box in the right-hand side is no longer available
|
||||
await expect(channelsPage.sidebarRight.postCreate.input).not.toBeVisible({timeout: pw.duration.ten_sec});
|
||||
},
|
||||
);
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify a channel header can be added from the "Set header" link in a new channel's intro,
|
||||
* that cancelling the modal makes no change, and that the saved header appears in the channel info.
|
||||
*/
|
||||
test('MM-T880 adds a channel header from the intro Set header link', {tag: '@channel_settings'}, async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a new public channel so its intro (with the "Set header" link) is shown
|
||||
const channelName = `channel header ${pw.random.id()}`;
|
||||
await channelsPage.newChannel(channelName, 'O');
|
||||
await channelsPage.centerView.header.toHaveTitle(channelName);
|
||||
|
||||
// # Open the Edit Header modal from the intro and cancel without entering text
|
||||
await channelsPage.centerView.channelIntro.getByRole('button', {name: 'Set header'}).click();
|
||||
await channelsPage.editChannelHeaderModal.toBeVisible();
|
||||
await channelsPage.editChannelHeaderModal.cancel();
|
||||
|
||||
// # Reopen the Edit Header modal and save a header
|
||||
const header = 'this is the channel header';
|
||||
await channelsPage.centerView.channelIntro.getByRole('button', {name: 'Set header'}).click();
|
||||
await channelsPage.editChannelHeaderModal.toBeVisible();
|
||||
await channelsPage.editChannelHeaderModal.setHeader(header);
|
||||
|
||||
// * Verify the header-change system message is posted
|
||||
await channelsPage.centerView.waitUntilLastPostContains(`updated the channel header to: ${header}`);
|
||||
|
||||
// # Open the channel info panel from the channel header menu
|
||||
const channelMenu = await channelsPage.openChannelMenu();
|
||||
await channelMenu.viewInfo.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the saved header appears in the channel info panel
|
||||
await expect(channelsPage.sidebarRight.container.getByText(header)).toBeVisible();
|
||||
});
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that deleting a parent post from a direct message reply thread removes the post, its reply, and closes the thread.
|
||||
*/
|
||||
test(
|
||||
'MM-T454 deletes a parent post in a direct message from the reply thread',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus another user to direct message
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [otherUser] = await adminClient.createUsers(team.id, 1, 'other');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open a direct message channel with the other user
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(otherUser);
|
||||
await dmModal.goToChannel();
|
||||
|
||||
// # Post a parent message and reply to it in the thread
|
||||
const parent = `This is the parent post ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(parent);
|
||||
const parentPost = await channelsPage.getLastPost();
|
||||
const parentId = await parentPost.getId();
|
||||
await parentPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage(`This is a reply ${pw.random.id()}`);
|
||||
|
||||
// # Delete the parent post (which has a reply) while its thread is open.
|
||||
// The parent is deleted from the center post menu, which is equivalent to and more
|
||||
// stable than the virtualized RHS root menu; the resulting behavior is identical.
|
||||
const centerParent = await channelsPage.centerView.getPostById(parentId);
|
||||
await centerParent.hover();
|
||||
await centerParent.postMenu.openDotMenu();
|
||||
await channelsPage.postDotMenu.deleteMenuItem.click();
|
||||
await channelsPage.deletePostModal.toBeVisible();
|
||||
await channelsPage.deletePostModal.confirm();
|
||||
|
||||
// * Verify the reply thread closes and the parent post is gone from the center channel
|
||||
await expect(channelsPage.sidebarRight.container).not.toBeVisible();
|
||||
await expect(channelsPage.centerView.container.getByText(parent, {exact: true})).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that mentioning a user inside a different pair of users' direct message does not notify the mentioned user.
|
||||
*/
|
||||
test('MM-T448 does not notify a user mentioned in a different DM', {tag: '@direct_messages'}, async ({pw}) => {
|
||||
// # Create the test user plus two more users on the team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [partner, mentioned] = await adminClient.createUsers(team.id, 2, 'dmuser');
|
||||
|
||||
const token = `dmmention${pw.random.id()}`;
|
||||
|
||||
// # As the test user, open a DM with the partner and mention the third user there
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(partner);
|
||||
await dmModal.goToChannel();
|
||||
await channelsPage.postMessage(`@${mentioned.username} ${token}`);
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(token);
|
||||
|
||||
// # Log in as the mentioned user and open Recent Mentions
|
||||
const {channelsPage: mentionedPage} = await pw.testBrowser.login(mentioned);
|
||||
await mentionedPage.goto(team.name, 'town-square');
|
||||
await mentionedPage.toBeVisible();
|
||||
await mentionedPage.globalHeader.openRecentMentions();
|
||||
|
||||
// * Verify the mentioned user did not receive the mention from the other users' DM
|
||||
await mentionedPage.searchResultsPanel.toBeVisible();
|
||||
await expect(mentionedPage.searchResultsPanel.getResultByText(token)).toHaveCount(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that deleting a parent post from a direct message reply thread removes the post, its reply, and closes the thread.
|
||||
*/
|
||||
test(
|
||||
'MM-T454 deletes a parent post in a direct message from the reply thread',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus another user to direct message
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [otherUser] = await adminClient.createUsers(team.id, 1, 'other');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open a direct message channel with the other user
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(otherUser);
|
||||
await dmModal.goToChannel();
|
||||
|
||||
// # Post a parent message and reply to it in the thread
|
||||
const parent = `This is the parent post ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(parent);
|
||||
const parentPost = await channelsPage.getLastPost();
|
||||
const parentId = await parentPost.getId();
|
||||
await parentPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage(`This is a reply ${pw.random.id()}`);
|
||||
|
||||
// # Delete the parent post (which has a reply) while its thread is open.
|
||||
// The parent is deleted from the center post menu, which is equivalent to and more
|
||||
// stable than the virtualized RHS root menu; the resulting behavior is identical.
|
||||
const centerParent = await channelsPage.centerView.getPostById(parentId);
|
||||
await centerParent.hover();
|
||||
await centerParent.postMenu.openDotMenu();
|
||||
await channelsPage.postDotMenu.deleteMenuItem.click();
|
||||
await channelsPage.deletePostModal.toBeVisible();
|
||||
await channelsPage.deletePostModal.confirm();
|
||||
|
||||
// * Verify the reply thread closes and the parent post is gone from the center channel
|
||||
await expect(channelsPage.sidebarRight.container).not.toBeVisible();
|
||||
await expect(channelsPage.centerView.container.getByText(parent, {exact: true})).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that a direct message can be opened from a user's profile popover on their post.
|
||||
*/
|
||||
test('MM-T455 opens a direct message from a profile popover', {tag: '@direct_messages'}, async ({pw}) => {
|
||||
// # Create the test user plus another user who posts in a shared channel
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [author] = await adminClient.createUsers(team.id, 1, 'author');
|
||||
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
await adminClient.addToChannel(author.id, offTopic.id);
|
||||
|
||||
const token = `popover dm ${pw.random.id()}`;
|
||||
const {client: authorClient} = await pw.makeClient(author);
|
||||
await authorClient.createPost({channel_id: offTopic.id, message: token});
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the author's profile popover from their post and click Message
|
||||
const post = await channelsPage.getLastPost();
|
||||
const popover = await channelsPage.openProfilePopover(post);
|
||||
await popover.message();
|
||||
|
||||
// * Verify a direct message channel with the author is opened
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/@${author.username}`);
|
||||
await channelsPage.centerView.header.toHaveTitle(author.username);
|
||||
|
||||
// # Post a message in the direct message channel
|
||||
const dmMessage = `direct message ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(dmMessage);
|
||||
|
||||
// * Verify the direct message is posted
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(dmMessage);
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {getChannelSlugFromUrl} from './helpers';
|
||||
|
||||
/**
|
||||
* @objective Verify that a group message lists its members and that adding another member creates a new group message.
|
||||
*/
|
||||
test(
|
||||
'MM-T467 adds a user to a group message to create a new group message',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus three more users on the team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [member1, member2, member3] = await adminClient.createUsers(team.id, 3, 'gm');
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a group message with two members
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(member1);
|
||||
await dmModal.selectUser(member2);
|
||||
await dmModal.goToChannel();
|
||||
const firstSlug = getChannelSlugFromUrl(page);
|
||||
|
||||
// * Verify the group message lists both members in the header
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
await channelsPage.centerView.header.toHaveTitle(member2.username);
|
||||
|
||||
// # Create a group message that adds a third member
|
||||
const dmModal2 = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal2.selectUser(member1);
|
||||
await dmModal2.selectUser(member2);
|
||||
await dmModal2.selectUser(member3);
|
||||
await dmModal2.goToChannel();
|
||||
|
||||
// * Verify a new, different group message channel is created that includes the added member
|
||||
const secondSlug = getChannelSlugFromUrl(page);
|
||||
expect(secondSlug).not.toBe(firstSlug);
|
||||
await channelsPage.centerView.header.toHaveTitle(member3.username);
|
||||
},
|
||||
);
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {getChannelSlugFromUrl} from './helpers';
|
||||
|
||||
/**
|
||||
* @objective Verify that a closed group message can be reopened via a saved message and via the Direct Messages modal.
|
||||
*
|
||||
* MM-T477 and MM-T479 are duplicates of MM-T476 and are covered by this test.
|
||||
*/
|
||||
test(
|
||||
'MM-T476 MM-T477 MM-T479 closes and reopens a group message via saved messages and the DM modal',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus two more users on the team
|
||||
const {user, team, adminClient, userClient} = await pw.initSetup();
|
||||
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'gm');
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a group message and post a message, then save that message
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(member1);
|
||||
await dmModal.selectUser(member2);
|
||||
await dmModal.goToChannel();
|
||||
const slug = getChannelSlugFromUrl(page);
|
||||
|
||||
const token = `message to save ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(token);
|
||||
const savedPost = await channelsPage.getLastPost();
|
||||
const savedPostId = await savedPost.getId();
|
||||
await userClient.savePreferences(user.id, [
|
||||
{user_id: user.id, category: 'flagged_post', name: savedPostId, value: 'true'},
|
||||
]);
|
||||
|
||||
// # Close the group message conversation
|
||||
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
|
||||
|
||||
// # Reopen the group message by jumping to the saved message
|
||||
await channelsPage.globalHeader.openSavedMessages();
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
|
||||
|
||||
// * Verify the group message channel is reopened
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
|
||||
|
||||
// # Close it again and reopen it via the Direct Messages modal
|
||||
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
|
||||
const dmModal2 = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal2.selectUser(member1);
|
||||
await dmModal2.selectUser(member2);
|
||||
await dmModal2.goToChannel();
|
||||
|
||||
// * Verify the group message channel is reopened again
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
|
||||
},
|
||||
);
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {getChannelSlugFromUrl} from './helpers';
|
||||
|
||||
/**
|
||||
* @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 that a group message lists its members and that adding another member creates a new group message.
|
||||
*/
|
||||
test(
|
||||
'MM-T467 adds a user to a group message to create a new group message',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus three more users on the team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [member1, member2, member3] = await adminClient.createUsers(team.id, 3, 'gm');
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a group message with two members
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(member1);
|
||||
await dmModal.selectUser(member2);
|
||||
await dmModal.goToChannel();
|
||||
const firstSlug = getChannelSlugFromUrl(page);
|
||||
|
||||
// * Verify the group message lists both members in the header
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
await channelsPage.centerView.header.toHaveTitle(member2.username);
|
||||
|
||||
// # Create a group message that adds a third member
|
||||
const dmModal2 = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal2.selectUser(member1);
|
||||
await dmModal2.selectUser(member2);
|
||||
await dmModal2.selectUser(member3);
|
||||
await dmModal2.goToChannel();
|
||||
|
||||
// * Verify a new, different group message channel is created that includes the added member
|
||||
const secondSlug = getChannelSlugFromUrl(page);
|
||||
expect(secondSlug).not.toBe(firstSlug);
|
||||
await channelsPage.centerView.header.toHaveTitle(member3.username);
|
||||
},
|
||||
);
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {duration, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {getChannelSlugFromUrl} from './helpers';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that a closed group message can be reopened via a saved message and via the Direct Messages modal.
|
||||
*
|
||||
* MM-T477 and MM-T479 are duplicates of MM-T476 and are covered by this test.
|
||||
*/
|
||||
test(
|
||||
'MM-T476 MM-T477 MM-T479 closes and reopens a group message via saved messages and the DM modal',
|
||||
{tag: '@direct_messages'},
|
||||
async ({pw}) => {
|
||||
// # Create the test user plus two more users on the team
|
||||
const {user, team, adminClient, userClient} = await pw.initSetup();
|
||||
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'gm');
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Create a group message and post a message, then save that message
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(member1);
|
||||
await dmModal.selectUser(member2);
|
||||
await dmModal.goToChannel();
|
||||
const slug = getChannelSlugFromUrl(page);
|
||||
|
||||
const token = `message to save ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(token);
|
||||
const savedPost = await channelsPage.getLastPost();
|
||||
const savedPostId = await savedPost.getId();
|
||||
await userClient.savePreferences(user.id, [
|
||||
{user_id: user.id, category: 'flagged_post', name: savedPostId, value: 'true'},
|
||||
]);
|
||||
|
||||
// # Close the group message conversation
|
||||
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
|
||||
|
||||
// # Reopen the group message by jumping to the saved message
|
||||
await channelsPage.globalHeader.openSavedMessages();
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
|
||||
|
||||
// * Verify the group message channel is reopened
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
|
||||
|
||||
// # Close it again and reopen it via the Direct Messages modal
|
||||
await channelsPage.sidebarLeft.closeConversationAndWait(slug);
|
||||
const dmModal2 = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal2.selectUser(member1);
|
||||
await dmModal2.selectUser(member2);
|
||||
await dmModal2.goToChannel();
|
||||
|
||||
// * Verify the group message channel is reopened again
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/${slug}`);
|
||||
},
|
||||
);
|
||||
+28
@@ -5,6 +5,34 @@ import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {getChannelSlugFromUrl} from './helpers';
|
||||
|
||||
/**
|
||||
* @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 that a group message can be created with a mention, closed, and then recreated with the same members.
|
||||
*
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
// 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);
|
||||
});
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that mentioning a user inside a different pair of users' direct message does not notify the mentioned user.
|
||||
*/
|
||||
test('MM-T448 does not notify a user mentioned in a different DM', {tag: '@direct_messages'}, async ({pw}) => {
|
||||
// # Create the test user plus two more users on the team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [partner, mentioned] = await adminClient.createUsers(team.id, 2, 'dmuser');
|
||||
|
||||
const token = `dmmention${pw.random.id()}`;
|
||||
|
||||
// # As the test user, open a DM with the partner and mention the third user there
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const dmModal = await channelsPage.openDirectChannelsModal();
|
||||
await dmModal.selectUser(partner);
|
||||
await dmModal.goToChannel();
|
||||
await channelsPage.postMessage(`@${mentioned.username} ${token}`);
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(token);
|
||||
|
||||
// # Log in as the mentioned user and open Recent Mentions
|
||||
const {channelsPage: mentionedPage} = await pw.testBrowser.login(mentioned);
|
||||
await mentionedPage.goto(team.name, 'town-square');
|
||||
await mentionedPage.toBeVisible();
|
||||
await mentionedPage.globalHeader.openRecentMentions();
|
||||
|
||||
// * Verify the mentioned user did not receive the mention from the other users' DM
|
||||
await mentionedPage.searchResultsPanel.toBeVisible();
|
||||
await expect(mentionedPage.searchResultsPanel.getResultByText(token)).toHaveCount(0);
|
||||
});
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a direct message can be opened from a user's profile popover on their post.
|
||||
*/
|
||||
test('MM-T455 opens a direct message from a profile popover', {tag: '@direct_messages'}, async ({pw}) => {
|
||||
// # Create the test user plus another user who posts in a shared channel
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const [author] = await adminClient.createUsers(team.id, 1, 'author');
|
||||
const offTopic = await adminClient.getChannelByName(team.id, 'off-topic');
|
||||
await adminClient.addToChannel(author.id, offTopic.id);
|
||||
|
||||
const token = `popover dm ${pw.random.id()}`;
|
||||
const {client: authorClient} = await pw.makeClient(author);
|
||||
await authorClient.createPost({channel_id: offTopic.id, message: token});
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the author's profile popover from their post and click Message
|
||||
const post = await channelsPage.getLastPost();
|
||||
const popover = await channelsPage.openProfilePopover(post);
|
||||
await popover.message();
|
||||
|
||||
// * Verify a direct message channel with the author is opened
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/messages/@${author.username}`);
|
||||
await channelsPage.centerView.header.toHaveTitle(author.username);
|
||||
|
||||
// # Post a message in the direct message channel
|
||||
const dmMessage = `direct message ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(dmMessage);
|
||||
|
||||
// * Verify the direct message is posted
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText(dmMessage);
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that while the emoji picker is open over a post, scrolling the center channel with
|
||||
* PageUp is disabled (the posts stay in place).
|
||||
*/
|
||||
test('MM-T2365 disables center channel scrolling while the emoji picker is open', {tag: '@messaging'}, async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Scrolling ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
// # Fill the channel with enough posts to make it scrollable
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message: `filler ${i}`});
|
||||
}
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open the emoji (reaction) picker over it
|
||||
const message = `react target ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(message);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.openReactionPicker();
|
||||
await expect(channelsPage.reactionEmojiPicker.container).toBeVisible();
|
||||
|
||||
// # Record the target post position, then press PageUp
|
||||
const before = await post.container.boundingBox();
|
||||
await page.keyboard.press('PageUp');
|
||||
await pw.wait(pw.duration.one_sec);
|
||||
const after = await post.container.boundingBox();
|
||||
|
||||
// * Verify the post did not move (the channel did not scroll while the picker was open)
|
||||
if (!before || !after) {
|
||||
throw new Error('Expected the post to have a bounding box while the emoji picker is open');
|
||||
}
|
||||
expect(Math.abs(after.y - before.y)).toBeLessThan(5);
|
||||
|
||||
// # Close the emoji picker and press PageUp again
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(channelsPage.reactionEmojiPicker.container).not.toBeVisible();
|
||||
await page.keyboard.press('PageUp');
|
||||
await pw.wait(pw.duration.one_sec);
|
||||
|
||||
// * Verify scrolling works again once the picker is closed (control: the post now moves)
|
||||
const afterClose = await post.container.boundingBox();
|
||||
if (!afterClose) {
|
||||
throw new Error('Expected the post to have a bounding box after the emoji picker is closed');
|
||||
}
|
||||
expect(Math.abs(afterClose.y - before.y)).toBeGreaterThan(5);
|
||||
});
|
||||
+23
@@ -39,3 +39,26 @@ test('MM-T1274 completes emoji autocomplete with Tab', {tag: '@keyboard_shortcut
|
||||
// Current behavior inserts the rendered emoji glyph rather than the `:tomato:` shortcode.
|
||||
expect(await postCreate.getInputValue()).toContain('🍅');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that selecting "Keyboard shortcuts" from the Help menu opens the Keyboard Shortcuts modal.
|
||||
*/
|
||||
test('MM-T1279 opens keyboard shortcuts modal from the Help menu', {tag: '@keyboard_shortcuts'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Help menu and click "Keyboard shortcuts"
|
||||
await channelsPage.globalHeader.openKeyboardShortcuts();
|
||||
|
||||
// * Verify the Keyboard Shortcuts modal is displayed
|
||||
await expect(channelsPage.keyboardShortcutsModal).toBeVisible();
|
||||
|
||||
// # Close the modal with the Escape key
|
||||
await channelsPage.page.keyboard.press('Escape');
|
||||
|
||||
// * Verify the Keyboard Shortcuts modal is no longer displayed
|
||||
await expect(channelsPage.keyboardShortcutsModal).not.toBeVisible();
|
||||
});
|
||||
+53
@@ -3,6 +3,59 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify the Ctrl/Cmd+K quick switcher can find and open a direct message, a group message,
|
||||
* and a public channel.
|
||||
*/
|
||||
test(
|
||||
'MM-T1247 finds and opens a direct message, group message, and channel with Ctrl/Cmd+K',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'switch');
|
||||
|
||||
// # Create a direct message, a group message, and a public channel for the user
|
||||
const dm = await adminClient.createDirectChannel([user.id, member1.id]);
|
||||
await adminClient.createPost({channel_id: dm.id, user_id: member1.id, message: 'dm hello'});
|
||||
const gm = await adminClient.createGroupChannel([user.id, member1.id, member2.id]);
|
||||
await adminClient.createPost({channel_id: gm.id, user_id: member2.id, message: 'gm hello'});
|
||||
const publicChannel = await adminClient.createPublicChannel(team.id, `Switcher ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, publicChannel.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {findChannelsModal} = channelsPage;
|
||||
const options = () => findChannelsModal.container.getByRole('option');
|
||||
|
||||
// # Open the quick switcher and open the direct message with member1 (the option with only member1)
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(member1.username);
|
||||
await options().filter({hasText: member1.username, hasNotText: member2.username}).first().click();
|
||||
|
||||
// * Verify the direct message with member1 is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
|
||||
// # Open the quick switcher and open the group message (the option containing both members)
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(member2.username);
|
||||
await options().filter({hasText: member1.username}).filter({hasText: member2.username}).first().click();
|
||||
|
||||
// * Verify the group message (containing both members) is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
await channelsPage.centerView.header.toHaveTitle(member2.username);
|
||||
|
||||
// # Open the quick switcher and open the public channel
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(publicChannel.display_name);
|
||||
await options().filter({hasText: publicChannel.display_name}).first().click();
|
||||
|
||||
// * Verify the public channel is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(publicChannel.display_name);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that pressing Shift+Up in the textbox in center channel opens the thread for the last post in RHS
|
||||
* and correctly focuses the reply textbox, even when there are large messages with attachments from other users.
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify the Ctrl/Cmd+K quick switcher can find and open a direct message, a group message,
|
||||
* and a public channel.
|
||||
*/
|
||||
test(
|
||||
'MM-T1247 finds and opens a direct message, group message, and channel with Ctrl/Cmd+K',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
const [member1, member2] = await adminClient.createUsers(team.id, 2, 'switch');
|
||||
|
||||
// # Create a direct message, a group message, and a public channel for the user
|
||||
const dm = await adminClient.createDirectChannel([user.id, member1.id]);
|
||||
await adminClient.createPost({channel_id: dm.id, user_id: member1.id, message: 'dm hello'});
|
||||
const gm = await adminClient.createGroupChannel([user.id, member1.id, member2.id]);
|
||||
await adminClient.createPost({channel_id: gm.id, user_id: member2.id, message: 'gm hello'});
|
||||
const publicChannel = await adminClient.createPublicChannel(team.id, `Switcher ${pw.random.id()}`);
|
||||
await adminClient.addToChannel(user.id, publicChannel.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {findChannelsModal} = channelsPage;
|
||||
const options = () => findChannelsModal.container.getByRole('option');
|
||||
|
||||
// # Open the quick switcher and open the direct message with member1 (the option with only member1)
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(member1.username);
|
||||
await options().filter({hasText: member1.username, hasNotText: member2.username}).first().click();
|
||||
|
||||
// * Verify the direct message with member1 is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
|
||||
// # Open the quick switcher and open the group message (the option containing both members)
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(member2.username);
|
||||
await options().filter({hasText: member1.username}).filter({hasText: member2.username}).first().click();
|
||||
|
||||
// * Verify the group message (containing both members) is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(member1.username);
|
||||
await channelsPage.centerView.header.toHaveTitle(member2.username);
|
||||
|
||||
// # Open the quick switcher and open the public channel
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
await findChannelsModal.input.fill(publicChannel.display_name);
|
||||
await options().filter({hasText: publicChannel.display_name}).first().click();
|
||||
|
||||
// * Verify the public channel is opened
|
||||
await channelsPage.centerView.header.toHaveTitle(publicChannel.display_name);
|
||||
},
|
||||
);
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that selecting "Keyboard shortcuts" from the Help menu opens the Keyboard Shortcuts modal.
|
||||
*/
|
||||
test('MM-T1279 opens keyboard shortcuts modal from the Help menu', {tag: '@keyboard_shortcuts'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Help menu and click "Keyboard shortcuts"
|
||||
await channelsPage.globalHeader.openKeyboardShortcuts();
|
||||
|
||||
// * Verify the Keyboard Shortcuts modal is displayed
|
||||
await expect(channelsPage.keyboardShortcutsModal).toBeVisible();
|
||||
|
||||
// # Close the modal with the Escape key
|
||||
await channelsPage.page.keyboard.press('Escape');
|
||||
|
||||
// * Verify the Keyboard Shortcuts modal is no longer displayed
|
||||
await expect(channelsPage.keyboardShortcutsModal).not.toBeVisible();
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify pressing Up arrow in an empty message box opens the edit box for the last post in both
|
||||
* the center channel and a thread, and that Escape closes the edit box without saving.
|
||||
*
|
||||
* MM-T1263 covers the same behavior as MM-T1262 and is covered by this test.
|
||||
*/
|
||||
test(
|
||||
'MM-T1262 MM-T1263 opens and cancels the edit box with Up arrow and Escape',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and press Up arrow from the empty center message box
|
||||
const message = `edit shortcut ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(message);
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
|
||||
// * Verify the edit box opens with the last message
|
||||
await channelsPage.centerView.postEdit.toBeVisible();
|
||||
await expect(channelsPage.centerView.postEdit.input).toHaveValue(message);
|
||||
|
||||
// # Close the edit box with Escape
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// * Verify the edit box closes and the post remains unchanged
|
||||
await channelsPage.centerView.postEdit.toNotBeVisible();
|
||||
await channelsPage.centerView.waitUntilLastPostContains(message);
|
||||
|
||||
// # Open a thread on the last post and post a reply
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
const reply = `reply shortcut ${pw.random.id()}`;
|
||||
await channelsPage.sidebarRight.postMessage(reply);
|
||||
await channelsPage.sidebarRight.toContainText(reply);
|
||||
|
||||
// # Press Up arrow from the empty thread reply box
|
||||
await channelsPage.sidebarRight.postCreate.input.focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
|
||||
// * Verify the thread edit box opens with the reply, then closes with Escape
|
||||
await channelsPage.sidebarRight.postEdit.toBeVisible();
|
||||
await expect(channelsPage.sidebarRight.postEdit.input).toHaveValue(reply);
|
||||
await page.keyboard.press('Escape');
|
||||
await channelsPage.sidebarRight.postEdit.toNotBeVisible();
|
||||
|
||||
// * Verify the reply is still present after cancelling the edit
|
||||
await channelsPage.sidebarRight.toContainText(reply);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify Shift+Enter adds new lines in the message box to compose a fenced code block, which is
|
||||
* then posted as a code block.
|
||||
*
|
||||
* MM-T1268 covers the same behavior as MM-T1267 and is covered by this test.
|
||||
*/
|
||||
test(
|
||||
'MM-T1267 MM-T1268 composes and posts a code block using Shift+Enter for new lines',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Compose a fenced code block using Shift+Enter for new lines
|
||||
const code = `code ${pw.random.id()}`;
|
||||
const input = channelsPage.centerView.postCreate.input;
|
||||
await input.focus();
|
||||
await input.pressSequentially('```');
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
await input.pressSequentially(code);
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
await input.pressSequentially('```');
|
||||
|
||||
// * Verify Shift+Enter produced the multi-line fenced code block in the message box
|
||||
await expect(input).toHaveValue(`\`\`\`\n${code}\n\`\`\``);
|
||||
|
||||
// # Post the code block (Enter inserts a newline inside a fenced block, so use the send button)
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
|
||||
// * Verify the code block is posted with the code content and the fence markers consumed (rendered as a code block, not literal backticks)
|
||||
await channelsPage.centerView.waitUntilLastPostContains(code);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.toContainText(code);
|
||||
await post.toNotContainText('```');
|
||||
},
|
||||
);
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// 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 for moving to the previous and next channel in the sidebar
|
||||
* switch between channels.
|
||||
*/
|
||||
test(
|
||||
'MM-T1259 moves to the previous and next channel with the keyboard',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.centerView.header.toHaveTitle('Off-Topic');
|
||||
|
||||
// # Move to the next channel with the keyboard
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('Alt+ArrowDown');
|
||||
|
||||
// * Verify the next channel in the sidebar is shown
|
||||
await channelsPage.centerView.header.toHaveTitle('Town Square');
|
||||
|
||||
// # Move to the previous channel with the keyboard
|
||||
await page.keyboard.press('Alt+ArrowUp');
|
||||
|
||||
// * Verify the original channel is shown again
|
||||
await channelsPage.centerView.header.toHaveTitle('Off-Topic');
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify Ctrl/Cmd+Shift+K opens the Direct Messages modal, and Escape closes it.
|
||||
*/
|
||||
test(
|
||||
'MM-T1276 opens the Direct Messages modal with the keyboard shortcut',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Focus the message box and press the Direct Messages shortcut
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ControlOrMeta+Shift+K');
|
||||
|
||||
// * Verify the Direct Messages modal opens
|
||||
await channelsPage.directChannelsModal.toBeVisible();
|
||||
|
||||
// # Close the modal with Escape
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// * Verify the Direct Messages modal closes
|
||||
await expect(channelsPage.directChannelsModal.container).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify Ctrl/Cmd+Shift+F opens the search box prefilled with the current channel filter.
|
||||
*
|
||||
* MM-T4872 covers the same behavior as MM-T1435 and is covered by this test.
|
||||
*/
|
||||
test(
|
||||
'MM-T1435 MM-T4872 prefills the search box with the channel filter using the keyboard shortcut',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Focus the message box and press the in-channel search shortcut
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.press('ControlOrMeta+Shift+F');
|
||||
|
||||
// * Verify the search box opens prefilled with the current channel filter
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await expect(channelsPage.searchBox.searchInput).toHaveValue('in:off-topic ');
|
||||
},
|
||||
);
|
||||
-61
@@ -64,64 +64,3 @@ test('MM-T1242 CTRL/CMD+K typed characters are not lost after switching channels
|
||||
// * 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.
|
||||
*
|
||||
* MM-T1256 and MM-T1257 cover the same behavior as MM-T1254 and are covered by this test.
|
||||
*/
|
||||
test('MM-T1254 MM-T1256 MM-T1257 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');
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify Ctrl/Cmd+Up and Ctrl/Cmd+Down cycle through previous messages in the post textbox.
|
||||
*
|
||||
* MM-T1256, MM-T1257, and MM-T1258 cover the same behavior as MM-T1254 and are covered by this test.
|
||||
*/
|
||||
test('MM-T1254 MM-T1256 MM-T1257 MM-T1258 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');
|
||||
});
|
||||
-103
@@ -99,109 +99,6 @@ test('MM-T248 Mark Direct Message post as Unread', async ({pw}) => {
|
||||
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)
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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 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']});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify selecting an emoji from the picker inserts it at the current caret position.
|
||||
*/
|
||||
test('MM-T95 Selecting an emoji from emoji picker should insert it at the cursor position', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Log in and place the caret between "Hello" and "World"
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.centerView.postCreate.writeMessage('HelloWorld!');
|
||||
for (let i = 0; i < 'World!'.length; i++) {
|
||||
await channelsPage.centerView.postCreate.input.press('ArrowLeft');
|
||||
}
|
||||
|
||||
// # Select the grinning emoji from the picker
|
||||
await channelsPage.centerView.postCreate.openEmojiPicker();
|
||||
await channelsPage.emojiGifPickerPopup.toBeVisible();
|
||||
await channelsPage.emojiGifPickerPopup.clickEmoji('grinning');
|
||||
|
||||
// * Verify the emoji was inserted at the caret and can be posted
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveValue('Hello 😀 World!');
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toContainText('Hello 😀 World!');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify an emoji followed by punctuation renders as an emoji without separating the punctuation.
|
||||
*/
|
||||
test('MM-T222 Emoji characters followed by punctuation', async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
|
||||
// # Post an emoticon followed by punctuation
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.postMessage(':)=');
|
||||
|
||||
// * Verify the emoticon renders and the punctuation remains immediately adjacent, with no stray
|
||||
// whitespace inserted between the rendered emoji and the punctuation
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.emoticon.first()).toHaveAttribute('aria-label', ':slightly_smiling_face:');
|
||||
await expect(lastPost.messageText.first()).toHaveText(':)=');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that a leading colon is ignored when searching in the emoji picker.
|
||||
*/
|
||||
test('MM-T156 ignores a leading colon when searching the emoji picker', {tag: '@messaging'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open the emoji reaction picker on it
|
||||
await channelsPage.postMessage(`emoji picker search ${pw.random.id()}`);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.openReactionPicker();
|
||||
|
||||
// * Verify the emoji picker is open
|
||||
await channelsPage.reactionEmojiPicker.toBeVisible();
|
||||
|
||||
// # Search using a leading colon
|
||||
await channelsPage.reactionEmojiPicker.searchEmoji(':tax');
|
||||
|
||||
// * Verify the leading colon is ignored and the taxi emoji is returned
|
||||
await expect(channelsPage.reactionEmojiPicker.getEmoji('taxi')).toBeVisible();
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a leading colon is ignored when searching in the emoji picker.
|
||||
*/
|
||||
test('MM-T156 ignores a leading colon when searching the emoji picker', {tag: '@messaging'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open the emoji reaction picker on it
|
||||
await channelsPage.postMessage(`emoji picker search ${pw.random.id()}`);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.openReactionPicker();
|
||||
|
||||
// * Verify the emoji picker is open
|
||||
await channelsPage.reactionEmojiPicker.toBeVisible();
|
||||
|
||||
// # Search using a leading colon
|
||||
await channelsPage.reactionEmojiPicker.searchEmoji(':tax');
|
||||
|
||||
// * Verify the leading colon is ignored and the taxi emoji is returned
|
||||
await expect(channelsPage.reactionEmojiPicker.getEmoji('taxi')).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
async function emojiHeight(post: {emoticon: {boundingBox: () => Promise<{height: number} | null>}}): Promise<number> {
|
||||
const box = await post.emoticon.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error('Expected the emoji to have a bounding box');
|
||||
}
|
||||
return box.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @objective Verify an emoji-only message renders as a jumbo (large) emoji, and that leading/trailing
|
||||
* whitespace around the emoji does not stop it from rendering jumbo.
|
||||
*/
|
||||
test(
|
||||
'MM-T2179 renders emoji-only messages as jumbo even when surrounded by whitespace',
|
||||
{tag: '@messaging'},
|
||||
async ({pw}) => {
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post an emoji inline with text (renders at the normal inline size)
|
||||
await channelsPage.postMessage('jumbo test :taco:');
|
||||
const inlineHeight = await emojiHeight(await channelsPage.getLastPost());
|
||||
|
||||
// # Post the same emoji on its own surrounded by whitespace
|
||||
await channelsPage.postMessage(' :taco: ');
|
||||
const jumboHeight = await emojiHeight(await channelsPage.getLastPost());
|
||||
|
||||
// * Verify the whitespace-surrounded emoji-only message renders larger (jumbo) than the inline emoji
|
||||
expect(jumboHeight).toBeGreaterThan(inlineHeight);
|
||||
},
|
||||
);
|
||||
-46
@@ -51,33 +51,6 @@ test('MM-T94 RHS fetches messages on reconnect while a different channel is in c
|
||||
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.
|
||||
*/
|
||||
@@ -100,22 +73,3 @@ test('MM-T175 Channel short-linking still works when placed in brackets', async
|
||||
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(':)=');
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that clicking a post timestamp in the RHS or center channel highlights the post in the center channel.
|
||||
*/
|
||||
test('MM-T176 highlights the center post when its timestamp is clicked', {tag: '@messaging'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread, then reply in the RHS
|
||||
await channelsPage.postMessage(`timestamp highlight ${pw.random.id()}`);
|
||||
const rootPost = await channelsPage.getLastPost();
|
||||
const rootId = await rootPost.getId();
|
||||
await rootPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage('Reply to test timestamp click');
|
||||
|
||||
// # Click the root post timestamp in the RHS
|
||||
await page.locator(`#RHS_ROOT_time_${rootId}`).click();
|
||||
|
||||
// * Verify the matching post is highlighted in the center channel
|
||||
const centerPost = await channelsPage.centerView.getPostById(rootId);
|
||||
await expect(centerPost.container).toHaveClass(/post--highlight/);
|
||||
|
||||
// # Close the RHS and wait for the highlight to fade
|
||||
await channelsPage.sidebarRight.close();
|
||||
await expect(centerPost.container).not.toHaveClass(/post--highlight/);
|
||||
|
||||
// # Click the same post timestamp in the center channel
|
||||
await page.locator(`#CENTER_time_${rootId}`).click();
|
||||
|
||||
// * Verify the post is highlighted again in the center channel
|
||||
await expect(centerPost.container).toHaveClass(/post--highlight/);
|
||||
});
|
||||
+36
@@ -3,6 +3,42 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that clicking a post timestamp in the RHS or center channel highlights the post in the center channel.
|
||||
*/
|
||||
test('MM-T176 highlights the center post when its timestamp is clicked', {tag: '@messaging'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread, then reply in the RHS
|
||||
await channelsPage.postMessage(`timestamp highlight ${pw.random.id()}`);
|
||||
const rootPost = await channelsPage.getLastPost();
|
||||
const rootId = await rootPost.getId();
|
||||
await rootPost.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage('Reply to test timestamp click');
|
||||
|
||||
// # Click the root post timestamp in the RHS
|
||||
await page.locator(`#RHS_ROOT_time_${rootId}`).click();
|
||||
|
||||
// * Verify the matching post is highlighted in the center channel
|
||||
const centerPost = await channelsPage.centerView.getPostById(rootId);
|
||||
await expect(centerPost.container).toHaveClass(/post--highlight/);
|
||||
|
||||
// # Close the RHS and wait for the highlight to fade
|
||||
await channelsPage.sidebarRight.close();
|
||||
await expect(centerPost.container).not.toHaveClass(/post--highlight/);
|
||||
|
||||
// # Click the same post timestamp in the center channel
|
||||
await page.locator(`#CENTER_time_${rootId}`).click();
|
||||
|
||||
// * Verify the post is highlighted again in the center channel
|
||||
await expect(centerPost.container).toHaveClass(/post--highlight/);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that following a permalink to the first post in a channel loads the channel start without an endless loading indicator above it.
|
||||
*/
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user who belongs to multiple teams can switch between them and log out to the sign-in page.
|
||||
*
|
||||
* MM-T432 is a duplicate of MM-T430 and is covered by this test.
|
||||
*/
|
||||
test('MM-T430 MM-T432 switches between teams and logs out', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create a user in one team, then add them to a second team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const secondTeam = await pw.createNewTeam(adminClient, {
|
||||
name: 'team',
|
||||
displayName: 'Second Team',
|
||||
type: 'O',
|
||||
unique: true,
|
||||
});
|
||||
await adminClient.addToTeam(secondTeam.id, user.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Switch to the second team
|
||||
await channelsPage.switchToTeam(secondTeam.name);
|
||||
|
||||
// * Verify the second team is displayed
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/${secondTeam.name}/`);
|
||||
|
||||
// # Switch back to the first team
|
||||
await channelsPage.switchToTeam(team.name);
|
||||
|
||||
// * Verify the first team is displayed
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/${team.name}/`);
|
||||
|
||||
// # Log out
|
||||
await channelsPage.logout();
|
||||
|
||||
// * Verify the user is returned to the sign-in page
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain('/login');
|
||||
});
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user who belongs to multiple teams can switch between them and log out to the sign-in page.
|
||||
*
|
||||
* MM-T432 is a duplicate of MM-T430 and is covered by this test.
|
||||
*/
|
||||
test('MM-T430 MM-T432 switches between teams and logs out', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create a user in one team, then add them to a second team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const secondTeam = await pw.createNewTeam(adminClient, {
|
||||
name: 'team',
|
||||
displayName: 'Second Team',
|
||||
type: 'O',
|
||||
unique: true,
|
||||
});
|
||||
await adminClient.addToTeam(secondTeam.id, user.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Switch to the second team
|
||||
await channelsPage.switchToTeam(secondTeam.name);
|
||||
|
||||
// * Verify the second team is displayed
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/${secondTeam.name}/`);
|
||||
|
||||
// # Switch back to the first team
|
||||
await channelsPage.switchToTeam(team.name);
|
||||
|
||||
// * Verify the first team is displayed
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/${team.name}/`);
|
||||
|
||||
// # Log out
|
||||
await channelsPage.logout();
|
||||
|
||||
// * Verify the user is returned to the sign-in page
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain('/login');
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that reopening the app returns the user to the previously viewed team and channel.
|
||||
*/
|
||||
test('MM-T431 reopens to the previously viewed team and channel', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create a user in one team, then add them to a second team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const secondTeam = await pw.createNewTeam(adminClient, {
|
||||
name: 'team',
|
||||
displayName: 'Second Team',
|
||||
type: 'O',
|
||||
unique: true,
|
||||
});
|
||||
await adminClient.addToTeam(secondTeam.id, user.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Switch to the second team and view its Off-Topic channel
|
||||
await channelsPage.switchToTeam(secondTeam.name);
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
await expect
|
||||
.poll(() => page.url(), {timeout: pw.duration.ten_sec})
|
||||
.toContain(`/${secondTeam.name}/channels/off-topic`);
|
||||
|
||||
// # Reopen the app by navigating to the root URL
|
||||
await page.goto('/');
|
||||
|
||||
// * Verify the app returns to the previously viewed team and channel
|
||||
await channelsPage.toBeVisible();
|
||||
await expect
|
||||
.poll(() => page.url(), {timeout: pw.duration.ten_sec})
|
||||
.toContain(`/${secondTeam.name}/channels/off-topic`);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that the right-hand-side reply panel can be expanded to overlay the center channel and collapsed back while staying open.
|
||||
*/
|
||||
test('MM-T440 expands and collapses the RHS reply panel', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread in the RHS
|
||||
await channelsPage.postMessage(`rhs expand ${pw.random.id()}`);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the RHS starts collapsed with an expand control
|
||||
await expect(channelsPage.sidebarRight.expandButton).toBeVisible();
|
||||
|
||||
// # Expand the RHS
|
||||
await channelsPage.sidebarRight.expand();
|
||||
|
||||
// * Verify the RHS is expanded (a collapse control is now shown)
|
||||
await expect(channelsPage.sidebarRight.collapseButton).toBeVisible();
|
||||
|
||||
// # Collapse the RHS again
|
||||
await channelsPage.sidebarRight.collapse();
|
||||
|
||||
// * Verify the RHS collapses but stays open, and the center channel is visible again
|
||||
await expect(channelsPage.sidebarRight.expandButton).toBeVisible();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await expect(channelsPage.centerView.postCreate.input).toBeVisible();
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that reopening the app returns the user to the previously viewed team and channel.
|
||||
*/
|
||||
test('MM-T431 reopens to the previously viewed team and channel', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create a user in one team, then add them to a second team
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const secondTeam = await pw.createNewTeam(adminClient, {
|
||||
name: 'team',
|
||||
displayName: 'Second Team',
|
||||
type: 'O',
|
||||
unique: true,
|
||||
});
|
||||
await adminClient.addToTeam(secondTeam.id, user.id);
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Switch to the second team and view its Off-Topic channel
|
||||
await channelsPage.switchToTeam(secondTeam.name);
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
await expect
|
||||
.poll(() => page.url(), {timeout: pw.duration.ten_sec})
|
||||
.toContain(`/${secondTeam.name}/channels/off-topic`);
|
||||
|
||||
// # Reopen the app by navigating to the root URL
|
||||
await page.goto('/');
|
||||
|
||||
// * Verify the app returns to the previously viewed team and channel
|
||||
await channelsPage.toBeVisible();
|
||||
await expect
|
||||
.poll(() => page.url(), {timeout: pw.duration.ten_sec})
|
||||
.toContain(`/${secondTeam.name}/channels/off-topic`);
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the right-hand-side reply panel can be expanded to overlay the center channel and collapsed back while staying open.
|
||||
*/
|
||||
test('MM-T440 expands and collapses the RHS reply panel', {tag: '@multi_team'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread in the RHS
|
||||
await channelsPage.postMessage(`rhs expand ${pw.random.id()}`);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the RHS starts collapsed with an expand control
|
||||
await expect(channelsPage.sidebarRight.expandButton).toBeVisible();
|
||||
|
||||
// # Expand the RHS
|
||||
await channelsPage.sidebarRight.expand();
|
||||
|
||||
// * Verify the RHS is expanded (a collapse control is now shown)
|
||||
await expect(channelsPage.sidebarRight.collapseButton).toBeVisible();
|
||||
|
||||
// # Collapse the RHS again
|
||||
await channelsPage.sidebarRight.collapse();
|
||||
|
||||
// * Verify the RHS collapses but stays open, and the center channel is visible again
|
||||
await expect(channelsPage.sidebarRight.expandButton).toBeVisible();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await expect(channelsPage.centerView.postCreate.input).toBeVisible();
|
||||
});
|
||||
+65
@@ -8,6 +8,71 @@ const NOTIFY_ALL_THRESHOLD = 5;
|
||||
|
||||
const NOTIFY_ALL_TITLE = 'Confirm sending notifications to entire channel';
|
||||
|
||||
/**
|
||||
* @objective Verify that channel-wide mentions with uppercase letters trigger notifications and are properly highlighted.
|
||||
*
|
||||
* @precondition
|
||||
* - Two users are members of the same team
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T483 triggers notification with uppercase channel-wide mention and highlights message for all users',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup and get the required users and team
|
||||
const {team, adminUser, user} = await pw.initSetup();
|
||||
|
||||
// # Log in as the admin in one browser session and navigate to the "town-square" channel
|
||||
const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
|
||||
// # Stub the Notification in the admin's browser to capture notifications
|
||||
await pw.stubNotification(adminPage, 'granted');
|
||||
|
||||
// # Log in as the regular user in a separate browser and navigate to the "off-topic" channel
|
||||
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(user);
|
||||
await otherChannelsPage.goto(team.name, 'off-topic');
|
||||
await otherChannelsPage.toBeVisible();
|
||||
|
||||
// # Post a channel-wide mention message "@ALL" in uppercase from the user's browser
|
||||
const message = `@ALL good morning, ${team.name}!`;
|
||||
await otherChannelsPage.postMessage(message);
|
||||
|
||||
// * Verify notification is received in the admin's browser with correct content
|
||||
const notifications = await pw.waitForNotification(adminPage);
|
||||
expect(notifications.length).toBe(1);
|
||||
|
||||
const notification = notifications[0];
|
||||
expect(notification.title).toBe('Off-Topic');
|
||||
expect(notification.body).toBe(`@${user.username}: ${message}`);
|
||||
|
||||
// The Notifications API tag must not leak the message body; the app uses the
|
||||
// opaque post ID as the tag so notifications coalesce per conversation.
|
||||
const otherLastPost = await otherChannelsPage.getLastPost();
|
||||
expect(notification.tag).toBe(await otherLastPost.getId());
|
||||
expect(notification.tag).not.toContain(message);
|
||||
|
||||
expect(notification.icon).toContain('.png');
|
||||
expect(notification.requireInteraction).toBe(false);
|
||||
expect(notification.silent).toBe(false);
|
||||
|
||||
// * Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
|
||||
await otherLastPost.toContainText(message);
|
||||
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
|
||||
// # Navigate admin to the "off-topic" channel
|
||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||
|
||||
// * Verify the message is posted and highlighted correctly for the admin user
|
||||
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||
await adminLastPost.toContainText(message);
|
||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a channel-wide mention typed in mixed case (@channEL) is recognized as a mention,
|
||||
* prompts the notify-all confirmation, and is posted with its original casing preserved.
|
||||
+54
@@ -103,3 +103,57 @@ test(
|
||||
await channelsPage.centerView.waitUntilLastPostContains(message);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user in "Do Not Disturb" does not receive desktop notifications, and receives them
|
||||
* again once the status is cleared.
|
||||
*
|
||||
* @precondition
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T495 suppresses desktop notifications while Do Not Disturb is enabled',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a direct message channel between the admin and the user
|
||||
const dmChannel = await adminClient.createDirectChannel([adminUser.id, user.id]);
|
||||
|
||||
// # Log in and view Town Square (not the DM), then stub notifications
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
// # Send a direct message from the admin while the user is available
|
||||
const firstMessage = `DND baseline ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: firstMessage});
|
||||
|
||||
// * Verify the direct message triggers a desktop notification
|
||||
const notifications = await pw.waitForNotification(page);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].body).toContain(firstMessage);
|
||||
|
||||
// # Enable Do Not Disturb via the slash command
|
||||
await channelsPage.postMessage('/dnd');
|
||||
|
||||
// * Verify the Do Not Disturb system message confirms it is enabled
|
||||
await channelsPage.centerView.waitUntilLastPostContains('Do Not Disturb is enabled');
|
||||
|
||||
// # Send another direct message from the admin while the user is in Do Not Disturb
|
||||
const secondMessage = `DND suppressed ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: secondMessage});
|
||||
|
||||
// * Verify no second desktop notification is received while in Do Not Disturb
|
||||
const suppressed = await pw.waitForNotification(page, 2, pw.duration.two_sec);
|
||||
expect(suppressed).toHaveLength(0);
|
||||
|
||||
// # Clear the status back to online via the slash command
|
||||
await channelsPage.postMessage('/online');
|
||||
|
||||
// * Verify the account menu reflects the online status
|
||||
const accountMenu = await channelsPage.openUserAccountMenu();
|
||||
await expect(accountMenu.online).toHaveAttribute('aria-checked', 'true');
|
||||
},
|
||||
);
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify a user in "Do Not Disturb" does not receive desktop notifications, and receives them
|
||||
* again once the status is cleared.
|
||||
*
|
||||
* @precondition
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T495 suppresses desktop notifications while Do Not Disturb is enabled',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, adminUser, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a direct message channel between the admin and the user
|
||||
const dmChannel = await adminClient.createDirectChannel([adminUser.id, user.id]);
|
||||
|
||||
// # Log in and view Town Square (not the DM), then stub notifications
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await pw.stubNotification(page, 'granted');
|
||||
|
||||
// # Send a direct message from the admin while the user is available
|
||||
const firstMessage = `DND baseline ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: firstMessage});
|
||||
|
||||
// * Verify the direct message triggers a desktop notification
|
||||
const notifications = await pw.waitForNotification(page);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].body).toContain(firstMessage);
|
||||
|
||||
// # Enable Do Not Disturb via the slash command
|
||||
await channelsPage.postMessage('/dnd');
|
||||
|
||||
// * Verify the Do Not Disturb system message confirms it is enabled
|
||||
await channelsPage.centerView.waitUntilLastPostContains('Do Not Disturb is enabled');
|
||||
|
||||
// # Send another direct message from the admin while the user is in Do Not Disturb
|
||||
const secondMessage = `DND suppressed ${pw.random.id()}`;
|
||||
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: secondMessage});
|
||||
|
||||
// * Verify no second desktop notification is received while in Do Not Disturb
|
||||
const suppressed = await pw.waitForNotification(page, 2, pw.duration.two_sec);
|
||||
expect(suppressed).toHaveLength(0);
|
||||
|
||||
// # Clear the status back to online via the slash command
|
||||
await channelsPage.postMessage('/online');
|
||||
|
||||
// * Verify the account menu reflects the online status
|
||||
const accountMenu = await channelsPage.openUserAccountMenu();
|
||||
await expect(accountMenu.online).toHaveAttribute('aria-checked', 'true');
|
||||
},
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that channel-wide mentions with uppercase letters trigger notifications and are properly highlighted.
|
||||
*
|
||||
* @precondition
|
||||
* - Two users are members of the same team
|
||||
* - Notification permissions are granted in the browser
|
||||
*/
|
||||
test(
|
||||
'MM-T483 triggers notification with uppercase channel-wide mention and highlights message for all users',
|
||||
{tag: '@notifications'},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup and get the required users and team
|
||||
const {team, adminUser, user} = await pw.initSetup();
|
||||
|
||||
// # Log in as the admin in one browser session and navigate to the "town-square" channel
|
||||
const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
|
||||
// # Stub the Notification in the admin's browser to capture notifications
|
||||
await pw.stubNotification(adminPage, 'granted');
|
||||
|
||||
// # Log in as the regular user in a separate browser and navigate to the "off-topic" channel
|
||||
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(user);
|
||||
await otherChannelsPage.goto(team.name, 'off-topic');
|
||||
await otherChannelsPage.toBeVisible();
|
||||
|
||||
// # Post a channel-wide mention message "@ALL" in uppercase from the user's browser
|
||||
const message = `@ALL good morning, ${team.name}!`;
|
||||
await otherChannelsPage.postMessage(message);
|
||||
|
||||
// * Verify notification is received in the admin's browser with correct content
|
||||
const notifications = await pw.waitForNotification(adminPage);
|
||||
expect(notifications.length).toBe(1);
|
||||
|
||||
const notification = notifications[0];
|
||||
expect(notification.title).toBe('Off-Topic');
|
||||
expect(notification.body).toBe(`@${user.username}: ${message}`);
|
||||
|
||||
// The Notifications API tag must not leak the message body; the app uses the
|
||||
// opaque post ID as the tag so notifications coalesce per conversation.
|
||||
const otherLastPost = await otherChannelsPage.getLastPost();
|
||||
expect(notification.tag).toBe(await otherLastPost.getId());
|
||||
expect(notification.tag).not.toContain(message);
|
||||
|
||||
expect(notification.icon).toContain('.png');
|
||||
expect(notification.requireInteraction).toBe(false);
|
||||
expect(notification.silent).toBe(false);
|
||||
|
||||
// * Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
|
||||
await otherLastPost.toContainText(message);
|
||||
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
|
||||
// # Navigate admin to the "off-topic" channel
|
||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||
|
||||
// * Verify the message is posted and highlighted correctly for the admin user
|
||||
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||
await adminLastPost.toContainText(message);
|
||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
},
|
||||
);
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify a system admin can open and close the plugin (App) Marketplace.
|
||||
*
|
||||
* MM-T1946 and MM-T1947 cover the same behavior as MM-T1945 and are covered by this test.
|
||||
*/
|
||||
test('MM-T1945 MM-T1946 MM-T1947 system admin can view the plugin marketplace', {tag: '@marketplace'}, async ({pw}) => {
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the App Marketplace from the product menu
|
||||
await channelsPage.globalHeader.openAppMarketplace();
|
||||
|
||||
// * Verify the Marketplace is shown
|
||||
const marketplace = channelsPage.marketplaceModal;
|
||||
await marketplace.toBeVisible();
|
||||
|
||||
// # Close the Marketplace
|
||||
await marketplace.close();
|
||||
|
||||
// * Verify the Marketplace is closed
|
||||
await marketplace.notToBeVisible();
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the top navigation buttons remain clickable and open their panels while focus is in the search box.
|
||||
*/
|
||||
test('MM-T367 clicks top navigation buttons while the search box has focus', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// The open search box traps focus and marks the top-right header buttons aria-hidden, so match hidden too.
|
||||
// (The channel-info button lives in the channel header, which the search popup overlays, so it is not
|
||||
// reachable while the search box is open in the current UI and is intentionally not exercised here.)
|
||||
const recentMentionsButton = page.getByRole('button', {name: 'Recent mentions', includeHidden: true});
|
||||
const savedMessagesButton = page.getByRole('button', {name: 'Saved messages', includeHidden: true});
|
||||
|
||||
// # Put focus in the search box and type text, then click Recent mentions
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await channelsPage.searchBox.searchInput.fill('some text');
|
||||
await recentMentionsButton.click();
|
||||
|
||||
// * Verify the Recent Mentions panel opens
|
||||
await channelsPage.searchResultsPanel.toHaveHeading('Recent Mentions');
|
||||
|
||||
// # Focus the search box again and click Saved messages
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await channelsPage.searchBox.searchInput.fill('some text');
|
||||
await savedMessagesButton.click();
|
||||
|
||||
// * Verify the Saved messages panel opens
|
||||
await expect(
|
||||
channelsPage.searchResultsPanel.container.getByRole('heading', {name: 'Saved messages'}).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
+4
-5
@@ -3,11 +3,6 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
// Returns the UTC calendar date of a timestamp formatted as the `on:` filter expects (YYYY-MM-DD).
|
||||
function toDateFilter(timestamp: number): string {
|
||||
return new Date(timestamp).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @objective Verify the "on:" search date filter can be set by clicking a day in the calendar picker,
|
||||
* and that the resulting date-filtered search returns the matching post.
|
||||
@@ -105,3 +100,7 @@ test('MM-T600 updates the search date with the keyboard', {tag: '@search'}, asyn
|
||||
// * Verify the post is no longer returned for the later date
|
||||
await expect(channelsPage.searchResultItems).toHaveCount(0);
|
||||
});
|
||||
|
||||
function toDateFilter(timestamp: number): string {
|
||||
return new Date(timestamp).toISOString().slice(0, 10);
|
||||
}
|
||||
-44
@@ -3,50 +3,6 @@
|
||||
|
||||
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.
|
||||
*/
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that deleting a parent post from the search results removes both the post and its reply.
|
||||
*/
|
||||
test('MM-T381 deletes a parent post from the search results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and open a channel
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Delete Parent', 'delete-parent');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const parentToken = `parentpost${pw.random.id()}`;
|
||||
const replyToken = `replypost${pw.random.id()}`;
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and reply to it, then close the thread
|
||||
await channelsPage.postMessage(parentToken);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage(replyToken);
|
||||
await channelsPage.sidebarRight.close();
|
||||
|
||||
// * Verify both the parent post and its reply are searchable before deletion
|
||||
await channelsPage.searchFor(replyToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(1);
|
||||
await channelsPage.searchFor(parentToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(1);
|
||||
|
||||
// # Delete the parent post from the search result
|
||||
await channelsPage.searchResultsPanel.openResultDotMenu(parentToken);
|
||||
await channelsPage.postDotMenu.deleteMenuItem.click();
|
||||
await channelsPage.deletePostModal.toBeVisible();
|
||||
await channelsPage.deletePostModal.confirm();
|
||||
|
||||
// * Verify the deleted parent post is removed from the open search results in real time
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(0);
|
||||
|
||||
// * Verify the reply was also removed (searching for it returns no results)
|
||||
await channelsPage.searchFor(replyToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(0);
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the search box shows help text when opened from the channel and again after opening a thread.
|
||||
*/
|
||||
test('MM-T370 shows search help text when the search box is opened', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread
|
||||
await channelsPage.postMessage(`help text ${pw.random.id()}`);
|
||||
|
||||
// # Open the search box
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
|
||||
// * Verify the search help text is shown
|
||||
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
|
||||
|
||||
// # Close the search box, then open a thread
|
||||
await channelsPage.page.keyboard.press('Escape');
|
||||
await expect(channelsPage.searchBox.container).not.toBeVisible();
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
|
||||
// * Verify the search help text is shown again
|
||||
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that searching with trailing double quotes highlights only the searched terms, with no unexpected highlighting.
|
||||
*/
|
||||
test('MM-T354 highlights only the searched terms when searching with double quotes', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message that contains the two search words plus an unrelated unique word
|
||||
const marker = `zzz${pw.random.id()}`;
|
||||
await channelsPage.postMessage(`test tool ${marker}`);
|
||||
|
||||
// # Search using the two words followed by an empty double-quoted phrase
|
||||
await channelsPage.searchFor('test tool ""');
|
||||
|
||||
// * Verify the message is returned
|
||||
await channelsPage.searchResultsPanel.toContainText(marker);
|
||||
|
||||
// * Verify only the two searched words are highlighted (no unexpected highlighting)
|
||||
const highlights = channelsPage.searchResultsPanel.getHighlightedTerms();
|
||||
await expect(highlights).toHaveCount(2);
|
||||
await expect(highlights.filter({hasText: 'test'})).toHaveCount(1);
|
||||
await expect(highlights.filter({hasText: 'tool'})).toHaveCount(1);
|
||||
await expect(highlights.filter({hasText: marker})).toHaveCount(0);
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the search result highlighting reflects the submitted query and does not change while the search input is edited before submitting again.
|
||||
*/
|
||||
test(
|
||||
'MM-T371 keeps search result highlighting unchanged while editing the search input',
|
||||
{tag: '@search'},
|
||||
async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message that contains two distinct words
|
||||
const suffix = pw.random.id();
|
||||
const firstWord = `apple${suffix}`;
|
||||
const secondWord = `banana${suffix}`;
|
||||
await channelsPage.postMessage(`${firstWord} ${secondWord}`);
|
||||
|
||||
// # Search for the first word
|
||||
await channelsPage.searchFor(firstWord);
|
||||
|
||||
// * Verify the result contains both words but highlights only the searched (first) word
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(secondWord);
|
||||
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
|
||||
|
||||
// # Reopen the search box and replace the query with the second word without submitting
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
const {searchInput} = channelsPage.searchBox;
|
||||
await expect(searchInput).toHaveValue(firstWord);
|
||||
await searchInput.fill(secondWord);
|
||||
|
||||
// * Verify the search results still highlight the originally searched (first) word
|
||||
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
|
||||
},
|
||||
);
|
||||
+67
@@ -3,6 +3,73 @@
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that searching with trailing double quotes highlights only the searched terms, with no unexpected highlighting.
|
||||
*/
|
||||
test('MM-T354 highlights only the searched terms when searching with double quotes', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message that contains the two search words plus an unrelated unique word
|
||||
const marker = `zzz${pw.random.id()}`;
|
||||
await channelsPage.postMessage(`test tool ${marker}`);
|
||||
|
||||
// # Search using the two words followed by an empty double-quoted phrase
|
||||
await channelsPage.searchFor('test tool ""');
|
||||
|
||||
// * Verify the message is returned
|
||||
await channelsPage.searchResultsPanel.toContainText(marker);
|
||||
|
||||
// * Verify only the two searched words are highlighted (no unexpected highlighting)
|
||||
const highlights = channelsPage.searchResultsPanel.getHighlightedTerms();
|
||||
await expect(highlights).toHaveCount(2);
|
||||
await expect(highlights.filter({hasText: 'test'})).toHaveCount(1);
|
||||
await expect(highlights.filter({hasText: 'tool'})).toHaveCount(1);
|
||||
await expect(highlights.filter({hasText: marker})).toHaveCount(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that the search result highlighting reflects the submitted query and does not change while the search input is edited before submitting again.
|
||||
*/
|
||||
test(
|
||||
'MM-T371 keeps search result highlighting unchanged while editing the search input',
|
||||
{tag: '@search'},
|
||||
async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message that contains two distinct words
|
||||
const suffix = pw.random.id();
|
||||
const firstWord = `apple${suffix}`;
|
||||
const secondWord = `banana${suffix}`;
|
||||
await channelsPage.postMessage(`${firstWord} ${secondWord}`);
|
||||
|
||||
// # Search for the first word
|
||||
await channelsPage.searchFor(firstWord);
|
||||
|
||||
// * Verify the result contains both words but highlights only the searched (first) word
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(secondWord);
|
||||
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
|
||||
|
||||
// # Reopen the search box and replace the query with the second word without submitting
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
const {searchInput} = channelsPage.searchBox;
|
||||
await expect(searchInput).toHaveValue(firstWord);
|
||||
await searchInput.fill(secondWord);
|
||||
|
||||
// * Verify the search results still highlight the originally searched (first) word
|
||||
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that search term highlighting appears in the search results panel but does not persist in the saved or pinned message panels.
|
||||
*/
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// 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 "in:" search autocomplete shows a renamed channel by both its new display name and
|
||||
* its original handle, so it stays findable after a display-name change.
|
||||
*/
|
||||
test(
|
||||
'MM-T1449 shows the channel display name and handle in the in: search autocomplete',
|
||||
{tag: '@search'},
|
||||
async ({pw}) => {
|
||||
const {adminClient, team, user} = await pw.initSetup();
|
||||
|
||||
// # Create a public channel whose handle differs from its display name, then add the user
|
||||
const id = pw.random.id();
|
||||
const handle = `original-${id}`;
|
||||
const updatedName = `Updated ${id}`;
|
||||
const channel = await adminClient.createPublicChannel(team.id, `Original ${id}`, handle);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Rename the channel's display name via Channel Settings
|
||||
const channelSettings = await channelsPage.openChannelSettings();
|
||||
const infoSettings = await channelSettings.openInfoTab();
|
||||
await infoSettings.updateName(updatedName);
|
||||
await channelSettings.save();
|
||||
await channelSettings.close();
|
||||
await channelsPage.centerView.header.toHaveTitle(updatedName);
|
||||
|
||||
// # Open search and type an "in:" filter matching the channel's original handle
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await channelsPage.searchBox.searchInput.fill('in:');
|
||||
await channelsPage.searchBox.searchInput.pressSequentially(handle);
|
||||
|
||||
// * Verify the autocomplete shows the channel by its new display name and its original handle
|
||||
const suggestion = channelsPage.searchBox.container.getByRole('option').filter({hasText: updatedName});
|
||||
await expect(suggestion).toBeVisible();
|
||||
await expect(suggestion).toContainText(updatedName);
|
||||
await expect(suggestion).toContainText(`~${handle}`);
|
||||
},
|
||||
);
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a wildcard (*) is disregarded when it is not immediately preceded by text.
|
||||
*/
|
||||
test('MM-T351 disregards a wildcard that is not preceded by text', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post two messages, one with a longer variant of the search term
|
||||
const token = `qwerty${pw.random.id()}`;
|
||||
await channelsPage.postMessage(`${token}jkl`);
|
||||
await channelsPage.postMessage(token);
|
||||
|
||||
// # Search using a trailing wildcard separated by a space
|
||||
await channelsPage.searchFor(`${token} *`);
|
||||
|
||||
// * Verify the wildcard is disregarded and the exact-term message is returned
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that the top navigation buttons remain clickable and open their panels while focus is in the search box.
|
||||
*/
|
||||
test('MM-T367 clicks top navigation buttons while the search box has focus', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// The open search box traps focus and marks the top-right header buttons aria-hidden, so match hidden too.
|
||||
// (The channel-info button lives in the channel header, which the search popup overlays, so it is not
|
||||
// reachable while the search box is open in the current UI and is intentionally not exercised here.)
|
||||
const recentMentionsButton = page.getByRole('button', {name: 'Recent mentions', includeHidden: true});
|
||||
const savedMessagesButton = page.getByRole('button', {name: 'Saved messages', includeHidden: true});
|
||||
|
||||
// # Put focus in the search box and type text, then click Recent mentions
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await channelsPage.searchBox.searchInput.fill('some text');
|
||||
await recentMentionsButton.click();
|
||||
|
||||
// * Verify the Recent Mentions panel opens
|
||||
await channelsPage.searchResultsPanel.toHaveHeading('Recent Mentions');
|
||||
|
||||
// # Focus the search box again and click Saved messages
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
await channelsPage.searchBox.searchInput.fill('some text');
|
||||
await savedMessagesButton.click();
|
||||
|
||||
// * Verify the Saved messages panel opens
|
||||
await expect(
|
||||
channelsPage.searchResultsPanel.container.getByRole('heading', {name: 'Saved messages'}).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that the search box shows help text when opened from the channel and again after opening a thread.
|
||||
*/
|
||||
test('MM-T370 shows search help text when the search box is opened', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and open its thread
|
||||
await channelsPage.postMessage(`help text ${pw.random.id()}`);
|
||||
|
||||
// # Open the search box
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
|
||||
// * Verify the search help text is shown
|
||||
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
|
||||
|
||||
// # Close the search box, then open a thread
|
||||
await channelsPage.page.keyboard.press('Escape');
|
||||
await expect(channelsPage.searchBox.container).not.toBeVisible();
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.globalHeader.openSearch();
|
||||
await channelsPage.searchBox.toBeVisible();
|
||||
|
||||
// * Verify the search help text is shown again
|
||||
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that jumping from a search result opens a post that is not already displayed in the center channel.
|
||||
*/
|
||||
test('MM-T380 jumps from a search result to a post in another channel', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and create a channel with many posts
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const linkChannel = await adminClient.createPublicChannel(team.id, 'Link Test', 'link-test');
|
||||
await adminClient.addToChannel(user.id, linkChannel.id);
|
||||
|
||||
// # Post a target message surrounded by many other messages so it is not initially loaded elsewhere
|
||||
const token = `asparagus${pw.random.id()}`;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post before ${i}`});
|
||||
}
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: token});
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post after ${i}`});
|
||||
}
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Search for the target message from a different channel
|
||||
await channelsPage.searchFor(token);
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
|
||||
// # Jump to the post from the search results
|
||||
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
|
||||
|
||||
// * Verify navigation to the target channel and that the target post is displayed in the center
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/channels/${linkChannel.name}`);
|
||||
await expect(channelsPage.centerView.container.getByText(token, {exact: true})).toBeVisible();
|
||||
});
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a full username containing '-' or '_' is shown as a mention link in the mentioned user's search results.
|
||||
*/
|
||||
test('MM-T348 shows a full special-character username as a link in search results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create a user whose username contains '-' and '_', plus a user who mentions them
|
||||
const {user: mentioningUser, team, adminClient, userClient} = await pw.initSetup();
|
||||
const [specialUser] = await adminClient.createUsers(team.id, 1, 'test-user_');
|
||||
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
|
||||
await adminClient.addToChannel(specialUser.id, townSquare.id);
|
||||
|
||||
// # The first user posts a message mentioning the special-character username
|
||||
const token = `mention${pw.random.id()}`;
|
||||
await userClient.createPost({
|
||||
channel_id: townSquare.id,
|
||||
message: `@${specialUser.username} ${token}`,
|
||||
user_id: mentioningUser.id,
|
||||
});
|
||||
|
||||
// # Log in as the mentioned user and open Recent Mentions
|
||||
const {channelsPage} = await pw.testBrowser.login(specialUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.globalHeader.openRecentMentions();
|
||||
|
||||
// * Verify the mention shows the full username as a clickable mention (rendered as a button)
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
const result = channelsPage.searchResultsPanel.getResultByText(token);
|
||||
await expect(result.getByRole('button', {name: `@${specialUser.username}`})).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that jumping from a search result opens a post that is not already displayed in the center channel.
|
||||
*/
|
||||
test('MM-T380 jumps from a search result to a post in another channel', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and create a channel with many posts
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const linkChannel = await adminClient.createPublicChannel(team.id, 'Link Test', 'link-test');
|
||||
await adminClient.addToChannel(user.id, linkChannel.id);
|
||||
|
||||
// # Post a target message surrounded by many other messages so it is not initially loaded elsewhere
|
||||
const token = `asparagus${pw.random.id()}`;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post before ${i}`});
|
||||
}
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: token});
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post after ${i}`});
|
||||
}
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'off-topic');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Search for the target message from a different channel
|
||||
await channelsPage.searchFor(token);
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
|
||||
// # Jump to the post from the search results
|
||||
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
|
||||
|
||||
// * Verify navigation to the target channel and that the target post is displayed in the center
|
||||
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/channels/${linkChannel.name}`);
|
||||
await expect(channelsPage.centerView.container.getByText(token, {exact: true})).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that deleting a parent post from the search results removes both the post and its reply.
|
||||
*/
|
||||
test('MM-T381 deletes a parent post from the search results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and open a channel
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Delete Parent', 'delete-parent');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const parentToken = `parentpost${pw.random.id()}`;
|
||||
const replyToken = `replypost${pw.random.id()}`;
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message and reply to it, then close the thread
|
||||
await channelsPage.postMessage(parentToken);
|
||||
const post = await channelsPage.getLastPost();
|
||||
await post.reply();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
await channelsPage.sidebarRight.postMessage(replyToken);
|
||||
await channelsPage.sidebarRight.close();
|
||||
|
||||
// * Verify both the parent post and its reply are searchable before deletion
|
||||
await channelsPage.searchFor(replyToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(1);
|
||||
await channelsPage.searchFor(parentToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(1);
|
||||
|
||||
// # Delete the parent post from the search result
|
||||
await channelsPage.searchResultsPanel.openResultDotMenu(parentToken);
|
||||
await channelsPage.postDotMenu.deleteMenuItem.click();
|
||||
await channelsPage.deletePostModal.toBeVisible();
|
||||
await channelsPage.deletePostModal.confirm();
|
||||
|
||||
// * Verify the deleted parent post is removed from the open search results in real time
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(0);
|
||||
|
||||
// * Verify the reply was also removed (searching for it returns no results)
|
||||
await channelsPage.searchFor(replyToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(0);
|
||||
});
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// 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 that a new search replaces the previous results instead of combining them.
|
||||
*/
|
||||
test('MM-T355 replaces old search results with new results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
const helloToken = `hello${pw.random.id()}`;
|
||||
const writingToken = `writing${pw.random.id()}`;
|
||||
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message in Town Square and search for it
|
||||
await channelsPage.postMessage(`${helloToken} there everyone`);
|
||||
await channelsPage.searchFor(helloToken);
|
||||
|
||||
// * Verify the first message is shown in the results
|
||||
await channelsPage.searchResultsPanel.toContainText(helloToken);
|
||||
|
||||
// # Post a different message in another channel and run a new search for it
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
await channelsPage.postMessage(`${writingToken} to you here`);
|
||||
await channelsPage.searchFor(writingToken);
|
||||
|
||||
// * Verify the new results are shown and the old results are gone
|
||||
await channelsPage.searchResultsPanel.toContainText(writingToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(helloToken)).toHaveCount(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify that scrolling the center channel while the search results panel is open does not change the results.
|
||||
*/
|
||||
test('MM-T382 keeps search results unchanged while scrolling the center channel', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and open a channel with many matching posts
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Search Scroll', 'search-scroll');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const token = `searchfun${pw.random.id()}`;
|
||||
const messageCount = 30;
|
||||
for (let i = 0; i < messageCount; i++) {
|
||||
await adminClient.createPost({channel_id: channel.id, message: `${token} ${i}`});
|
||||
}
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Search for the matching posts
|
||||
await channelsPage.searchFor(token);
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
const resultCountBefore = await channelsPage.searchResultsPanel.getResultItems().count();
|
||||
expect(resultCountBefore).toBeGreaterThan(0);
|
||||
|
||||
// # Scroll the center channel up to load older messages
|
||||
const box = await channelsPage.centerView.container.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await page.mouse.wheel(0, -1500);
|
||||
await pw.wait(pw.duration.half_sec);
|
||||
}
|
||||
|
||||
// * Verify the search results panel is still open and its results are unchanged
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
expect(await channelsPage.searchResultsPanel.getResultItems().count()).toBe(resultCountBefore);
|
||||
});
|
||||
|
||||
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});
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a full username containing '-' or '_' is shown as a mention link in the mentioned user's search results.
|
||||
*/
|
||||
test('MM-T348 shows a full special-character username as a link in search results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create a user whose username contains '-' and '_', plus a user who mentions them
|
||||
const {user: mentioningUser, team, adminClient, userClient} = await pw.initSetup();
|
||||
const [specialUser] = await adminClient.createUsers(team.id, 1, 'test-user_');
|
||||
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
|
||||
await adminClient.addToChannel(specialUser.id, townSquare.id);
|
||||
|
||||
// # The first user posts a message mentioning the special-character username
|
||||
const token = `mention${pw.random.id()}`;
|
||||
await userClient.createPost({
|
||||
channel_id: townSquare.id,
|
||||
message: `@${specialUser.username} ${token}`,
|
||||
user_id: mentioningUser.id,
|
||||
});
|
||||
|
||||
// # Log in as the mentioned user and open Recent Mentions
|
||||
const {channelsPage} = await pw.testBrowser.login(specialUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.globalHeader.openRecentMentions();
|
||||
|
||||
// * Verify the mention shows the full username as a clickable mention (rendered as a button)
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
const result = channelsPage.searchResultsPanel.getResultByText(token);
|
||||
await expect(result.getByRole('button', {name: `@${specialUser.username}`})).toBeVisible();
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a new search replaces the previous results instead of combining them.
|
||||
*/
|
||||
test('MM-T355 replaces old search results with new results', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
const helloToken = `hello${pw.random.id()}`;
|
||||
const writingToken = `writing${pw.random.id()}`;
|
||||
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post a message in Town Square and search for it
|
||||
await channelsPage.postMessage(`${helloToken} there everyone`);
|
||||
await channelsPage.searchFor(helloToken);
|
||||
|
||||
// * Verify the first message is shown in the results
|
||||
await channelsPage.searchResultsPanel.toContainText(helloToken);
|
||||
|
||||
// # Post a different message in another channel and run a new search for it
|
||||
await channelsPage.sidebarLeft.goToItem('off-topic');
|
||||
await channelsPage.postMessage(`${writingToken} to you here`);
|
||||
await channelsPage.searchFor(writingToken);
|
||||
|
||||
// * Verify the new results are shown and the old results are gone
|
||||
await channelsPage.searchResultsPanel.toContainText(writingToken);
|
||||
await expect(channelsPage.searchResultsPanel.getResultByText(helloToken)).toHaveCount(0);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that scrolling the center channel while the search results panel is open does not change the results.
|
||||
*/
|
||||
test('MM-T382 keeps search results unchanged while scrolling the center channel', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user, and open a channel with many matching posts
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const channel = await adminClient.createPublicChannel(team.id, 'Search Scroll', 'search-scroll');
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const token = `searchfun${pw.random.id()}`;
|
||||
const messageCount = 30;
|
||||
for (let i = 0; i < messageCount; i++) {
|
||||
await adminClient.createPost({channel_id: channel.id, message: `${token} ${i}`});
|
||||
}
|
||||
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Search for the matching posts
|
||||
await channelsPage.searchFor(token);
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
const resultCountBefore = await channelsPage.searchResultsPanel.getResultItems().count();
|
||||
expect(resultCountBefore).toBeGreaterThan(0);
|
||||
|
||||
// # Scroll the center channel up to load older messages
|
||||
const box = await channelsPage.centerView.container.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await page.mouse.wheel(0, -1500);
|
||||
await pw.wait(pw.duration.half_sec);
|
||||
}
|
||||
|
||||
// * Verify the search results panel is still open and its results are unchanged
|
||||
await channelsPage.searchResultsPanel.toBeVisible();
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
expect(await channelsPage.searchResultsPanel.getResultItems().count()).toBe(resultCountBefore);
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a wildcard (*) is disregarded when it is not immediately preceded by text.
|
||||
*/
|
||||
test('MM-T351 disregards a wildcard that is not preceded by text', {tag: '@search'}, async ({pw}) => {
|
||||
// # Create and log in as a test user
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Post two messages, one with a longer variant of the search term
|
||||
const token = `qwerty${pw.random.id()}`;
|
||||
await channelsPage.postMessage(`${token}jkl`);
|
||||
await channelsPage.postMessage(token);
|
||||
|
||||
// # Search using a trailing wildcard separated by a space
|
||||
await channelsPage.searchFor(`${token} *`);
|
||||
|
||||
// * Verify the wildcard is disregarded and the exact-term message is returned
|
||||
await channelsPage.searchResultsPanel.toContainText(token);
|
||||
});
|
||||
@@ -39,8 +39,23 @@ const AutoHeightSwitcher = ({showSlot, onTransitionEnd, slot1 = null, slot2 = nu
|
||||
} else {
|
||||
// switch slots using height animation
|
||||
prevSlot.current = showSlot;
|
||||
setAnimate(true);
|
||||
|
||||
if (animate) {
|
||||
// A previous transition is still in flight, so the Transition's
|
||||
// `in` prop is already `true`; toggling it `true` again is a
|
||||
// no-op and `onEnter` will not re-run. Swap the child directly
|
||||
// to the newly selected slot so the correct content is shown
|
||||
// once the in-flight transition ends (otherwise the stale slot
|
||||
// stays rendered).
|
||||
setChild(showSlot === AutoHeightSlots.SLOT1 ? slot1 : slot2);
|
||||
} else {
|
||||
setAnimate(true);
|
||||
}
|
||||
}
|
||||
|
||||
// `animate` is read to detect an in-flight transition but intentionally excluded from the
|
||||
// deps so this effect only runs on a slot change, not on every animation state toggle.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showSlot, slot1, slot2]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user