From b8944f372d9dd64c61cec853737a7de14df774fd Mon Sep 17 00:00:00 2001 From: yasser khan Date: Wed, 19 Nov 2025 15:46:57 +0530 Subject: [PATCH] Feat(e2e): Add tests cases for Content Flagging (#34288) - E2E tests for Content Flagging - Fixes tests failing on master --- .../playwright/lib/src/browser_context.ts | 12 +- e2e-tests/playwright/lib/src/global_setup.ts | 2 +- e2e-tests/playwright/lib/src/server/init.ts | 4 +- e2e-tests/playwright/lib/src/server/team.ts | 9 +- e2e-tests/playwright/lib/src/server/user.ts | 6 +- e2e-tests/playwright/lib/src/test_fixture.ts | 3 + .../src/ui/components/channels/center_view.ts | 20 + .../configuration_settings.ts | 11 +- .../channels/flag_post_confirmation_dialog.ts | 86 ++++ .../ui/components/channels/post_dot_menu.ts | 6 + .../lib/src/ui/components/global_header.ts | 7 + .../playwright/lib/src/ui/components/index.ts | 3 + .../lib/src/ui/pages/content_review_dm.ts | 183 ++++++++ .../playwright/lib/src/ui/pages/index.ts | 3 + e2e-tests/playwright/lib/src/util.ts | 26 +- .../specs/client/upload_file.spec.ts | 10 +- .../profile/popover_fields.spec.ts | 2 +- .../channel_banner/channel_banner.spec.ts | 4 +- ...thor-deletes-message-before-review.spec.ts | 49 +++ ...author-edits-message-during-review.spec.ts | 72 ++++ .../flagging/flag-messages.spec.ts | 400 ++++++++++++++++++ .../notifications/author-notification.spec.ts | 90 ++++ .../reporter-notification.spec.ts | 76 ++++ .../reviewer-actions/reviewer-actions.spec.ts | 62 +++ ...team-flag-reports-global-reviewers.spec.ts | 74 ++++ ...ltiple-reviewers-receive-same-flag.spec.ts | 45 ++ .../channels/content_flagging/support.ts | 127 ++++++ .../mentions/multiple_mentions.spec.ts | 2 +- ..._appears_and_scrollable_in_the_rhs.spec.ts | 2 +- .../scheduled_messages.spec.ts | 2 +- .../channels/search/team_selector.spec.ts | 4 +- .../highlight_without_notification.spec.ts | 7 +- .../permissions/team_access.spec.ts | 2 +- .../system_users/actions.spec.ts | 10 +- .../system_users/column_sort.spec.ts | 4 +- .../system_users/filter_popover.spec.ts | 16 +- .../system_users/search.spec.ts | 20 +- e2e-tests/playwright/utils/utils.ts | 10 +- 38 files changed, 1402 insertions(+), 69 deletions(-) create mode 100644 e2e-tests/playwright/lib/src/ui/components/channels/flag_post_confirmation_dialog.ts create mode 100644 e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-deletes-message-before-review.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-edits-message-during-review.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/flagging/flag-messages.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/author-notification.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/reporter-notification.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/cross-team-flag-reports-global-reviewers.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/multiple-reviewers-receive-same-flag.spec.ts create mode 100644 e2e-tests/playwright/specs/functional/channels/content_flagging/support.ts diff --git a/e2e-tests/playwright/lib/src/browser_context.ts b/e2e-tests/playwright/lib/src/browser_context.ts index bbf3dcc80ec..2d04ed00964 100644 --- a/e2e-tests/playwright/lib/src/browser_context.ts +++ b/e2e-tests/playwright/lib/src/browser_context.ts @@ -38,10 +38,20 @@ export class TestBrowser { const scheduledPostsPage = new pages.ScheduledPostsPage(page); const draftsPage = new pages.DraftsPage(page); const threadsPage = new pages.ThreadsPage(page); + const contentReviewPage = new pages.ContentReviewPage(page); this.context = context; - return {context, page, channelsPage, systemConsolePage, scheduledPostsPage, draftsPage, threadsPage}; + return { + context, + page, + channelsPage, + systemConsolePage, + scheduledPostsPage, + draftsPage, + threadsPage, + contentReviewPage, + }; } async close() { diff --git a/e2e-tests/playwright/lib/src/global_setup.ts b/e2e-tests/playwright/lib/src/global_setup.ts index d6441504abc..f58df95047b 100644 --- a/e2e-tests/playwright/lib/src/global_setup.ts +++ b/e2e-tests/playwright/lib/src/global_setup.ts @@ -45,7 +45,7 @@ async function sysadminSetup(client: Client4, user: UserProfile | null) { const myTeams = await client.getMyTeams(); const myDefaultTeam = myTeams && myTeams.length > 0 && myTeams.find((team) => team.name === defaultTeam.name); if (!myDefaultTeam) { - await client.createTeam(createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false)); + await client.createTeam(await createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false)); } else if (myDefaultTeam && testConfig.resetBeforeTest) { await Promise.all( myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)), diff --git a/e2e-tests/playwright/lib/src/server/init.ts b/e2e-tests/playwright/lib/src/server/init.ts index 9dccf71c23b..882b54bfa5a 100644 --- a/e2e-tests/playwright/lib/src/server/init.ts +++ b/e2e-tests/playwright/lib/src/server/init.ts @@ -33,10 +33,10 @@ export async function initSetup({ const adminConfig = await adminClient.updateConfig(getOnPremServerConfig() as any); // Create new team - const team = await adminClient.createTeam(createRandomTeam(teamPrefix.name, teamPrefix.displayName)); + const team = await adminClient.createTeam(await createRandomTeam(teamPrefix.name, teamPrefix.displayName)); // Create new user and add to newly created team - const randomUser = createRandomUser(userPrefix); + const randomUser = await createRandomUser(userPrefix); const user = await adminClient.createUser(randomUser, '', ''); user.password = randomUser.password; await adminClient.addToTeam(team.id, user.id); diff --git a/e2e-tests/playwright/lib/src/server/team.ts b/e2e-tests/playwright/lib/src/server/team.ts index 6e0be52463a..6ea48c1ba32 100644 --- a/e2e-tests/playwright/lib/src/server/team.ts +++ b/e2e-tests/playwright/lib/src/server/team.ts @@ -5,8 +5,13 @@ import {Team, TeamType} from '@mattermost/types/teams'; import {getRandomId} from '@/util'; -export function createRandomTeam(name = 'team', displayName = 'Team', type: TeamType = 'O', unique = true): Team { - const randomSuffix = getRandomId(); +export async function createRandomTeam( + name = 'team', + displayName = 'Team', + type: TeamType = 'O', + unique = true, +): Promise { + const randomSuffix = await getRandomId(); const team = { name: unique ? `${name}-${randomSuffix}` : name, diff --git a/e2e-tests/playwright/lib/src/server/user.ts b/e2e-tests/playwright/lib/src/server/user.ts index ff81eda854b..d5c6569c242 100644 --- a/e2e-tests/playwright/lib/src/server/user.ts +++ b/e2e-tests/playwright/lib/src/server/user.ts @@ -10,7 +10,7 @@ import {testConfig} from '@/test_config'; import {REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY, REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY} from '@/constant'; export async function createNewUserProfile(client: Client4, prefix = 'user') { - const randomUser = createRandomUser(prefix); + const randomUser = await createRandomUser(prefix); const newUser = await client.createUser(randomUser, '', ''); newUser.password = randomUser.password; @@ -18,8 +18,8 @@ export async function createNewUserProfile(client: Client4, prefix = 'user') { return newUser; } -export function createRandomUser(prefix = 'user') { - const randomId = getRandomId(); +export async function createRandomUser(prefix = 'user') { + const randomId = await getRandomId(); const user = { email: `${prefix}${randomId}@sample.mattermost.com`, diff --git a/e2e-tests/playwright/lib/src/test_fixture.ts b/e2e-tests/playwright/lib/src/test_fixture.ts index e25dec479ed..f6d6273addd 100644 --- a/e2e-tests/playwright/lib/src/test_fixture.ts +++ b/e2e-tests/playwright/lib/src/test_fixture.ts @@ -26,6 +26,7 @@ import { getAdminClient, initSetup, isOutsideRemoteUserHour, + makeClient, mergeWithOnPremServerConfig, } from './server'; import { @@ -102,6 +103,7 @@ export class PlaywrightExtended { // ./server readonly createNewUserProfile; readonly isOutsideRemoteUserHour; + readonly makeClient; // ./visual readonly matchSnapshot; @@ -165,6 +167,7 @@ export class PlaywrightExtended { // ./server this.createNewUserProfile = createNewUserProfile; + this.makeClient = makeClient; // ./visual this.matchSnapshot = matchSnapshot; diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts b/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts index 33c47df5219..530e66ab122 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts @@ -8,6 +8,7 @@ import ChannelsPostCreate from './post_create'; import ChannelsPostEdit from './post_edit'; import ChannelsPost from './post'; import ScheduledPostIndicator from './scheduled_post_indicator'; +import FlagPostConfirmationDialog from './flag_post_confirmation_dialog'; import {duration, hexToRgb} from '@/util'; import {waitUntil} from '@/test_action'; @@ -23,6 +24,9 @@ export default class ChannelsCenterView { readonly postEdit; readonly editedPostIcon; readonly channelBanner; + readonly flagPostConfirmationDialog; + readonly messageDeleted; + readonly postText; constructor(container: Locator, page: Page) { this.container = container; @@ -35,6 +39,13 @@ export default class ChannelsCenterView { this.scheduledPostIndicator = new ScheduledPostIndicator(container.getByTestId('scheduledPostIndicator')); this.editedPostIcon = (postID: string) => container.locator(`#postEdited_${postID}`); this.channelBanner = container.getByTestId('channel_banner_container'); + this.flagPostConfirmationDialog = new FlagPostConfirmationDialog( + page.locator('#FlagPostModal div.modal-content'), + page, + ); + this.messageDeleted = (postId: string) => + this.container.locator(`#${postId}_message >> text=(message deleted)`); + this.postText = (postID: string) => this.container.locator(`#postMessageText_${postID}`); } async toBeVisible() { @@ -165,4 +176,13 @@ export default class ChannelsCenterView { const actualText = await strikethroughText.textContent(); expect(actualText).toBe(text); } + + async messageDeletedVisible(isVisible: boolean = false, postId: string, message: string) { + await expect(this.messageDeleted(postId)).toBeVisible({visible: isVisible}); + if (!isVisible) { + const postMessageText = this.postText(postId); + const postMessageTextContent = await postMessageText.textContent(); + expect(postMessageTextContent).toBe(message); + } + } } diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/channel_settings/configuration_settings.ts b/e2e-tests/playwright/lib/src/ui/components/channels/channel_settings/configuration_settings.ts index dcfcb22c154..81a3e8c503e 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/channel_settings/configuration_settings.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/channel_settings/configuration_settings.ts @@ -21,7 +21,7 @@ export default class ConfigurationSettings { } async enableChannelBanner() { - const toggleButton = await this.container.getByTestId('channelBannerToggle-button'); + const toggleButton = this.container.getByTestId('channelBannerToggle-button'); const classes = await toggleButton.getAttribute('class'); if (!classes?.includes('active')) { await toggleButton.click(); @@ -29,7 +29,7 @@ export default class ConfigurationSettings { } async disableChannelBanner() { - const toggleButton = await this.container.getByTestId('channelBannerToggle-button'); + const toggleButton = this.container.getByTestId('channelBannerToggle-button'); const classes = await toggleButton.getAttribute('class'); if (classes?.includes('active')) { await toggleButton.click(); @@ -37,16 +37,15 @@ export default class ConfigurationSettings { } async setChannelBannerText(text: string) { - const textBox = await this.container.getByTestId('channel_banner_banner_text_textbox'); + const textBox = this.container.getByTestId('channel_banner_banner_text_textbox'); await expect(textBox).toBeVisible(); await textBox.fill(text); } async setChannelBannerTextColor(color: string) { - const colorInput = await this.container.locator( - '#channel_banner_banner_background_color_picker-inputColorValue', - ); + const colorInput = this.container.locator('#channel_banner_banner_background_color_picker-inputColorValue'); await expect(colorInput).toBeVisible(); await colorInput.fill(color); + expect((await colorInput.inputValue()).replace('#', '')).toBe(color); } } diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/flag_post_confirmation_dialog.ts b/e2e-tests/playwright/lib/src/ui/components/channels/flag_post_confirmation_dialog.ts new file mode 100644 index 00000000000..9db68e212c8 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/channels/flag_post_confirmation_dialog.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Locator, expect, Page} from '@playwright/test'; + +export default class FlagPostConfirmationDialog { + readonly page: Page; + readonly container: Locator; + + readonly cancelButton; + readonly flagPostReasonInput; + readonly flagPostCommentInput; + readonly submitButton; + readonly postContainer; + readonly postText; + readonly flagReasonOption; + readonly flagReasonMenuItems; + readonly cannotFlagPostErrorMessage; + readonly requireCommentsErrorMessage; + + constructor(container: Locator, page: Page) { + this.container = container; + this.page = page; + + this.flagPostReasonInput = container.locator('#FlagPostModal__reason'); + this.flagPostCommentInput = container.locator('#FlagPostModal__comment'); + this.cancelButton = container.locator('button.btn.btn-tertiary'); + this.submitButton = container.locator('button.btn-primary.confirm'); + this.postContainer = container.locator('[data-testid="FlagPostModal__post-preview_container"]'); + this.postText = container.locator('div.post-message__text'); + this.flagReasonOption = page.locator('.react-select__menu-list'); + this.flagReasonMenuItems = (reason: string) => + this.flagReasonOption.locator(`div.react-select__option:has-text("${reason}")`); + this.cannotFlagPostErrorMessage = container.locator('div.FlagPostModal__request-error span'); + this.requireCommentsErrorMessage = container.locator('div.AdvancedTextbox__error-message span'); + } + + async fillFlagComment(comment: string) { + await this.flagPostCommentInput.fill(comment); + } + + async selectFlagReason(reason: string) { + // Open the dropdown + await this.flagPostReasonInput.click(); + // Wait for dropdown options to appear and click the desired one + await this.flagReasonOption.waitFor({state: 'visible'}); + await this.flagReasonMenuItems(reason).click(); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await expect(this.cancelButton).toBeVisible(); + await expect(this.submitButton).toBeVisible(); + await expect(this.postContainer).toBeVisible(); + } + + async toContainPostText(message: string) { + await expect(this.postText).toBeVisible(); + await expect(this.postText).toHaveText(message); + } + + async notToBeVisible() { + await expect(this.container).not.toBeVisible(); + await expect(this.cancelButton).not.toBeVisible(); + await expect(this.submitButton).not.toBeVisible(); + } + + async cannotFlagAlreadyFlaggedPostToBeVisible() { + await expect(this.cannotFlagPostErrorMessage).toBeVisible(); + await expect(this.cannotFlagPostErrorMessage).toHaveText('Cannot flag this post as it is already flagged.'); + } + + async requireCommentsForFlaggingPost() { + await expect(this.requireCommentsErrorMessage).toBeVisible(); + await expect(this.requireCommentsErrorMessage).toHaveText( + 'Please add a comment explaining why you’re flagging this message.', + ); + } + + async cannotFlagPreviouslyRetainedPostToBeVisible() { + await expect(this.cannotFlagPostErrorMessage).toBeVisible(); + await expect(this.cannotFlagPostErrorMessage).toHaveText( + 'Cannot flag this post as it was retained in a previous flagging request.', + ); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/post_dot_menu.ts b/e2e-tests/playwright/lib/src/ui/components/channels/post_dot_menu.ts index 8fd77e6a843..8cf88986827 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/post_dot_menu.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/post_dot_menu.ts @@ -20,6 +20,7 @@ export default class PostDotMenu { readonly editMenuItem; readonly copyTextMenuItem; readonly deleteMenuItem; + readonly flagMessageMenuItem; constructor(container: Locator) { this.container = container; @@ -40,9 +41,14 @@ export default class PostDotMenu { this.editMenuItem = getMenuItem('Edit'); this.copyTextMenuItem = getMenuItem('Copy Text'); this.deleteMenuItem = getMenuItem('Delete'); + this.flagMessageMenuItem = getMenuItem('Flag Message'); } async toBeVisible() { await expect(this.container).toBeVisible(); } + + async flagMessageMenuItemNotToBeVisible() { + await expect(this.flagMessageMenuItem).not.toBeVisible(); + } } diff --git a/e2e-tests/playwright/lib/src/ui/components/global_header.ts b/e2e-tests/playwright/lib/src/ui/components/global_header.ts index 4a30580e9d5..ee7b1c42187 100644 --- a/e2e-tests/playwright/lib/src/ui/components/global_header.ts +++ b/e2e-tests/playwright/lib/src/ui/components/global_header.ts @@ -15,6 +15,7 @@ export default class GlobalHeader { readonly savedMessagesButton; readonly settingsButton; readonly searchBox; + readonly userProfileMenu; constructor(channelsPage: ChannelsPage, container: Locator) { this.channelsPage = channelsPage; @@ -26,6 +27,7 @@ export default class GlobalHeader { this.savedMessagesButton = container.getByRole('button', {name: 'Saved messages'}); this.settingsButton = container.getByRole('button', {name: 'Settings'}); this.searchBox = container.locator('#searchFormContainer'); + this.userProfileMenu = container.locator('#userAccountMenuButton'); } async toBeVisible(name: string) { @@ -56,6 +58,11 @@ export default class GlobalHeader { await this.searchBox.click(); } + async openUserProfileMenu() { + await expect(this.userProfileMenu).toBeVisible(); + await this.userProfileMenu.click(); + } + async closeSearch() { await expect(this.searchBox).toBeVisible(); await this.searchBox.getByTestId('searchBoxClose').click(); diff --git a/e2e-tests/playwright/lib/src/ui/components/index.ts b/e2e-tests/playwright/lib/src/ui/components/index.ts index 3be334b3376..45e40d1b581 100644 --- a/e2e-tests/playwright/lib/src/ui/components/index.ts +++ b/e2e-tests/playwright/lib/src/ui/components/index.ts @@ -48,6 +48,7 @@ import ScheduledPost from './channels/scheduled_post'; import SendMessageNowModal from './channels/send_message_now_modal'; import DeleteScheduledPostModal from './channels/delete_scheduled_post_modal'; import DraftPost from './channels/draft_post'; +import FlagPostConfirmationDialog from './channels/flag_post_confirmation_dialog'; const components = { GlobalHeader, @@ -63,6 +64,7 @@ const components = { ChannelSettingsModal, DraftPost, FindChannelsModal, + FlagPostConfirmationDialog, DeletePostModal, DeleteScheduledPostModal, InvitePeopleModal, @@ -114,6 +116,7 @@ export { ChannelSettingsModal, DraftPost, FindChannelsModal, + FlagPostConfirmationDialog, DeletePostModal, DeleteScheduledPostModal, InvitePeopleModal, diff --git a/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts b/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts new file mode 100644 index 00000000000..7346c5911fd --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts @@ -0,0 +1,183 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Page, Locator, expect} from '@playwright/test'; + +import {wait} from '@/util'; + +export default class ContentReviewPage { + private readonly page: Page; + private readonly cards: Locator; + private readonly rhsCard: Locator; + private reportCard?: Locator; + readonly keepMessageButton: Locator; + readonly removeMessageButton: Locator; + readonly postActionConformationModal: Locator; + readonly cancelButton: Locator; + readonly confirmRemoveMessageButton: Locator; + readonly confirmKeepMessageButton: Locator; + readonly confirmationModalComment: Locator; + + constructor(page: Page) { + this.page = page; + this.cards = page.locator('[data-testid="property-card-view"]'); + this.rhsCard = page.getByTestId('rhsPostView').getByTestId('property-card-view'); + this.keepMessageButton = this.rhsCard.getByTestId('data-spillage-action-keep-message'); + this.removeMessageButton = this.rhsCard.getByTestId('data-spillage-action-remove-message'); + this.postActionConformationModal = page.locator('div.GenericModal__wrapper'); + this.cancelButton = this.postActionConformationModal.getByRole('button', {name: 'Cancel'}); + this.confirmRemoveMessageButton = this.postActionConformationModal.getByRole('button', { + name: 'Remove message', + }); + this.confirmKeepMessageButton = this.postActionConformationModal.getByRole('button', {name: 'Keep message'}); + this.confirmationModalComment = this.postActionConformationModal.getByTestId( + 'RemoveFlaggedMessageConfirmationModal__comment', + ); + } + + async setReportCardByPostID(postID: string) { + this.reportCard = this.page + .locator('div.DataSpillageReport') + .filter({has: this.page.locator(`#postMessageText_${postID}`)}); + } + + private ensureReportCardSet() { + if (!this.reportCard) { + throw new Error('Report card not set. Call setReportCardByPostID(postID) first.'); + } + } + + async openViewDetails() { + this.ensureReportCardSet(); + const button = this.reportCard!.locator('button:has-text("View Details")'); + await button.scrollIntoViewIfNeeded(); + await button.click(); + } + + async waitForPageLoaded() { + await this.page.waitForResponse( + (res) => res.url().includes('as_content_reviewer=true') && res.status() === 200, + ); + await this.page.waitForTimeout(1000); + await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + this.ensureReportCardSet(); + await expect(this.reportCard!).toBeVisible(); + } + + async getLastCard(): Promise { + const count = await this.cards.count(); + if (count === 0) throw new Error('No content review cards found.'); + return this.cards.nth(count - 1); + } + + async openCardByMessage(message: string) { + const targetCard = this.page + .locator('div.DataSpillageReport') + .filter({has: this.page.locator(`.row:has-text("${message}")`)}); + await targetCard.first().click(); + } + + private field(fieldName: string): Locator { + return this.rhsCard.locator('.row', { + has: this.rhsCard.locator(`.field:has-text("${fieldName}")`), + }); + } + + /** + * Gets the value text for a given field label (e.g. "Status", "Reason", etc.) + */ + async getValueForField(fieldName: string): Promise { + await expect(this.rhsCard).toBeVisible({timeout: 10000}); + const valueLocator = this.rhsCard.locator(`.row:has(.field:has-text("${fieldName}")) .value`); + await expect(valueLocator).toBeVisible({timeout: 5000}); + return valueLocator.innerText(); + } + + /** + * Asserts that a field's value matches the expected text + */ + async expectSelectProperty(fieldName: string, expectedValue: string): Promise { + const actualValue = await this.getValueForField(fieldName); + expect(actualValue.trim()).toBe(expectedValue); + } + + async expectTextProperty(fieldName: string, expected: string) { + await expect(this.field(fieldName).locator('.TextProperty')).toHaveText(expected); + } + + async expectUser(fieldName: string, expected: string) { + await expect(this.rhsCard).toBeVisible({timeout: 10000}); + + const userButton = this.rhsCard.locator(`.row:has(.field:has-text("${fieldName}")) .user-popover`); + + // Wait for either visible or attached then read text + await userButton.waitFor({state: 'attached', timeout: 10000}); + const text = (await userButton.innerText()).trim(); + expect(text).toBe(expected); + } + + async expectTeam(expected: string) { + await expect(this.rhsCard.locator('.TeamPropertyRenderer')).toContainText(expected); + } + + async expectChannel(expected: string) { + await expect(this.rhsCard.locator('.ChannelPropertyRenderer')).toContainText(expected); + } + + async expectMessageContains(expected: string) { + await expect(this.rhsCard.locator('.post-message__text')).toContainText(expected); + } + + async waitForRHSVisible() { + await this.page.waitForResponse( + (res) => res.url().includes('as_content_reviewer=true') && res.status() === 200, + ); + + const gotIt = this.page.getByRole('button', {name: 'Got it'}); + if (await gotIt.isVisible()) { + await gotIt.click(); + } + await wait(5000); + } + + async verifyFlaggedPostStatus(expected: string) { + this.ensureReportCardSet(); + await expect(this.reportCard!.locator('.row:has-text("Status") .SelectProperty')).toHaveText(expected); + } + + async verifyFlaggedPostReason(expected: string) { + this.ensureReportCardSet(); + await expect(this.reportCard!.locator('.row:has-text("Reason") .SelectProperty')).toHaveText(expected); + } + + async verifyFlaggedPostMessage(expected: string) { + this.ensureReportCardSet(); + await expect(this.reportCard!.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected); + } + + async clickKeepMessage() { + await this.keepMessageButton.scrollIntoViewIfNeeded(); + await this.keepMessageButton.click(); + await this.postActionConformationModal.waitFor({state: 'visible'}); + } + + async clickRemoveMessage() { + await this.removeMessageButton.scrollIntoViewIfNeeded(); + await this.removeMessageButton.click(); + await this.postActionConformationModal.waitFor({state: 'visible'}); + } + + async enterConfirmationComment(comment: string) { + await this.confirmationModalComment.fill(comment); + } + + async confirmRemove() { + await this.confirmRemoveMessageButton.click(); + await this.postActionConformationModal.waitFor({state: 'hidden'}); + } + + async confirmKeep() { + await this.confirmKeepMessageButton.click(); + await this.postActionConformationModal.waitFor({state: 'hidden'}); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/pages/index.ts b/e2e-tests/playwright/lib/src/ui/pages/index.ts index 624199b77c0..7c72fa7e0e3 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/index.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/index.ts @@ -10,6 +10,7 @@ import SystemConsolePage from './system_console'; import ScheduledPostsPage from './scheduled_posts'; import DraftsPage from './drafts'; import ThreadsPage from './threads'; +import ContentReviewPage from './content_review_dm'; const pages = { ChannelsPage, @@ -18,6 +19,7 @@ const pages = { ResetPasswordPage, SignupPage, ScheduledPostsPage, + ContentReviewPage, SystemConsolePage, DraftsPage, ThreadsPage, @@ -26,6 +28,7 @@ const pages = { export { pages, ChannelsPage, + ContentReviewPage, DraftsPage, LandingLoginPage, LoginPage, diff --git a/e2e-tests/playwright/lib/src/util.ts b/e2e-tests/playwright/lib/src/util.ts index b6d3949212d..1d59ad10432 100644 --- a/e2e-tests/playwright/lib/src/util.ts +++ b/e2e-tests/playwright/lib/src/util.ts @@ -4,8 +4,16 @@ import path from 'node:path'; import fs from 'node:fs'; -import {v4 as uuidv4} from 'uuid'; +// Lazy-load the ESM-only uuid package dynamically +let uuidv4: (() => string) | null = null; +async function loadUuid() { + if (!uuidv4) { + const {v4} = await import('uuid'); + uuidv4 = v4; + } + return uuidv4!; +} const second = 1000; const minute = 60 * 1000; @@ -26,18 +34,19 @@ export const duration = { * @param {number} ms - duration in millisecond * @return {Promise} promise with timeout */ -export const wait = async (ms = 0) => { +export const wait = async (ms = 0): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; /** - * @param {Number} length - length on random string to return, e.g. 7 (default) - * @return {String} random string + * Generate a random ID string. + * Because uuid is dynamically loaded, this is async. */ -export function getRandomId(length = 7): string { +export async function getRandomId(length = 7): Promise { const MAX_SUBSTRING_INDEX = 27; - return uuidv4() + const v4 = await loadUuid(); + return v4() .replace(/-/g, '') .substring(MAX_SUBSTRING_INDEX - length, MAX_SUBSTRING_INDEX); } @@ -51,15 +60,10 @@ export const illegalRe = /[/?<>\\:*|":&();]/g; export const simpleEmailRe = /\S+@\S+\.\S+/; export function hexToRgb(hex: string): string { - // Remove the # if present hex = hex.replace(/^#/, ''); - - // Parse the hex values const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); - - // Return the RGB string return `rgb(${r}, ${g}, ${b})`; } diff --git a/e2e-tests/playwright/specs/client/upload_file.spec.ts b/e2e-tests/playwright/specs/client/upload_file.spec.ts index f333d225c61..c8016ba1b0f 100644 --- a/e2e-tests/playwright/specs/client/upload_file.spec.ts +++ b/e2e-tests/playwright/specs/client/upload_file.spec.ts @@ -27,7 +27,7 @@ test.beforeEach(async ({pw}) => { test('should succeed with File', async ({pw}) => { // # Prepare data with File - const clientId = pw.random.id(); + const clientId = await pw.random.id(); const formData = new FormData(); formData.set('channel_id', townSquareChannel.id); formData.set('client_ids', clientId); @@ -40,7 +40,7 @@ test('should succeed with File', async ({pw}) => { test('should succeed with Blob', async ({pw}) => { // # Prepare data with Blob - const clientId = pw.random.id(); + const clientId = await pw.random.id(); const formData = new FormData(); formData.set('channel_id', townSquareChannel.id); formData.set('client_ids', clientId); @@ -69,7 +69,7 @@ test('should succeed even with channel_id only', async () => { }); test('should fail on invalid channel ID', async ({pw}) => { - const clientId = pw.random.id(); + const clientId = await pw.random.id(); // # Set with invalid channel ID let formData = new FormData(); @@ -92,7 +92,7 @@ test('should fail on invalid channel ID', async ({pw}) => { }); test('should fail on missing files', async ({pw}) => { - const clientId = pw.random.id(); + const clientId = await pw.random.id(); // # Set with invalid channel ID const formData = new FormData(); @@ -105,7 +105,7 @@ test('should fail on missing files', async ({pw}) => { }); test('should fail on incorrect order setting up FormData', async ({pw}) => { - const clientId = pw.random.id(); + const clientId = await pw.random.id(); // # Set with files before client_ids const formData = new FormData(); diff --git a/e2e-tests/playwright/specs/functional/channels/account_settings/profile/popover_fields.spec.ts b/e2e-tests/playwright/specs/functional/channels/account_settings/profile/popover_fields.spec.ts index 3dd8a9d5602..7121c3ad7ee 100644 --- a/e2e-tests/playwright/specs/functional/channels/account_settings/profile/popover_fields.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/account_settings/profile/popover_fields.spec.ts @@ -22,7 +22,7 @@ test('Profile popover should show correct fields after at-mention autocomplete @ }); // Create and add another user using admin client - const testUser2 = await adminClient.createUser(pw.random.user('other'), '', ''); + const testUser2 = await adminClient.createUser(await pw.random.user('other'), '', ''); await adminClient.addToTeam(team.id, testUser2.id); // 1. Login as the first user diff --git a/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts b/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts index c86fce43aa6..d8d47ab3f59 100644 --- a/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/channel_banner/channel_banner.spec.ts @@ -13,7 +13,7 @@ test('Should show channel banner when configured', async ({pw}) => { await channelsPage.goto(); await channelsPage.toBeVisible(); - await channelsPage.newChannel(getRandomId(), 'O'); + await channelsPage.newChannel(await getRandomId(), 'O'); let channelSettingsModal = await channelsPage.openChannelSettings(); let configurationTab = await channelSettingsModal.openConfigurationTab(); @@ -58,7 +58,7 @@ test('Should render markdown', async ({pw}) => { await channelsPage.goto(); await channelsPage.toBeVisible(); - await channelsPage.newChannel(getRandomId(), 'O'); + await channelsPage.newChannel(await getRandomId(), 'O'); const channelSettingsModal = await channelsPage.openChannelSettings(); const configurationTab = await channelSettingsModal.openConfigurationTab(); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-deletes-message-before-review.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-deletes-message-before-review.spec.ts new file mode 100644 index 00000000000..0241a14afd8 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-deletes-message-before-review.spec.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +import {createPost, verifyAuthorNotification, setupContentFlagging} from './../support'; + +/** + * @objective Verify that when the author deletes a flagged message before review, + * the flag status is updated to "Removed" and the report reflects the deletion. + */ +// TODO: Fix defect https://mattermost.atlassian.net/browse/MM-66342 +test.skip('should not be able to restore flagged messages when author deletes message', async ({pw}) => { + const {adminClient, team, user: reviewerUser} = await pw.initSetup(); + // Create second user and add to team + const reporterUser = await pw.random.user('second'); + const {id: reporterUserID} = await adminClient.createUser(reporterUser, '', ''); + await adminClient.addToTeam(team.id, reporterUserID); + + // Create third user and add to team + const postFromThirdUser = await pw.random.user('third'); + const {id: postFromThirdUserID} = await adminClient.createUser(postFromThirdUser, '', ''); + await adminClient.addToTeam(team.id, postFromThirdUserID); + + const {client: thirdUserClient} = await pw.makeClient(postFromThirdUser); + const {client: reporterUserClient} = await pw.makeClient(reporterUser); + + await setupContentFlagging(adminClient, [reviewerUser.id], true, false); + const message = `Post by @${reviewerUser.username}, is flagged once`; + + const {post} = await createPost(adminClient, thirdUserClient, team, postFromThirdUser, message); + + await reporterUserClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + + // delete the post as the author + await thirdUserClient.deletePost(post.id); + + // verify the flag status is updated to "Removed" + const flagReport = await adminClient.getFlaggedPost(post.id); + + // Verify the delete_at timestamp is set (indicating deletion) + expect(flagReport.delete_at).not.toBe(0); + + const {channelsPage, contentReviewPage} = await pw.testBrowser.login(reviewerUser); + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + await verifyAuthorNotification(post.id, channelsPage, contentReviewPage, team.name, message, 'Removed'); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-edits-message-during-review.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-edits-message-during-review.spec.ts new file mode 100644 index 00000000000..f3a87e3dc43 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/edge-cases/author-edits-message-during-review.spec.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; + +import {createPost, verifyAuthorNotification, setupContentFlagging} from './../support'; + +/** @objective Verify Post message is updated for the reviewer, if author updates the post before reviewer\'s action + * @testcase + * 1. Setup Content Flagging with reviewers + * 2. Create a post by User A + * 3. Flag the post by User B + * 4. Edit the post by User A before reviewer's action + * 5. Login as Reviewer and verify the updated message in Content Review page + */ +test("Verify Post message is updated for the reviewer, if author updates the post before reviewer's action ", async ({ + pw, +}) => { + const {adminClient, team, user, userClient, adminUser} = await pw.initSetup(); + + // Create second user and add to team + const secondUser = await pw.random.user('reviewer'); + const {id: secondUserID} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(team.id, secondUserID); + + // Setup content flagging *after* roles are set + await setupContentFlagging(adminClient, [adminUser.id, secondUserID], true, false); + + const message = `Post by @${user.username}, is flagged once`; + + const {post} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + + let updatedMessage = `${message} - Edited during review`; + await userClient.updatePost({ + id: post.id, + create_at: post.create_at, + update_at: Date.now(), + edit_at: 0, + delete_at: 0, + is_pinned: false, + user_id: post.user_id, + channel_id: post.channel_id, + root_id: '', + original_id: '', + message: updatedMessage, + type: '', + props: {}, + hashtags: '', + file_ids: [], + pending_post_id: '', + remote_id: '', + reply_count: 0, + last_reply_at: 0, + participants: null, + metadata: post.metadata, + }); + + const {channelsPage: secondChannelsPage, contentReviewPage: secondContentReviewPage} = + await pw.testBrowser.login(secondUser); + + // The edited post will have Edited indicator automatically added by the system + updatedMessage = `${updatedMessage} Edited`; + await verifyAuthorNotification( + post.id, + secondChannelsPage, + secondContentReviewPage, + team.name, + updatedMessage, + 'Pending', + ); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/flagging/flag-messages.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/flagging/flag-messages.spec.ts new file mode 100644 index 00000000000..6a45e7738c2 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/flagging/flag-messages.spec.ts @@ -0,0 +1,400 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +// Constants for repeated strings +const FLAG_REASON_INAPPROPRIATE: string = 'Inappropriate Content'; +const FLAG_REASON_INAPPROPRIATE_ALT: string = 'Inappropriate content'; +const FLAG_COMMENT: string = 'This message is inappropriate'; +const SYSTEM_MESSAGE = (username: string): string => + `The message from @${username} has been flagged for review. You will be notified once it is reviewed by a Content Reviewer. `; + +// Helper to login and navigate to channel +async function loginAndNavigate(pw: any, user: any, teamName?: string, channelName?: string): Promise { + const {channelsPage} = await pw.testBrowser.login(user); + if (teamName && channelName) { + await channelsPage.goto(teamName, channelName); + } else { + await channelsPage.goto(); + } + await channelsPage.toBeVisible(); + return channelsPage; +} + +// Helper to post a message and get post info +async function postMessage(channelsPage: any, message: string): Promise<{post: any; postId: any}> { + await channelsPage.postMessage(message); + const post = await channelsPage.getLastPost(); + const postId = await channelsPage.centerView.getLastPostID(); + return {post, postId}; +} + +// Helper to flag a post +async function flagPostFlow( + post: any, + channelsPage: any, + message: string, + reason: string = FLAG_REASON_INAPPROPRIATE, + comment: string = FLAG_COMMENT, +): Promise { + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.selectFlagReason(reason); + await channelsPage.centerView.flagPostConfirmationDialog.fillFlagComment(comment); + await channelsPage.centerView.flagPostConfirmationDialog.submitButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.notToBeVisible(); +} + +// Helper to open the dot menu for a given post +async function openPostDotMenu(post: any, channelsPage: any): Promise { + await post.hover(); + await post.postMenu.toBeVisible(); + await post.postMenu.dotMenuButton.click(); + await channelsPage.postDotMenu.toBeVisible(); +} + +/** + * @objective: Test the basic flow of flagging a message and verify flagged message is hidden + * + * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Flag the message + * 4. Verify the message is hidden and a system message is shown + */ +test('Verify flagged message is hidden by default', async ({pw}) => { + const {user, adminClient} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + }, + }); + + const channelsPage = await loginAndNavigate(pw, user); + const message = 'This is a test message to be flagged'; + const {post, postId} = await postMessage(channelsPage, message); + + // Cancel flagging the message + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.cancelButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.notToBeVisible(); + + // Flag the message + await flagPostFlow(post, channelsPage, message, FLAG_REASON_INAPPROPRIATE_ALT); + + // Verify the message is flagged + await channelsPage.centerView.messageDeletedVisible(true, postId, message); + const systemMessage = await channelsPage.getLastPost(); + await expect(systemMessage.body).toContainText(SYSTEM_MESSAGE(user.username)); +}); + +/** + * @objective: Verify Post is not hidden after flagging if HideFlaggedContent is false + * + * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Flag the message + * 4. Verify the message is not hidden + */ +test('Verify Post is not hidden after flagging if HideFlaggedContent is false', async ({pw}) => { + const {user, adminClient} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + AdditionalSettings: { + HideFlaggedContent: false, + }, + }, + }); + + const channelsPage = await loginAndNavigate(pw, user); + const message = 'This is a test message to be flagged'; + const {post, postId} = await postMessage(channelsPage, message); + await post.toBeVisible(); + + // Cancel flagging the message + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.cancelButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.notToBeVisible(); + + // Flag the message + await flagPostFlow(post, channelsPage, message); + + // Verify the message is flagged + const originaltext = await channelsPage.centerView.getPostById(postId); + await expect(originaltext.body).toContainText(message); + const systemMessage = await channelsPage.getLastPost(); + await expect(systemMessage.body).toContainText(SYSTEM_MESSAGE(user.username)); +}); + +/** + * @objective: Test that another user cannot flag an already flagged message + * + * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Flag the message + * 4. Login as another user + * 5. Attempt to flag the already flagged message + * 6. Verify that the message cannot be flagged again + */ +test('Verify user cannot flag already flagged message', async ({pw}) => { + const {user, adminClient, team} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + AdditionalSettings: { + HideFlaggedContent: false, + }, + NotificationSettings: { + EventTargetMapping: { + assigned: ['reviewers'], + dismissed: ['reporter', 'author', 'reviewers'], + flagged: ['reviewers'], + removed: ['author', 'reporter', 'reviewers'], + }, + }, + }, + }); + + const secondUser = await pw.random.user('mentioned'); + const {id: secondUserID} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(team.id, secondUserID); + const channels = await adminClient.getMyChannels(team.id); + const townSquare = channels.find((channel) => channel.name === 'town-square'); + if (!townSquare) throw new Error('Town Square channel not found'); + + const message = `Post by @${user.username}, is flagged once`; + const postToBeflagged = await adminClient.createPost({ + channel_id: townSquare.id, + message, + user_id: user.id, + }); + await adminClient.flagPost(postToBeflagged.id, FLAG_REASON_INAPPROPRIATE_ALT, FLAG_COMMENT); + + // Login as the second user + const channelsPage = await loginAndNavigate(pw, secondUser, team.name, 'town-square'); + const post = await channelsPage.getLastPost(); + + // Try to flag already flagged post + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.selectFlagReason(FLAG_REASON_INAPPROPRIATE); + await channelsPage.centerView.flagPostConfirmationDialog.fillFlagComment(FLAG_COMMENT); + await channelsPage.centerView.flagPostConfirmationDialog.submitButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.cannotFlagAlreadyFlaggedPostToBeVisible(); +}); + +/** + * @objective: Test that user cannot flag a message that was previously retained. + * + * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Flag the message + * 4. Retain the message as a reviewer + * 5. Attempt to flag the retained message again + * 6. Verify that the message cannot be flagged again + */ +test('Verify user cannot flag a message that was previously retained', async ({pw}) => { + const {user, adminClient, team} = await pw.initSetup(); + const secondUser = await pw.random.user('mentioned-'); + const {id: secondUserID, username: secondUsername} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(team.id, secondUserID); + const channels = await adminClient.getMyChannels(team.id); + const townSquare = channels.find((channel) => channel.name === 'town-square'); + if (!townSquare) throw new Error('Town Square channel not found'); + + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + AdditionalSettings: { + HideFlaggedContent: false, + }, + NotificationSettings: { + EventTargetMapping: { + assigned: ['reviewers'], + dismissed: ['reporter', 'author', 'reviewers'], + flagged: ['reviewers'], + removed: ['author', 'reporter', 'reviewers'], + }, + }, + ReviewerSettings: { + CommonReviewers: true, + SystemAdminsAsReviewers: true, + TeamAdminsAsReviewers: true, + CommonReviewerIds: [user.id, secondUserID], + }, + }, + }); + + const message = `Post by @${secondUsername}, is flagged once`; + const postToBeflagged = await adminClient.createPost({ + channel_id: townSquare.id, + message, + user_id: secondUserID, + }); + await adminClient.flagPost(postToBeflagged.id, FLAG_REASON_INAPPROPRIATE_ALT, FLAG_COMMENT); + await adminClient.keepFlaggedPost(postToBeflagged.id, 'Retaining this post after review'); + + // Login as the second user + const channelsPage = await loginAndNavigate(pw, secondUser, team.name, 'town-square'); + const post = await channelsPage.getLastPost(); + + // Try to flag previously retained post + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.selectFlagReason(FLAG_REASON_INAPPROPRIATE); + await channelsPage.centerView.flagPostConfirmationDialog.fillFlagComment(FLAG_COMMENT); + await channelsPage.centerView.flagPostConfirmationDialog.submitButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.cannotFlagPreviouslyRetainedPostToBeVisible(); +}); + +/** + * @objective: Test that flag message option is not available when Content Flagging feature is disabled + * * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Verify that flag message option is not available in the post menu + */ +test('Verify the Flag message option is not available when feature is disabled', async ({pw}) => { + const {user, adminClient} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: false, + }, + }); + + const channelsPage = await loginAndNavigate(pw, user); + const message = 'This is a test message to be flagged'; + const {post} = await postMessage(channelsPage, message); + + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItemNotToBeVisible(); +}); + +/** + * @objective: Verify Flagging reason dropdown options + * * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Open flag message dialog + * 4. Verify the flagging reason dropdown options + */ +test('Verify Flagging reason dropdown', async ({pw}) => { + const {user, adminClient, team} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + AdditionalSettings: { + Reasons: ['Spam', FLAG_REASON_INAPPROPRIATE, 'Harassment', 'Hate Speech', 'Other'], + }, + }, + }); + + const channelsPage = await loginAndNavigate(pw, user, team.name, 'town-square'); + const message = 'This is a test message to be flagged'; + const {post} = await postMessage(channelsPage, message); + + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.selectFlagReason('Spam'); +}); + +/** + * @objective: Verify Comments are required for Flagging + * * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Open flag message dialog + * 4. Verify that comments are required for flagging + */ +test('Verify Comments are required for Flagging', async ({pw}) => { + const {user, adminClient, team} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + AdditionalSettings: { + Reasons: ['Spam', FLAG_REASON_INAPPROPRIATE, 'Harassment', 'Hate Speech', 'Other'], + ReporterCommentRequired: true, + }, + }, + }); + + const channelsPage = await loginAndNavigate(pw, user, team.name, 'town-square'); + const message = 'This is a test message to be flagged'; + const {post} = await postMessage(channelsPage, message); + + await openPostDotMenu(post, channelsPage); + await channelsPage.postDotMenu.flagMessageMenuItem.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.toContainPostText(message); + await channelsPage.centerView.flagPostConfirmationDialog.selectFlagReason('Spam'); + await channelsPage.centerView.flagPostConfirmationDialog.submitButton.click(); + await channelsPage.centerView.flagPostConfirmationDialog.toBeVisible(); + await channelsPage.centerView.flagPostConfirmationDialog.requireCommentsForFlaggingPost(); +}); + +/** + * @objective: Verify message is removed from channel if the reviewer removed the message + * + * @testcase + * 1. Login as a user + * 2. Post a message + * 3. Flag the message + * 4. Login as a reviewer and remove the message + * 5. Verify the message is removed from the channel + */ +test('Verify message is removed from channel if the reviewer removed the message', async ({pw}) => { + const {user, adminClient, team} = await pw.initSetup(); + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: true, + ReviewerSettings: { + CommonReviewers: true, + SystemAdminsAsReviewers: true, + TeamAdminsAsReviewers: true, + CommonReviewerIds: [user.id], + }, + AdditionalSettings: { + HideFlaggedContent: false, + }, + }, + }); + + const channels = await adminClient.getMyChannels(team.id); + const townSquare = channels.find((channel) => channel.name === 'town-square'); + if (!townSquare) throw new Error('Town Square channel not found'); + + const message = `Post by @${user.username}, is flagged once`; + const postToBeflagged = await adminClient.createPost({ + channel_id: townSquare.id, + message, + user_id: user.id, + }); + await adminClient.flagPost(postToBeflagged.id, FLAG_REASON_INAPPROPRIATE_ALT, FLAG_COMMENT); + await adminClient.removeFlaggedPost(postToBeflagged.id, 'Removing this post after review'); + + // Login as the user + const channelsPage = await loginAndNavigate(pw, user, team.name, 'town-square'); + const lastPostId = await channelsPage.centerView.getLastPostID(); + expect(lastPostId).not.toBe(postToBeflagged.id); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/author-notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/author-notification.spec.ts new file mode 100644 index 00000000000..036a444eefb --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/author-notification.spec.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +import {createPost} from './../support'; + +async function setupContentFlagging(adminClient: any, userIds: string[], enable = true) { + await adminClient.patchConfig({ + ContentFlaggingSettings: { + EnableContentFlagging: enable, + ReviewerSettings: { + CommonReviewers: true, + SystemAdminsAsReviewers: true, + TeamAdminsAsReviewers: true, + CommonReviewerIds: userIds, + }, + NotificationSettings: { + EventTargetMapping: { + assigned: ['reviewers', 'author'], + dismissed: ['reporter', 'author', 'reviewers'], + flagged: ['reviewers', 'author'], + removed: ['author', 'reporter', 'reviewers'], + }, + }, + AdditionalSettings: { + HideFlaggedContent: true, + }, + }, + }); + return adminClient; +} + +async function verifyAuthorNotification(channelsPage: any, teamName: string, expectedMessage: string) { + await channelsPage.goto(teamName, '@content-review'); + await channelsPage.toBeVisible(); + + const lastPostId = await channelsPage.centerView.getLastPostID(); + const post = await channelsPage.centerView.getPostById(lastPostId); + await expect(post.body).toContainText(expectedMessage); +} +/** + * @objective Verify Author is notified if the post is flagged in a channel + */ +test('Verify Author is notified if the post is flagged in a channel', async ({pw}) => { + const {adminClient, team, user, userClient} = await pw.initSetup(); + await setupContentFlagging(adminClient, [user.id]); + + const message = `Post by @${user.username}, is flagged once`; + const {post, townSquare} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + + const {channelsPage} = await pw.testBrowser.login(user); + const expected = `Your post having ID ${post.id} in the channel ${townSquare.display_name} has been flagged for review.`; + await verifyAuthorNotification(channelsPage, team.name, expected); +}); + +/** + * @objective Verify Author is notified if flagged post is Retained by the reviewer + */ +test('Verify Author is notified if flagged post is Retained in a channel', async ({pw}) => { + const {adminClient, team, user, userClient} = await pw.initSetup(); + await setupContentFlagging(adminClient, [user.id]); + + const message = `Post by @${user.username}, is flagged once`; + const {post, townSquare} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + await adminClient.keepFlaggedPost(post.id, 'Retaining this post after review'); + + const {channelsPage} = await pw.testBrowser.login(user); + const expected = `Your post having ID ${post.id} in the channel ${townSquare.display_name} which was flagged for review has been restored by a reviewer.`; + await verifyAuthorNotification(channelsPage, team.name, expected); +}); + +/** + * @objective Verify Author is notified if flagged post is Removed by the reviewer + */ +test('Verify Author is notified if flagged post is Removed from a channel', async ({pw}) => { + const {adminClient, team, user, userClient} = await pw.initSetup(); + await setupContentFlagging(adminClient, [user.id]); + + const message = `Post by @${user.username}, is flagged once`; + const {post, townSquare} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + await adminClient.removeFlaggedPost(post.id, 'Removing this post after review'); + + const {channelsPage} = await pw.testBrowser.login(user); + const expected = `Your post having ID ${post.id} in the channel ${townSquare.display_name} which was flagged for review has been permanently removed by a reviewer.`; + await verifyAuthorNotification(channelsPage, team.name, expected); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/reporter-notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/reporter-notification.spec.ts new file mode 100644 index 00000000000..04c37247e8a --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/notifications/reporter-notification.spec.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; + +import {setupContentFlagging, createPost, verifyReporterNotification} from './../support'; + +/** + * @objective Verify Reporter is notified if the flagged post is Retained by the reviewer + */ +test('Verify Reporter is notified if flagged post is Retained in a channel', async ({pw}) => { + const {adminClient, team, user: reviewerUser, userClient: reviewerUserClient} = await pw.initSetup(); + + // Create second user and add to team + const reporterUser = await pw.random.user('second'); + const {id: reporterUserID} = await adminClient.createUser(reporterUser, '', ''); + await adminClient.addToTeam(team.id, reporterUserID); + + // Create third user and add to team + const postFromThirdUser = await pw.random.user('third'); + const {id: postFromThirdUserID} = await adminClient.createUser(postFromThirdUser, '', ''); + await adminClient.addToTeam(team.id, postFromThirdUserID); + + const {client: thirdUserClient} = await pw.makeClient(postFromThirdUser); + const {client: reporterUserClient} = await pw.makeClient(reporterUser); + + await setupContentFlagging(adminClient, [reviewerUser.id]); + const message = `Post by @${reviewerUser.username}, is flagged once`; + + const {post, townSquare} = await createPost(adminClient, thirdUserClient, team, postFromThirdUser, message); + + await reporterUserClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + await reviewerUserClient.keepFlaggedPost(post.id, 'Retaining this post after review'); + + const {channelsPage} = await pw.testBrowser.login(reporterUser); + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + const expected = `The post having ID ${post.id} in the channel ${townSquare.display_name} which you flagged for review has been restored by a reviewer.`; + await verifyReporterNotification(channelsPage, team.name, expected); +}); + +/** + * @objective Verify Reporter is notified if flagged post is Removed from a channel + */ +test('Verify Reporter is notified if flagged post is Removed from a channel', async ({pw}) => { + const {adminClient, team, user: reviewerUser, userClient: reviewerUserClient} = await pw.initSetup(); + + // Create second user and add to team + const reporterUser = await pw.random.user('second'); + const {id: reporterUserID} = await adminClient.createUser(reporterUser, '', ''); + await adminClient.addToTeam(team.id, reporterUserID); + + // Create third user and add to team + const postFromThirdUser = await pw.random.user('third'); + const {id: postFromThirdUserID} = await adminClient.createUser(postFromThirdUser, '', ''); + await adminClient.addToTeam(team.id, postFromThirdUserID); + + const {client: thirdUserClient} = await pw.makeClient(postFromThirdUser); + const {client: reporterUserClient} = await pw.makeClient(reporterUser); + + await setupContentFlagging(adminClient, [reviewerUser.id]); + const message = `Post by @${reviewerUser.username}, is flagged once`; + + const {post, townSquare} = await createPost(adminClient, thirdUserClient, team, postFromThirdUser, message); + + await reporterUserClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + await reviewerUserClient.removeFlaggedPost(post.id, 'Retaining this post after review'); + + const {channelsPage} = await pw.testBrowser.login(reporterUser); + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + const expected = `The post having ID ${post.id} in the channel ${townSquare.display_name} which you flagged for review has been permanently removed by a reviewer.`; + await verifyReporterNotification(channelsPage, team.name, expected); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts new file mode 100644 index 00000000000..2d7c0e7068d --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; + +import {setupContentFlagging, createPost, verifyAuthorNotification} from './../support'; + +/** @objective Verify Retained and Removed Flagged posts do not appear in RHS after once reviewed + * @testcase + * 1. Create three users and add them as reviewers to a team + * 2. Setup content flagging with the three users as reviewers + * 3. Create a post and flag it + * 4. As Reviewer 1, Retain the flagged post and verify the status is updated to 'Retained' + * 5. As Reviewer 2, Verify the flagged post status is 'Retained' + */ +test('Verify Removed Flagged posts show appropriate status and do not show the post message', async ({pw}) => { + const {adminClient, team, user, userClient, adminUser} = await pw.initSetup(); + + // Create second user and add to team + const secondUser = await pw.random.user('reviewer'); + const {id: secondUserID} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(team.id, secondUserID); + + // Create third user and add to team + const thirdUser = await pw.random.user('reviewer'); + const {id: thirdUserID} = await adminClient.createUser(thirdUser, '', ''); + await adminClient.addToTeam(team.id, thirdUserID); + + // Setup content flagging *after* roles are set + await setupContentFlagging(adminClient, [adminUser.id, secondUserID, thirdUserID]); + + const message = `Post by @${user.username}, is flagged once`; + + const {post} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + + const {channelsPage: secondChannelsPage, contentReviewPage: secondContentReviewPage} = + await pw.testBrowser.login(secondUser); + await verifyAuthorNotification(post.id, secondChannelsPage, secondContentReviewPage, team.name, message, 'Pending'); + + const commentRemove = 'Removing this message as it violates the guidelines.'; + const contentModerationMessage = 'Content deleted as part of Content Flagging review process'; + await secondContentReviewPage.setReportCardByPostID(post.id); + await secondContentReviewPage.openViewDetails(); + await secondContentReviewPage.waitForRHSVisible(); + + await secondContentReviewPage.openViewDetails(); + await secondContentReviewPage.clickRemoveMessage(); + await secondContentReviewPage.enterConfirmationComment(commentRemove); + await secondContentReviewPage.confirmRemove(); + + const {channelsPage: channelsPageThird, contentReviewPage: contentReviewPageThird} = + await pw.testBrowser.login(thirdUser); + await verifyAuthorNotification( + post.id, + channelsPageThird, + contentReviewPageThird, + team.name, + contentModerationMessage, + 'Removed', + ); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/cross-team-flag-reports-global-reviewers.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/cross-team-flag-reports-global-reviewers.spec.ts new file mode 100644 index 00000000000..340d0f9e2fb --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/cross-team-flag-reports-global-reviewers.spec.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; + +import {createPost, verifyFlaggedPostCardDetails, verifyRHSFlaggedPostDetails} from './../support'; + +/** + * @objective Verify a reviewer from other team can receive a review request for a flagged post + * @testcase + * 1. Create two teams and users + * 2. Setup content flagging with reviewers from both teams + * 3. Create a post in team A and flag it + * 4. Verify that a reviewer from team B receives a review request in Content Review channel + * 5. Verify the flagged post details in the reviewer's Content Review DM and RHS + */ +test('Verify reviewer from another team can receive a review request for a flagged post', async ({pw}) => { + const reasonToFlag = 'Inappropriate content'; + const flagPostReviewStatus = 'Pending'; + const flagPostComment = 'This message is inappropriate'; + + const {adminClient, team, user, userClient, adminUser} = await pw.initSetup(); + const secondTeam = await userClient.createTeam(await pw.random.team('team', 'Team', 'O', true)); + + const secondUser = await pw.random.user('mentioned'); + const {id: secondUserID} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(secondTeam.id, secondUserID); + + // Configure content flagging + await adminClient.saveContentFlaggingConfig({ + EnableContentFlagging: true, + NotificationSettings: { + EventTargetMapping: { + assigned: ['reviewers', 'author'], + dismissed: ['reporter', 'author', 'reviewers'], + flagged: ['reviewers', 'author'], + removed: ['author', 'reporter', 'reviewers'], + }, + }, + ReviewerSettings: { + CommonReviewers: true, + CommonReviewerIds: [user.id, adminUser.id, secondUserID], + TeamReviewersSetting: {}, + SystemAdminsAsReviewers: true, + TeamAdminsAsReviewers: true, + }, + AdditionalSettings: { + Reasons: ['Inappropriate content', 'Spam', 'Harassment', 'Other'], + ReporterCommentRequired: true, + ReviewerCommentRequired: true, + HideFlaggedContent: true, + }, + }); + + // Create and flag post + const message = `Post by @${user.username}, is flagged once`; + const {post, townSquare} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, reasonToFlag, flagPostComment); + + // Reviewer logs in and verifies flagged post in content review + const {channelsPage, contentReviewPage} = await pw.testBrowser.login(secondUser); + + await verifyFlaggedPostCardDetails(post.id, channelsPage, contentReviewPage, secondTeam, message); + await verifyRHSFlaggedPostDetails( + post.id, + contentReviewPage, + user.username, + adminUser.username, + message, + reasonToFlag, + flagPostReviewStatus, + townSquare.display_name, + ); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/multiple-reviewers-receive-same-flag.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/multiple-reviewers-receive-same-flag.spec.ts new file mode 100644 index 00000000000..227512a4e0c --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-reports/multiple-reviewers-receive-same-flag.spec.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {test} from '@mattermost/playwright-lib'; + +import {setupContentFlagging, createPost, verifyAuthorNotification} from './../support'; + +/** @objective Verify that multiple reviewers receive the same flag notification + * @testcase + * 1. Create three users and add them as reviewers to a team + * 2. Setup content flagging with the three users as reviewers + * 3. Create a post and flag it + * 4. Verify that all three reviewers receive a review request in their Content Review channel + * 5. Verify the flagged post details in each reviewer's Content Review DM and RHS + * + */ +test('Verify multiple reviewers receive same flagged post', async ({pw}) => { + const {adminClient, team, user, userClient, adminUser} = await pw.initSetup(); + + // Create second user and add to team + const secondUser = await pw.random.user('reviewer'); + const {id: secondUserID} = await adminClient.createUser(secondUser, '', ''); + await adminClient.addToTeam(team.id, secondUserID); + + // Create third user and add to team + const thirdUser = await pw.random.user('reviewer'); + const {id: thirdUserID} = await adminClient.createUser(thirdUser, '', ''); + await adminClient.addToTeam(team.id, thirdUserID); + + // Setup content flagging *after* roles are set + await setupContentFlagging(adminClient, [adminUser.id, secondUserID, thirdUserID]); + + const message = `Post by @${user.username}, is flagged once`; + + const {post} = await createPost(adminClient, userClient, team, user, message); + await adminClient.flagPost(post.id, 'Inappropriate content', 'This message is inappropriate'); + + const {channelsPage: secondChannelsPage, contentReviewPage: secondContentReviewPage} = + await pw.testBrowser.login(secondUser); + await verifyAuthorNotification(post.id, secondChannelsPage, secondContentReviewPage, team.name, message, 'Pending'); + + const {channelsPage: channelsPageThird, contentReviewPage: contentReviewPageThird} = + await pw.testBrowser.login(thirdUser); + await verifyAuthorNotification(post.id, channelsPageThird, contentReviewPageThird, team.name, message, 'Pending'); +}); diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/support.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/support.ts new file mode 100644 index 00000000000..d089185ff95 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/support.ts @@ -0,0 +1,127 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect} from '@mattermost/playwright-lib'; + +export async function setupContentFlagging( + adminClient: any, + userIds: string[], + enable = true, + hideFlaggedContent = true, +) { + // Configure content flagging + await adminClient.saveContentFlaggingConfig({ + EnableContentFlagging: enable, + NotificationSettings: { + EventTargetMapping: { + assigned: ['reviewers', 'author'], + dismissed: ['reporter', 'author', 'reviewers'], + flagged: ['reviewers', 'author'], + removed: ['author', 'reporter', 'reviewers'], + }, + }, + ReviewerSettings: { + CommonReviewers: true, + CommonReviewerIds: userIds, + TeamReviewersSetting: {}, + SystemAdminsAsReviewers: true, + TeamAdminsAsReviewers: true, + }, + AdditionalSettings: { + Reasons: ['Inappropriate content', 'Spam', 'Harassment', 'Other'], + ReporterCommentRequired: true, + ReviewerCommentRequired: true, + HideFlaggedContent: hideFlaggedContent, + }, + }); + return adminClient; +} + +export async function createPost(adminClient: any, userClient: any, team: any, user: any, message: string) { + const channels = await adminClient.getMyChannels(team.id); + const townSquare = channels.find((ch: any) => ch.name === 'town-square'); + + if (!townSquare) throw new Error('Town Square channel not found'); + + const post = await userClient.createPost({ + channel_id: townSquare.id, + message, + user_id: user.id, + }); + + return {post, message, townSquare}; +} + +export async function verifyAuthorNotification( + postID: string, + channelsPage: any, + contentReviewPage: any, + teamName: string, + expectedMessage: string, + postStatus: string, +) { + await channelsPage.goto(teamName, '@content-review'); + await channelsPage.toBeVisible(); + + await contentReviewPage.setReportCardByPostID(postID); + await contentReviewPage.waitForPageLoaded(); + + await contentReviewPage.verifyFlaggedPostStatus(postStatus); + await contentReviewPage.verifyFlaggedPostReason('Inappropriate content'); + await contentReviewPage.verifyFlaggedPostMessage(expectedMessage); +} + +/** + * Verify flagged post details inside the RHS view + */ +export async function verifyRHSFlaggedPostDetails( + postID: string, + contentReviewPage: any, + postedByUsername: string, + flaggedByUsername: string, + postMessageFlagged: string, + reasonToFlag: string, + flagPostReviewStatus: string, + postFlaggedInChannel: string, +) { + await contentReviewPage.setReportCardByPostID(postID); + await contentReviewPage.openViewDetails(); + await contentReviewPage.waitForRHSVisible(); + + await contentReviewPage.expectSelectProperty('Status', flagPostReviewStatus); + await contentReviewPage.expectSelectProperty('Reason', reasonToFlag); + await contentReviewPage.expectMessageContains(postMessageFlagged); + await contentReviewPage.expectUser('Flagged by', flaggedByUsername); + await contentReviewPage.expectUser('Posted by', postedByUsername); + await contentReviewPage.expectChannel(postFlaggedInChannel); +} + +/** + * Verify flagged post card details in the Content Review DM + */ +export async function verifyFlaggedPostCardDetails( + postID: string, + channelsPage: any, + contentReviewPage: any, + team: any, + expectedMessage: string, +) { + await channelsPage.goto(team.name, '@content-review'); + await channelsPage.toBeVisible(); + + await contentReviewPage.setReportCardByPostID(postID); + await contentReviewPage.waitForPageLoaded(); + + await contentReviewPage.verifyFlaggedPostStatus('Pending'); + await contentReviewPage.verifyFlaggedPostReason('Inappropriate content'); + await contentReviewPage.verifyFlaggedPostMessage(expectedMessage); +} + +export async function verifyReporterNotification(channelsPage: any, teamName: string, expectedMessage: string) { + await channelsPage.goto(teamName, '@content-review'); + await channelsPage.toBeVisible(); + + const lastPostId = await channelsPage.centerView.getLastPostID(); + const post = await channelsPage.centerView.getPostById(lastPostId); + await expect(post.body).toContainText(expectedMessage); +} diff --git a/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts b/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts index 8b98b939751..4ffb8d8976c 100644 --- a/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts @@ -24,7 +24,7 @@ test('displays multiple mentions correctly in Recent Mentions panel', {tag: '@me // # Create a second user to be mentioned const {adminClient} = await pw.getAdminClient(); - const mentionedUser = pw.random.user('mentioned'); + const mentionedUser = await pw.random.user('mentioned'); const {id: mentionedUserID} = await adminClient.createUser(mentionedUser, '', ''); // # Add the mentioned user to the team diff --git a/e2e-tests/playwright/specs/functional/channels/message_scroll/thread_appears_and_scrollable_in_the_rhs.spec.ts b/e2e-tests/playwright/specs/functional/channels/message_scroll/thread_appears_and_scrollable_in_the_rhs.spec.ts index 83dce52ebd2..9d105969fd6 100644 --- a/e2e-tests/playwright/specs/functional/channels/message_scroll/thread_appears_and_scrollable_in_the_rhs.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/message_scroll/thread_appears_and_scrollable_in_the_rhs.spec.ts @@ -26,7 +26,7 @@ test('MM-T3293 The entire thread appears in the RHS (scrollable)', {tag: ['@mess } as Partial), ); - const otherUser = pw.random.user('other'); + const otherUser = await pw.random.user('other'); const createdOtherUser = await adminClient.createUser(otherUser, '', ''); otherUser.id = createdOtherUser.id; diff --git a/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts b/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts index a410655c37e..1ef114fe0f4 100644 --- a/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts @@ -300,7 +300,7 @@ test( // # Initialize test setup with main user and create a second user const {user, team, adminClient} = await pw.initSetup(); - const otherUser = await adminClient.createUser(pw.random.user(), '', ''); + const otherUser = await adminClient.createUser(await pw.random.user(), '', ''); // # Login as first user and navigate to DM channel with second user const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); diff --git a/e2e-tests/playwright/specs/functional/channels/search/team_selector.spec.ts b/e2e-tests/playwright/specs/functional/channels/search/team_selector.spec.ts index 496c2a508f3..c69da630a5b 100644 --- a/e2e-tests/playwright/specs/functional/channels/search/team_selector.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/search/team_selector.spec.ts @@ -56,7 +56,7 @@ test('team selector should be visible if user belongs to multiple teams', async const {adminClient, user, team} = await pw.initSetup(); // # Create a second team and add the user to it - const secondTeam = await adminClient.createTeam(pw.random.team('team', 'Team', 'O', true)); + const secondTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true)); await adminClient.addUsersToTeam(secondTeam.id, [user.id]); // # Create a channel in the first team @@ -131,7 +131,7 @@ test('team selector should show filter input with more than 4 teams', async ({pw // # Create 4 more teams (for a total of 5) and add the user to them const teams = [team]; for (let i = 0; i < 4; i++) { - const newTeam = await adminClient.createTeam(pw.random.team('team', 'Team', 'O', true)); + const newTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true)); await adminClient.addUsersToTeam(newTeam.id, [user.id]); teams.push(newTeam); } diff --git a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts index c4814327924..5368af1a4fd 100644 --- a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts @@ -7,7 +7,12 @@ let keywords: string[]; const highlightWithoutNotificationClass = 'non-notification-highlight'; test.beforeEach(async ({pw}) => { - keywords = [`AB${pw.random.id()}`, `CD${pw.random.id()}`, `EF${pw.random.id()}`, `Highlight me ${pw.random.id()}`]; + keywords = [ + `AB${await pw.random.id()}`, + `CD${await pw.random.id()}`, + `EF${await pw.random.id()}`, + `Highlight me ${await pw.random.id()}`, + ]; }); test('MM-T5465-1 Should add the keyword when enter, comma or tab is pressed on the textbox', async ({pw}) => { diff --git a/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts b/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts index 28ce3dd6e83..9addfa88c11 100644 --- a/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/permissions/team_access.spec.ts @@ -92,7 +92,7 @@ test('MM-63378 System Manager without team access permissions cannot view team d await adminClient.updateUserRoles(systemManagerUser.id, 'system_user system_manager'); // Create another team of which the user is not a member. - const otherTeam = await adminClient.createTeam(pw.random.team()); + const otherTeam = await adminClient.createTeam(await pw.random.team()); // Login as the user const {systemConsolePage} = await pw.testBrowser.login(systemManagerUser); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts index c6a6b269651..fb2d661e31c 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/actions.spec.ts @@ -20,8 +20,8 @@ async function setupAndGetRandomUser(pw: PlaywrightExtended) { const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create a random user to edit for - const user = await adminClient.createUser(pw.random.user(), '', ''); - const team = await adminClient.createTeam(pw.random.team()); + const user = await adminClient.createUser(await pw.random.user(), '', ''); + const team = await adminClient.createTeam(await pw.random.team()); await adminClient.addToTeam(team.id, user.id); // # Visit system console @@ -156,7 +156,7 @@ test('MM-T5520-4 should reset the users password', async ({pw}) => { // # Enter a random password and click Save const passwordInput = systemConsolePage.page.locator('input[type="password"]'); - await passwordInput.fill(pw.random.id()); + await passwordInput.fill(await pw.random.id()); await systemConsolePage.clickResetButton(); // * Verify that the modal closed and no error showed @@ -165,7 +165,7 @@ test('MM-T5520-4 should reset the users password', async ({pw}) => { test('MM-T5520-5 should change the users email', async ({pw}) => { const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw); - const newEmail = `${pw.random.id()}@example.com`; + const newEmail = `${await pw.random.id()}@example.com`; // # Open menu and click Update Email await systemConsolePage.systemUsers.actionMenuButtons[0].click(); @@ -173,7 +173,7 @@ test('MM-T5520-5 should change the users email', async ({pw}) => { await updateEmail.click(); // # Enter a random password and click Save - const emailInput = await systemConsolePage.page.locator('input[type="email"]'); + const emailInput = systemConsolePage.page.locator('input[type="email"]'); await emailInput.fill(newEmail); await systemConsolePage.clickResetButton(); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts index 4afcbe77820..29b7b207cbf 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/column_sort.spec.ts @@ -15,7 +15,7 @@ test('MM-T5523-1 Sortable columns should sort the list when clicked', async ({pw // # Create 10 random users for (let i = 0; i < 10; i++) { - await adminClient.createUser(pw.random.user(), '', ''); + await adminClient.createUser(await pw.random.user(), '', ''); } // # Visit system console @@ -59,7 +59,7 @@ test('MM-T5523-2 Non sortable columns should not sort the list when clicked', as // # Create 10 random users for (let i = 0; i < 10; i++) { - await adminClient.createUser(pw.random.user(), '', ''); + await adminClient.createUser(await pw.random.user(), '', ''); } // # Visit system console diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts index b53eca49e82..c7801715233 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/filter_popover.spec.ts @@ -14,13 +14,13 @@ test('MM-T5521-7 Should be able to filter users with team filter', async ({pw}) const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create a team with a user - const team1 = await adminClient.createTeam(pw.random.team()); - const user1 = await adminClient.createUser(pw.random.user(), '', ''); + const team1 = await adminClient.createTeam(await pw.random.team()); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); await adminClient.addToTeam(team1.id, user1.id); // # Create another team with a user - const team2 = await adminClient.createTeam(pw.random.team()); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const team2 = await adminClient.createTeam(await pw.random.team()); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); await adminClient.addToTeam(team2.id, user2.id); // # Visit system console @@ -62,11 +62,11 @@ test('MM-T5521-8 Should be able to filter users with role filter', async ({pw}) const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create a guest user - const guestUser = await adminClient.createUser(pw.random.user(), '', ''); + const guestUser = await adminClient.createUser(await pw.random.user(), '', ''); await adminClient.updateUserRoles(guestUser.id, 'system_guest'); // # Create a regular user - const regularUser = await adminClient.createUser(pw.random.user(), '', ''); + const regularUser = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); @@ -117,11 +117,11 @@ test('MM-T5521-9 Should be able to filter users with status filter', async ({pw} const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create a user and then deactivate it - const deactivatedUser = await adminClient.createUser(pw.random.user(), '', ''); + const deactivatedUser = await adminClient.createUser(await pw.random.user(), '', ''); await adminClient.updateUserActive(deactivatedUser.id, false); // # Create a regular user - const regularUser = await adminClient.createUser(pw.random.user(), '', ''); + const regularUser = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts index 17a92b95cd7..62896f760ef 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/search.spec.ts @@ -14,8 +14,8 @@ test('MM-T5521-1 Should be able to search users with their first names', async ( const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create 2 users - const user1 = await adminClient.createUser(pw.random.user(), '', ''); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); @@ -46,8 +46,8 @@ test('MM-T5521-2 Should be able to search users with their last names', async ({ const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create 2 users - const user1 = await adminClient.createUser(pw.random.user(), '', ''); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); @@ -78,8 +78,8 @@ test('MM-T5521-3 Should be able to search users with their emails', async ({pw}) const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create 2 users - const user1 = await adminClient.createUser(pw.random.user(), '', ''); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); @@ -110,8 +110,8 @@ test('MM-T5521-4 Should be able to search users with their usernames', async ({p const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create 2 users - const user1 = await adminClient.createUser(pw.random.user(), '', ''); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); @@ -142,8 +142,8 @@ test('MM-T5521-5 Should be able to search users with their nick names', async ({ const {systemConsolePage} = await pw.testBrowser.login(adminUser); // # Create 2 users - const user1 = await adminClient.createUser(pw.random.user(), '', ''); - const user2 = await adminClient.createUser(pw.random.user(), '', ''); + const user1 = await adminClient.createUser(await pw.random.user(), '', ''); + const user2 = await adminClient.createUser(await pw.random.user(), '', ''); // # Visit system console await systemConsolePage.goto(); diff --git a/e2e-tests/playwright/utils/utils.ts b/e2e-tests/playwright/utils/utils.ts index 806b851b815..45fa85261e3 100644 --- a/e2e-tests/playwright/utils/utils.ts +++ b/e2e-tests/playwright/utils/utils.ts @@ -1,11 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {v4 as uuidv4} from 'uuid'; +let uuidv4: (() => string) | null = null; + +export async function getRandomId(length = 7): Promise { + if (!uuidv4) { + const {v4} = await import('uuid'); + uuidv4 = v4; + } -export function getRandomId(length = 7): string { const MAX_SUBSTRING_INDEX = 27; - return uuidv4() .replace(/-/g, '') .substring(MAX_SUBSTRING_INDEX - length, MAX_SUBSTRING_INDEX);