[MM-53127] e2e-tests : Drafts badge doesn't clear when parent message deleted and removed (#23705)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
33565dc118
Коммит
b153a39d3e
@@ -2,7 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import { PreferenceType } from '@mattermost/types/preferences';
|
||||
|
||||
import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server';
|
||||
import {defaultTeam} from './support/util';
|
||||
@@ -63,6 +65,9 @@ async function sysadminSetup(client: Client, user: UserProfile | null) {
|
||||
);
|
||||
}
|
||||
|
||||
// Set default preferences
|
||||
await savePreferences(client, user?.id ?? '');
|
||||
|
||||
// Ensure all products as plugin are installed and active.
|
||||
await ensurePluginsLoaded(client);
|
||||
|
||||
@@ -184,4 +189,23 @@ async function ensureServerDeployment(client: Client) {
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences(client: Client, userId: UserProfile['id']) {
|
||||
try {
|
||||
if (!userId) {
|
||||
throw new Error('userId is not defined');
|
||||
}
|
||||
|
||||
const preferences: PreferenceType[] = [
|
||||
{user_id: userId, category: 'tutorial_step', name: userId, value: '999'},
|
||||
{user_id: userId, category: 'drafts', name: 'drafts_tour_tip_showed', value: JSON.stringify({drafts_tour_tip_showed: true})},
|
||||
{user_id: userId, category: 'crt_thread_pane_step', name: userId, value: '999'},
|
||||
];
|
||||
|
||||
await client.savePreferences(userId, preferences);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error saving preferences', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
|
||||
@@ -52,6 +52,8 @@ export async function initSetup({
|
||||
// Update user preference
|
||||
const preferences: PreferenceType[] = [
|
||||
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
||||
{user_id: user.id, category: 'drafts', name: 'drafts_tour_tip_showed', value: JSON.stringify({drafts_tour_tip_showed: true})},
|
||||
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
|
||||
];
|
||||
await userClient.savePreferences(user.id, preferences);
|
||||
|
||||
|
||||
32
e2e-tests/playwright/support/server/post.ts
Обычный файл
32
e2e-tests/playwright/support/server/post.ts
Обычный файл
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Post, PostMetadata} from '@mattermost/types/posts';
|
||||
import {getRandomId} from '@e2e-support/util';
|
||||
|
||||
export function createRandomPost(post?: Partial<Post>): Post {
|
||||
if (post && post.channel_id && post.user_id) {
|
||||
const time = Date.now();
|
||||
|
||||
const defaultPost = {
|
||||
create_at: time,
|
||||
user_id: post.user_id,
|
||||
channel_id: post.channel_id,
|
||||
root_id: post.root_id || '',
|
||||
message: `${post?.message ?? ''}${getRandomId()}`,
|
||||
pending_post_id: `${post.user_id}:${time}`,
|
||||
props: post?.props || {},
|
||||
file_ids: post?.file_ids || [],
|
||||
metadata: {} as PostMetadata,
|
||||
};
|
||||
|
||||
Reflect.deleteProperty(post, 'user_id');
|
||||
Reflect.deleteProperty(post, 'channel_id');
|
||||
Reflect.deleteProperty(post, 'message');
|
||||
Reflect.deleteProperty(post, 'pending_post_id');
|
||||
|
||||
return {...defaultPost, ...post} as Post;
|
||||
}
|
||||
|
||||
throw new Error('Post is missing channel_id or user_id or both');
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class DeletePostModal {
|
||||
readonly container: Locator;
|
||||
readonly confirmButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.confirmButton = this.container.locator('#deletePostModalButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.confirmButton.waitFor();
|
||||
await this.confirmButton.click();
|
||||
|
||||
// Wait for the modal to disappear
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {DeletePostModal};
|
||||
@@ -3,25 +3,43 @@
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class ChannelsPost {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly body;
|
||||
readonly profileIcon;
|
||||
readonly replyButton;
|
||||
|
||||
readonly removePostButton;
|
||||
|
||||
readonly postMenu;
|
||||
readonly threadFooter;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.body = container.locator('.post__body');
|
||||
|
||||
this.profileIcon = container.locator('.profile-icon');
|
||||
this.replyButton = container.getByRole('button', {name: 'reply'});
|
||||
|
||||
this.removePostButton = container.locator('.post__remove');
|
||||
|
||||
this.postMenu = new components.PostMenu(container.locator('.post-menu'));
|
||||
this.threadFooter = new components.ThreadFooter(container.locator('.ThreadFooter'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over the post. Can be used for post menu to appear.
|
||||
*/
|
||||
async hover() {
|
||||
await this.container.hover();
|
||||
}
|
||||
|
||||
async getId() {
|
||||
const id = await this.container.getAttribute('id');
|
||||
expect(id, 'No post ID found.').toBeTruthy();
|
||||
@@ -29,13 +47,21 @@ export default class ChannelsPost {
|
||||
}
|
||||
|
||||
async getProfileImage(username: string) {
|
||||
return await this.profileIcon.getByAltText(`${username} profile image`);
|
||||
return this.profileIcon.getByAltText(`${username} profile image`);
|
||||
}
|
||||
|
||||
async openRHS() {
|
||||
/**
|
||||
* Clicks on the deleted post's remove 'x' button.
|
||||
* Also verifies that the post is a deleted post.
|
||||
*/
|
||||
async remove() {
|
||||
// Verify the post is a deleted post
|
||||
await expect(this.container).toContainText(/\(message deleted\)/);
|
||||
|
||||
// Hover over the post and click on the remove post button
|
||||
await this.container.hover();
|
||||
await this.replyButton.waitFor();
|
||||
await this.replyButton.click();
|
||||
await this.removePostButton.waitFor();
|
||||
await this.removePostButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default class ChannelsPostCreate {
|
||||
readonly input;
|
||||
readonly attachmentButton;
|
||||
readonly emojiButton;
|
||||
readonly sendButton: Locator;
|
||||
readonly sendMessageButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
@@ -17,7 +17,7 @@ export default class ChannelsPostCreate {
|
||||
this.input = container.getByTestId('post_textbox');
|
||||
this.attachmentButton = container.getByLabel('attachment');
|
||||
this.emojiButton = container.getByLabel('select an emoji');
|
||||
this.sendButton = container.getByTestId('SendMessageButton');
|
||||
this.sendMessageButton = container.getByTestId('SendMessageButton');
|
||||
}
|
||||
|
||||
async postMessage(message: string) {
|
||||
@@ -25,7 +25,7 @@ export default class ChannelsPostCreate {
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
await this.sendButton.click();
|
||||
await this.sendMessageButton.click();
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class PostDotMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly deleteMenuItem;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.deleteMenuItem = this.container.getByText('Delete', {exact: true});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.deleteMenuItem.waitFor();
|
||||
await this.deleteMenuItem.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {PostDotMenu};
|
||||
40
e2e-tests/playwright/support/ui/components/channels/post_menu.ts
Обычный файл
40
e2e-tests/playwright/support/ui/components/channels/post_menu.ts
Обычный файл
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class PostMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly replyButton;
|
||||
readonly dotMenuButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.replyButton = container.getByRole('button', {name: 'reply'});
|
||||
this.dotMenuButton = container.getByRole('button', {name: 'more'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the reply button from the post menu.
|
||||
*/
|
||||
async reply() {
|
||||
await this.replyButton.waitFor();
|
||||
await this.replyButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the dot menu button from the post menu.
|
||||
*/
|
||||
async openDotMenu() {
|
||||
await this.dotMenuButton.waitFor();
|
||||
await this.dotMenuButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {PostMenu};
|
||||
@@ -16,6 +16,34 @@ export default class ChannelsSidebarLeft {
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the sidebar channel link with the given name.
|
||||
* It can be any sidebar item name including channels, direct messages, or group messages, threads, etc.
|
||||
* @param channelName
|
||||
*/
|
||||
async goToItem(channelName: string) {
|
||||
const channel = this.container.locator(`#sidebarItem_${channelName}`);
|
||||
await channel.waitFor();
|
||||
await channel.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies 'Drafts' as a sidebar link exists in LHS.
|
||||
*/
|
||||
async draftsVisible() {
|
||||
const draftSidebarLink = this.container.getByText('Drafts', {exact: true});
|
||||
await draftSidebarLink.waitFor();
|
||||
await expect(draftSidebarLink).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies 'Drafts' as a sidebar link does not exist in LHS.
|
||||
*/
|
||||
async draftsNotVisible() {
|
||||
const channel = this.container.getByText('Drafts', {exact: true});
|
||||
await expect(channel).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsSidebarLeft};
|
||||
|
||||
@@ -3,16 +3,62 @@
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class ChannelsSidebarRight {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly input;
|
||||
readonly sendMessageButton;
|
||||
readonly closeButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.input = container.getByTestId('reply_textbox');
|
||||
this.sendMessageButton = container.getByTestId('SendMessageButton');
|
||||
|
||||
this.closeButton = container.locator('#rhsCloseButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async postMessage(message: string) {
|
||||
await this.input.fill(message);
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
await this.sendMessageButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the textbox in RHS
|
||||
*/
|
||||
async getInputValue() {
|
||||
return await this.input.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RHS post by post id
|
||||
* @param postId Just the ID without the prefix
|
||||
*/
|
||||
async getRHSPostById(postId: string) {
|
||||
const rhsPostId = `rhsPost_${postId}`;
|
||||
const postLocator = this.container.locator(`#${rhsPostId}`);
|
||||
return new components.ChannelsPost(postLocator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the RHS
|
||||
*/
|
||||
async close() {
|
||||
await this.closeButton.waitFor();
|
||||
await this.closeButton.click();
|
||||
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsSidebarRight};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class ThreadFooter {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly replyButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.replyButton = container.locator('.ReplyButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the reply button in the thread footer to open the thread in RHS.
|
||||
*/
|
||||
async reply() {
|
||||
await this.replyButton.waitFor();
|
||||
await this.replyButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {ThreadFooter};
|
||||
@@ -11,6 +11,10 @@ import {ChannelsSidebarLeft} from './channels/sidebar_left';
|
||||
import {ChannelsSidebarRight} from './channels/sidebar_right';
|
||||
import {FindChannelsModal} from './channels/find_channels_modal';
|
||||
import {GlobalHeader} from './global_header';
|
||||
import {PostDotMenu} from './channels/post_dot_menu';
|
||||
import {DeletePostModal} from './channels/delete_post_modal';
|
||||
import {PostMenu} from './channels/post_menu';
|
||||
import {ThreadFooter} from './channels/thread_footer';
|
||||
|
||||
const components = {
|
||||
BoardsSidebar,
|
||||
@@ -23,6 +27,10 @@ const components = {
|
||||
ChannelsSidebarRight,
|
||||
FindChannelsModal,
|
||||
GlobalHeader,
|
||||
PostDotMenu,
|
||||
DeletePostModal,
|
||||
PostMenu,
|
||||
ThreadFooter,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -37,4 +45,8 @@ export {
|
||||
ChannelsSidebarRight,
|
||||
FindChannelsModal,
|
||||
GlobalHeader,
|
||||
PostDotMenu,
|
||||
DeletePostModal,
|
||||
PostMenu,
|
||||
ThreadFooter,
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ export default class ChannelsPage {
|
||||
readonly appBar;
|
||||
readonly sidebarLeft;
|
||||
readonly sidebarRight;
|
||||
readonly postDotMenu;
|
||||
readonly deletePostModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
@@ -29,6 +31,8 @@ export default class ChannelsPage {
|
||||
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
|
||||
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
|
||||
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
|
||||
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||
}
|
||||
|
||||
async goto(teamName = '', channelName = '') {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test, expect} from '@e2e-support/test_fixture';
|
||||
import {createRandomPost} from '@e2e-support/server/post';
|
||||
|
||||
test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another user deleted root post and user removes the deleted post ', async ({
|
||||
pw,
|
||||
pages,
|
||||
}) => {
|
||||
const {adminClient, team, adminUser, user} = await pw.initSetup();
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Get the default channel of the team for getting the channel id
|
||||
const channel = await adminClient.getChannelByName(team.id, 'town-square');
|
||||
|
||||
// # Create a post in the channel by admin
|
||||
const adminPost = await adminClient.createPost(
|
||||
createRandomPost({
|
||||
channel_id: channel.id,
|
||||
user_id: adminUser.id,
|
||||
})
|
||||
);
|
||||
|
||||
// # Log in as user in new browser context
|
||||
const {page} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit default channel page
|
||||
const channelPage = new pages.ChannelsPage(page);
|
||||
await channelPage.goto();
|
||||
await channelPage.toBeVisible();
|
||||
|
||||
const lastPostByAdmin = await channelPage.getLastPost();
|
||||
await lastPostByAdmin.toBeVisible();
|
||||
|
||||
// # Open the last post sent by admin in RHS
|
||||
await lastPostByAdmin.hover();
|
||||
await lastPostByAdmin.postMenu.toBeVisible();
|
||||
await lastPostByAdmin.postMenu.reply();
|
||||
|
||||
// # Write a message as a user
|
||||
const sidebarRight = channelPage.sidebarRight;
|
||||
await sidebarRight.toBeVisible();
|
||||
await sidebarRight.postMessage('Replying to a thread');
|
||||
await sidebarRight.sendMessage();
|
||||
|
||||
// # Write a message in the reply thread but don't send it now so that it becomes a draft
|
||||
const draftMessageByUser = 'I should be in drafts by User';
|
||||
await sidebarRight.postMessage(draftMessageByUser);
|
||||
|
||||
// # Close the RHS for draft to be saved
|
||||
await sidebarRight.close()
|
||||
|
||||
// * Verify drafts link in channel sidebar is visible
|
||||
await channelPage.sidebarLeft.draftsVisible();
|
||||
|
||||
// # Delete the last post by admin
|
||||
try {
|
||||
await adminClient.deletePost(adminPost.id);
|
||||
} catch (error) {
|
||||
throw new Error('Failed to delete post by admin');
|
||||
}
|
||||
|
||||
// # Open the last post in the channel sent by admin again
|
||||
await lastPostByAdmin.threadFooter.reply();
|
||||
|
||||
// * Verify drafts in user's textbox is still visible
|
||||
const rhsTextboxValue = await sidebarRight.getInputValue();
|
||||
expect(rhsTextboxValue).toBe(draftMessageByUser);
|
||||
|
||||
// # Click on remove post
|
||||
const deletedPostByAdminInRHS = await sidebarRight.getRHSPostById(adminPost.id);
|
||||
await deletedPostByAdminInRHS.remove();
|
||||
|
||||
// * Verify the drafts links should also be removed from sidebar
|
||||
await channelPage.sidebarLeft.draftsNotVisible();
|
||||
});
|
||||
|
||||
test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user deletes root post ', async ({pw, pages}) => {
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in as user in new browser context
|
||||
const {page} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit default channel page
|
||||
const channelPage = new pages.ChannelsPage(page);
|
||||
await channelPage.goto();
|
||||
await channelPage.toBeVisible();
|
||||
|
||||
// # Post a message in the channel
|
||||
await channelPage.postMessage('Message which will be deleted');
|
||||
await channelPage.sendMessage();
|
||||
|
||||
// # Start a thread by clicking on reply menuitem from post options menu
|
||||
const post = await channelPage.getLastPost();
|
||||
await post.hover();
|
||||
await post.postMenu.toBeVisible();
|
||||
await post.postMenu.reply();
|
||||
|
||||
const sidebarRight = channelPage.sidebarRight;
|
||||
await sidebarRight.toBeVisible();
|
||||
|
||||
// # Write a message in the thread
|
||||
await sidebarRight.postMessage('Replying to a thread');
|
||||
await sidebarRight.sendMessage();
|
||||
|
||||
// # Write a message in the reply thread but don't send it
|
||||
await sidebarRight.postMessage('I should be in drafts');
|
||||
|
||||
// # Close the RHS for draft to be saved
|
||||
await sidebarRight.close()
|
||||
|
||||
// * Verify drafts link in channel sidebar is visible
|
||||
await channelPage.sidebarLeft.draftsVisible();
|
||||
|
||||
// # Click on the dot menu of the post and select delete
|
||||
await post.hover();
|
||||
await post.postMenu.toBeVisible();
|
||||
await post.postMenu.openDotMenu();
|
||||
await channelPage.postDotMenu.toBeVisible()
|
||||
await channelPage.postDotMenu.delete();
|
||||
|
||||
// # Confirm the delete from the modal
|
||||
await channelPage.deletePostModal.toBeVisible();
|
||||
await channelPage.deletePostModal.confirm();
|
||||
|
||||
// * Verify drafts link in channel sidebar is visible
|
||||
await channelPage.sidebarLeft.draftsNotVisible();
|
||||
});
|
||||
Ссылка в новой задаче
Block a user