[MM-53127] e2e-tests : Drafts badge doesn't clear when parent message deleted and removed (#23705)

Этот коммит содержится в:
M-ZubairAhmed
2023-07-05 17:13:55 +05:30
коммит произвёл GitHub
родитель 33565dc118
Коммит b153a39d3e
14 изменённых файлов: 440 добавлений и 9 удалений

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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};

Просмотреть файл

@@ -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();
});