E2E/Playwright: Refactor and fix tests for scheduled posts (#30871)

Этот коммит содержится в:
sabril
2025-05-06 11:27:18 +08:00
коммит произвёл GitHub
родитель 15efbb658f
Коммит 6a4407de76
44 изменённых файлов: 2060 добавлений и 2449 удалений

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

@@ -296,7 +296,7 @@ $(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
PW_RESET_BEFORE_TEST: "false"
PW_HEADLESS: "true"
PW_SLOWMO: 0
PW_WORKERS: 2
PW_WORKERS: 1
PW_SNAPSHOT_ENABLE: "false"
PW_PERCY_ENABLE: "false"
ulimits:

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

@@ -84,7 +84,7 @@ npm run playwright-ui
#### 1. Run Playwright's docker container
Change to the `e2e-tests/playwright` directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
Change to the `./` project directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
```bash
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.52.0-noble /bin/bash

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

@@ -28,6 +28,7 @@ export default {
'@axe-core/playwright',
'@percy/playwright',
'dotenv',
'luxon',
'node:fs/promises',
'node:path',
'node:fs',

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

@@ -32,12 +32,12 @@ export class TestBrowser {
const channelsPage = new pages.ChannelsPage(page);
const systemConsolePage = new pages.SystemConsolePage(page);
const scheduledDraftPage = new pages.ScheduledDraftPage(page);
const draftPage = new pages.DraftPage(page);
const scheduledPostsPage = new pages.ScheduledPostsPage(page);
const draftsPage = new pages.DraftsPage(page);
this.context = context;
return {context, page, channelsPage, systemConsolePage, scheduledDraftPage, draftPage};
return {context, page, channelsPage, systemConsolePage, scheduledPostsPage, draftsPage};
}
async close() {

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

@@ -4,3 +4,7 @@
export const appsPluginId = 'com.mattermost.apps';
export const callsPluginId = 'com.mattermost.calls';
export const playbooksPluginId = 'playbooks';
// Remote users hour limit taken from webapp/channels/src/utils/constants.ts
export const REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY = 22;
export const REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY = 6;

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

@@ -24,6 +24,9 @@ export async function baseGlobalSetup() {
({client: adminClient, user: adminUser} = await makeClient(defaultAdmin));
}
// Print playwright configs
printPlaywrightTestConfig();
await sysadminSetup(adminClient, adminUser);
}
@@ -69,6 +72,14 @@ async function sysadminSetup(client: Client4, user: UserProfile | null) {
await printPluginDetails(client);
}
function printPlaywrightTestConfig() {
// eslint-disable-next-line no-console
console.log(`Playwright Test Config:
- Headless = ${testConfig.headless}
- SlowMo = ${testConfig.slowMo}
- Workers = ${testConfig.workers}`);
}
async function printLicenseInfo(client: Client4) {
const license = await client.getClientLicenseOld();
// eslint-disable-next-line no-console
@@ -94,7 +105,7 @@ async function printClientInfo(client: Client4) {
- TelemetryId = ${config.TelemetryId}
- ServiceEnvironment = ${config.ServiceEnvironment}`);
const {LogSettings, ServiceSettings, FeatureFlags} = await client.getConfig();
const {LogSettings, ServiceSettings, PluginSettings, FeatureFlags} = await client.getConfig();
// eslint-disable-next-line no-console
console.log(`Notable Server Config:
- ServiceSettings.EnableSecurityFixAlert = ${ServiceSettings?.EnableSecurityFixAlert}
@@ -108,6 +119,12 @@ async function printClientInfo(client: Client4) {
.map(([key, value]) => ` - ${key} = ${value}`)
.join('\n'),
);
// eslint-disable-next-line no-console
console.log(`Plugin Settings:
- Enable = ${PluginSettings?.Enable}
- EnableUploads = ${PluginSettings?.EnableUploads}
- AutomaticPrepackagedPlugins = ${PluginSettings?.AutomaticPrepackagedPlugins}`);
}
async function printPluginDetails(client: Client4) {

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

@@ -14,9 +14,56 @@ export {
LoginPage,
ResetPasswordPage,
SignupPage,
ScheduledDraftPage,
ScheduledPostsPage,
SystemConsolePage,
DraftPage,
DraftsPage,
} from './ui/pages';
export {
components,
GlobalHeader,
SearchPopover,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,
ChannelsAppBar,
ChannelsHeader,
ChannelsPostCreate,
ChannelsPostEdit,
ChannelsPost,
DraftPost,
FindChannelsModal,
DeletePostModal,
DeleteScheduledPostModal,
SettingsModal,
PostDotMenu,
PostMenu,
ThreadFooter,
Footer,
MainHeader,
PostReminderMenu,
EmojiGifPicker,
GenericConfirmModal,
ScheduleMessageMenu,
ScheduleMessageModal,
ScheduledPostIndicator,
ScheduledDraftModal,
ScheduledPost,
SendMessageNowModal,
SystemConsoleSidebar,
SystemConsoleNavbar,
SystemUsers,
SystemUsersFilterPopover,
SystemUsersFilterMenu,
SystemUsersColumnToggleMenu,
SystemConsoleFeatureDiscovery,
SystemConsoleMobileSecurity,
MessagePriority,
UserProfilePopover,
UserAccountMenu,
DeletePostConfirmationDialog,
RestorePostConfirmationDialog,
ProfileModal,
} from './ui/components';
export {TestArgs, ScreenshotOptions} from './types';

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

@@ -396,7 +396,7 @@ const defaultServerConfig: AdminConfig = {
AboutLink: 'https://mattermost.com/pl/about-mattermost',
HelpLink: 'https://mattermost.com/pl/help/',
ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug',
ReportAProblemType: 'link',
ReportAProblemType: 'default',
ReportAProblemMail: '',
AllowDownloadLogs: true,
ForgotPasswordLink: '',

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

@@ -7,4 +7,4 @@ export {getOnPremServerConfig} from './default_config';
export {initSetup, getAdminClient} from './init';
export {createRandomPost} from './post';
export {createRandomTeam} from './team';
export {createNewUserProfile, createRandomUser, getDefaultAdminUser} from './user';
export {createNewUserProfile, createRandomUser, getDefaultAdminUser, isOutsideRemoteUserHour} from './user';

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

@@ -2,10 +2,12 @@
// See LICENSE.txt for license information.
import {Client4} from '@mattermost/client';
import {UserProfile} from '@mattermost/types/users';
import {UserProfile, UserTimezone} from '@mattermost/types/users';
import {DateTime} from 'luxon';
import {getRandomId} from '@/util';
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);
@@ -42,3 +44,15 @@ export function getDefaultAdminUser() {
return admin as UserProfile;
}
export function isOutsideRemoteUserHour(userTz: UserTimezone | undefined) {
const timezone = (userTz?.useAutomaticTimezone ? userTz?.automaticTimezone : userTz?.manualTimezone) || 'UTC';
const teammateUserDate = DateTime.local().setZone(timezone);
const currentHour = teammateUserDate.hour;
return (
currentHour >= REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY ||
currentHour < REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY
);
}

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

@@ -25,6 +25,7 @@ import {
createRandomUser,
getAdminClient,
initSetup,
isOutsideRemoteUserHour,
} from './server';
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
import {pages} from './ui/pages';
@@ -90,6 +91,7 @@ export class PlaywrightExtended {
// ./server
readonly createNewUserProfile;
readonly isOutsideRemoteUserHour;
// ./visual
readonly matchSnapshot;
@@ -131,7 +133,7 @@ export class PlaywrightExtended {
this.ensurePluginsLoaded = ensurePluginsLoaded;
this.initSetup = initSetup;
this.getAdminClient = getAdminClient;
this.isOutsideRemoteUserHour = isOutsideRemoteUserHour;
// ./test_action
this.hideDynamicChannelsContent = hideDynamicChannelsContent;
this.waitForAnimationEnd = waitForAnimationEnd;

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

@@ -7,6 +7,7 @@ import ChannelsHeader from './header';
import ChannelsPostCreate from './post_create';
import ChannelsPostEdit from './post_edit';
import ChannelsPost from './post';
import ScheduledPostIndicator from './scheduled_post_indicator';
import {duration, hexToRgb} from '@/util';
import {waitUntil} from '@/test_action';
@@ -18,14 +19,7 @@ export default class ChannelsCenterView {
readonly header;
readonly postCreate;
readonly scheduledDraftOptions;
readonly postBoxIndicator;
readonly scheduledDraftChannelIcon;
readonly scheduledDraftChannelInfoMessage;
readonly scheduledDraftChannelInfoMessageLocator;
readonly scheduledDraftDMChannelLocator;
readonly scheduledDraftChannelInfoMessageText;
readonly scheduledDraftDMChannelLocatorString;
readonly scheduledDraftSeeAllLink;
readonly scheduledPostIndicator;
readonly postEdit;
readonly editedPostIcon;
readonly channelBanner;
@@ -34,18 +28,11 @@ export default class ChannelsCenterView {
this.container = container;
this.page = page;
this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")';
this.scheduledDraftDMChannelLocatorString = 'div.ScheduledPostIndicator span a';
this.header = new ChannelsHeader(this.container.locator('.channel-header'));
this.postCreate = new ChannelsPostCreate(container.getByTestId('post-create'));
this.scheduledDraftOptions = new ChannelsPostCreate(container.locator('#dropdown_send_post_options'));
this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
this.postBoxIndicator = container.locator('div.postBoxIndicator');
this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator');
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
this.scheduledDraftChannelInfoMessageText = container.locator(this.scheduledDraftChannelInfoMessageLocator);
this.scheduledDraftDMChannelLocator = container.locator(this.scheduledDraftDMChannelLocatorString);
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
this.scheduledPostIndicator = new ScheduledPostIndicator(container.getByTestId('scheduledPostIndicator'));
this.editedPostIcon = (postID: string) => container.locator(`#postEdited_${postID}`);
this.channelBanner = container.getByTestId('channel_banner_container');
}
@@ -59,14 +46,6 @@ export default class ChannelsCenterView {
await this.postCreate.postMessage(message, files);
}
/**
* Click on "See all scheduled messages"
*/
async clickOnSeeAllscheduledDrafts() {
await this.scheduledDraftSeeAllLink.isVisible();
await this.scheduledDraftSeeAllLink.click();
}
/**
* Return the first post in the Center
*/
@@ -140,22 +119,6 @@ export default class ChannelsCenterView {
);
}
async goToScheduledDraftsFromDMChannel() {
if (await this.scheduledDraftDMChannelLocator.isVisible()) {
await this.scheduledDraftDMChannelLocator.click();
return;
}
await this.scheduledDraftSeeAllLink.isVisible();
await this.scheduledDraftSeeAllLink.click();
}
async verifyscheduledDraftChannelInfo() {
await this.postBoxIndicator.isVisible();
await this.scheduledDraftChannelIcon.isVisible();
const messageLocator = this.scheduledDraftChannelInfoMessage.first();
await expect(messageLocator).toContainText('Message scheduled for');
}
async clickOnLastEditedPost(postID: string | null) {
if (postID) {
await this.editedPostIcon(postID).click();

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

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class DeleteScheduledPostModal {
readonly container: Locator;
readonly body: Locator;
readonly deleteButton: Locator;
readonly cancelButton: Locator;
readonly closeButton: Locator;
constructor(container: Locator) {
this.container = container;
this.body = container.locator('.modal-body');
this.deleteButton = container.locator('button:has-text("Yes, delete")');
this.cancelButton = container.locator('button:has-text("Cancel")');
this.closeButton = container.getByRole('button', {name: 'Close'});
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}

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

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class DraftPost {
readonly container: Locator;
readonly panelHeader;
readonly panelBody;
readonly postBody;
readonly postHeader;
readonly postImage;
readonly deleteButton;
readonly editButton;
readonly scheduleButton;
readonly sendButton;
constructor(container: Locator) {
this.container = container;
this.panelHeader = container.locator('.PanelHeader');
this.panelBody = container.locator('.DraftPanelBody');
this.postBody = container.locator('.post__body');
this.postHeader = container.locator('.post__header');
this.postImage = container.locator('.post__img');
this.deleteButton = container.locator('#draft_icon-trash-can-outline_delete');
this.editButton = container.locator('#draft_icon-pencil-outline_edit');
this.scheduleButton = container.locator('#draft_icon-clock-send-outline_reschedule');
this.sendButton = container.locator('#draft_icon-send-outline_send');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async hover() {
await this.container.hover();
}
}

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

@@ -51,7 +51,14 @@ export default class ChannelsPost {
return this.profileIcon.getByAltText(`${username} profile image`);
}
async openRhs() {
async openAThread() {
await this.container.hover();
await this.postMenu.toBeVisible();
await this.postMenu.replyButton.waitFor();
await this.postMenu.replyButton.click();
}
async reply() {
await this.container.hover();
await this.postMenu.toBeVisible();
await this.postMenu.reply();

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

@@ -16,7 +16,7 @@ export default class ChannelsPostCreate {
readonly attachmentButton;
readonly emojiButton;
readonly sendMessageButton;
readonly scheduleDraftMessageButton;
readonly scheduleMessageButton;
readonly priorityButton;
readonly suggestionList;
readonly filePreview;
@@ -33,7 +33,7 @@ export default class ChannelsPostCreate {
this.attachmentButton = container.locator('#fileUploadButton');
this.emojiButton = container.getByLabel('select an emoji');
this.sendMessageButton = container.getByTestId('SendMessageButton');
this.scheduleDraftMessageButton = container.getByLabel('Schedule message');
this.scheduleMessageButton = container.getByLabel('Schedule message');
this.priorityButton = container.getByLabel('Message priority');
this.suggestionList = container.getByTestId('suggestionList');
this.filePreview = container.locator('.file-preview__container');
@@ -79,18 +79,6 @@ export default class ChannelsPostCreate {
await this.sendMessageButton.click();
}
/**
* Click on Scheduled Draft button to open options
*/
async clickOnScheduleDraftDropdownButton() {
await expect(this.input).toBeVisible();
await expect(this.scheduleDraftMessageButton).toBeVisible();
await expect(this.scheduleDraftMessageButton).toBeEnabled();
await this.scheduleDraftMessageButton.click();
}
/**
* Opens the message priority menu
*/

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

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ScheduleMessageMenu {
readonly container: Locator;
readonly tomorrowMenuItem;
readonly mondayMenuItem;
readonly nextMondayMenuItem;
readonly recentlyUsedCustomTimeMenuItem;
readonly customTimeMenuItem;
constructor(container: Locator) {
this.container = container;
this.tomorrowMenuItem = container.getByTestId('scheduling_time_tomorrow_9_am');
this.mondayMenuItem = container.getByTestId('scheduling_time_monday_9_am');
this.nextMondayMenuItem = container.getByTestId('scheduling_time_next_monday_9_am');
this.recentlyUsedCustomTimeMenuItem = container.getByTestId('recently_used_custom_time');
this.customTimeMenuItem = container.getByText('Choose a custom time');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async selectCustomTime() {
await this.customTimeMenuItem.click();
}
}

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

@@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ScheduleMessageModal {
readonly container: Locator;
readonly dateButton: Locator;
readonly timeButton: Locator;
readonly timeOptionDropdown: Locator;
readonly closeButton: Locator;
readonly scheduleButton: Locator;
readonly cancelButton: Locator;
constructor(container: Locator) {
this.container = container;
this.dateButton = container.locator('#customStatus__calendar-input');
this.timeButton = container.getByTestId('time_button');
this.timeOptionDropdown = container.getByLabel('Choose a time');
this.closeButton = container.getByRole('button', {name: 'Close'});
this.scheduleButton = container.locator('button:has-text("Schedule")');
this.cancelButton = container.locator('button:has-text("Cancel")');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
getDaySuffix(day: number): string {
if (day > 3 && day < 21) return 'th';
switch (day % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
dateLocator(day: number, month: string, dayOfWeek: string) {
const daySuffix = this.getDaySuffix(day);
const name = `${day}${daySuffix} ${month} (${dayOfWeek})`;
return this.container.getByRole('button', {name});
}
async selectDate(dayFromToday: number = 0) {
await this.dateButton.click();
const pacificDate = new Date();
const originDate = new Date();
if (dayFromToday) {
pacificDate.setDate(pacificDate.getDate() + dayFromToday);
}
const day = pacificDate.getDate();
const month = pacificDate.toLocaleString('default', {month: 'long'});
const dayOfWeek = pacificDate.toLocaleDateString('en-US', {weekday: 'long'});
const dateLocator = this.dateLocator(day, month, dayOfWeek);
const isMonthChanged = pacificDate.getMonth() !== originDate.getMonth();
if (!(await dateLocator.isVisible()) && isMonthChanged) {
await this.container.getByLabel('Go to next month').click();
}
await dateLocator.click();
// if day is less than 9 then add a 0 in front of the day
if (day < 9) {
return `${month} 0${day}`;
}
return `${month} ${day}`;
}
async selectTime(optionIndex: number = 0) {
await this.timeButton.click();
const timeButton = this.timeOptionDropdown.getByTestId(`time_option_${optionIndex}-button`);
await expect(timeButton).toBeVisible();
await timeButton.click();
return await timeButton.textContent();
}
async scheduleMessage(dayFromToday: number = 0, timeOptionIndex: number = 0) {
await this.toBeVisible();
const selectedDate = await this.selectDate(dayFromToday);
const fromDateButton = await this.dateButton.inputValue();
const selectedTime = await this.selectTime(timeOptionIndex);
await this.scheduleButton.click();
// if selectedDate is Today or Tomorrow then return Today or Tomorrow
if (fromDateButton === 'Today' || fromDateButton === 'Tomorrow') {
return {selectedDate: fromDateButton, selectedTime};
}
// if selectedDate is a date in the future then return the date
return {selectedDate, selectedTime};
}
}

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

@@ -1,24 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ScheduledDraftMenu {
readonly container: Locator;
readonly scheduleDraftMessageCustomTimeOption;
constructor(container: Locator) {
this.container = container;
this.scheduleDraftMessageCustomTimeOption = container.getByText('Choose a custom time');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async selectCustomTime() {
await this.scheduleDraftMessageCustomTimeOption.click();
}
}

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

@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ScheduledPost {
readonly container: Locator;
readonly panelHeader;
readonly panelBody;
readonly postBody;
readonly postHeader;
readonly postImage;
readonly deleteButton;
readonly editButton;
readonly copyTextButton;
readonly rescheduleButton;
readonly sendNowButton;
readonly editTextBox;
readonly saveButton;
readonly cancelButton;
constructor(container: Locator) {
this.container = container;
this.panelHeader = container.locator('.PanelHeader');
this.panelBody = container.locator('.DraftPanelBody');
this.postBody = container.locator('.post__body');
this.postHeader = container.locator('.post__header');
this.postImage = container.locator('.post__img');
this.deleteButton = container.locator('#draft_icon-trash-can-outline_delete');
this.editButton = container.locator('#draft_icon-pencil-outline_edit');
this.copyTextButton = container.locator('#draft_icon-content-copy_copy_text');
this.rescheduleButton = container.locator('#draft_icon-clock-send-outline_reschedule');
this.sendNowButton = container.locator('#draft_icon-send-outline_sendNow');
this.editTextBox = container.getByTestId('edit_textbox');
this.saveButton = container.locator('button:has-text("Save")');
this.cancelButton = container.locator('button:has-text("Cancel")');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async hover() {
await this.container.hover();
}
}

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

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ScheduledPostIndicator {
readonly container: Locator;
readonly icon;
readonly messageText;
readonly seeAllLink;
readonly scheduledMessageLink;
constructor(container: Locator) {
this.container = container;
this.icon = container.getByTestId('scheduledPostIcon');
this.messageText = container.locator('span').first();
this.seeAllLink = container.locator('a:has-text("See all")');
this.scheduledMessageLink = container.locator('a:has-text("scheduled message")');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async toBeNotVisible() {
await expect(this.container).not.toBeVisible();
}
async getText() {
return await this.messageText.innerText();
}
}

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

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class SendMessageNowModal {
readonly container: Locator;
readonly body: Locator;
readonly sendNowButton: Locator;
readonly cancelButton: Locator;
readonly closeButton: Locator;
constructor(container: Locator) {
this.container = container;
this.body = container.locator('.modal-body');
this.sendNowButton = container.locator('button:has-text("Yes, send now")');
this.cancelButton = container.locator('button:has-text("Cancel")');
this.closeButton = container.getByRole('button', {name: 'Close'});
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}

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

@@ -6,28 +6,19 @@ import {Locator, expect} from '@playwright/test';
export default class ChannelsSidebarLeft {
readonly container: Locator;
readonly findChannelButton;
readonly scheduledDraftCountonLHS;
readonly scheduledPostBadge;
constructor(container: Locator) {
this.container = container;
this.findChannelButton = container.getByRole('button', {name: 'Find Channels'});
this.scheduledDraftCountonLHS = container.locator('span.scheduledPostBadge');
this.scheduledPostBadge = container.locator('span.scheduledPostBadge');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async assertNoPendingScheduledDraft() {
await expect(this.scheduledDraftCountonLHS).not.toBeVisible();
}
async assertscheduledDraftCountLHS(count: string) {
await expect(this.scheduledDraftCountonLHS).toBeVisible();
await expect(this.scheduledDraftCountonLHS).toHaveText(count);
}
/**
* Clicks on the sidebar channel link with the given name.
* It can be any sidebar item name including channels, direct messages, or group messages, threads, etc.

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

@@ -6,6 +6,7 @@ import {Locator, expect} from '@playwright/test';
import ChannelsPostCreate from './post_create';
import ChannelsPostEdit from './post_edit';
import ChannelsPost from './post';
import ScheduledPostIndicator from './scheduled_post_indicator';
export default class ChannelsSidebarRight {
readonly container: Locator;
@@ -13,7 +14,7 @@ export default class ChannelsSidebarRight {
readonly closeButton;
readonly postCreate;
readonly rhsPostBody;
readonly postBoxIndicator;
readonly scheduledPostIndicator;
readonly scheduledDraftChannelInfoMessage;
readonly scheduledDraftSeeAllLink;
readonly scheduledDraftChannelInfoMessageText;
@@ -25,7 +26,7 @@ export default class ChannelsSidebarRight {
constructor(container: Locator) {
this.container = container;
this.postBoxIndicator = container.locator('div.postBoxIndicator');
this.scheduledPostIndicator = new ScheduledPostIndicator(container.getByTestId('scheduledPostIndicator'));
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
@@ -84,11 +85,6 @@ export default class ChannelsSidebarRight {
await expect(this.container).not.toBeVisible();
}
async clickOnSeeAllscheduledDrafts() {
await this.scheduledDraftSeeAllLink.isVisible();
await this.scheduledDraftSeeAllLink.click();
}
async toContainText(text: string) {
await expect(this.container).toContainText(text);
}

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

@@ -22,7 +22,9 @@ import ThreadFooter from './channels/thread_footer';
import EmojiGifPicker from './channels/emoji_gif_picker';
import GenericConfirmModal from './channels/generic_confirm_modal';
import MessagePriority from './channels/message_priority';
import ScheduledDraftMenu from './channels/scheduled_draft_menu';
import ScheduleMessageMenu from './channels/schedule_message_menu';
import ScheduleMessageModal from './channels/schedule_message_modal';
import ScheduledPostIndicator from './channels/scheduled_post_indicator';
import ScheduledDraftModal from './channels/scheduled_draft_modal';
import UserAccountMenu from './user_account_menu';
import ProfileModal from './channels/profile_modal';
@@ -38,6 +40,10 @@ import DeletePostConfirmationDialog from './channels/delete_post_confirmation_di
import RestorePostConfirmationDialog from './channels/restore_post_confirmation_dialog';
import SystemConsoleFeatureDiscovery from './system_console/sections/system_users/feature_discovery';
import SystemConsoleMobileSecurity from './system_console/sections/system_users/mobile_security';
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';
const components = {
GlobalHeader,
@@ -50,8 +56,10 @@ const components = {
ChannelsPostCreate,
ChannelsPostEdit,
ChannelsPost,
DraftPost,
FindChannelsModal,
DeletePostModal,
DeleteScheduledPostModal,
SettingsModal,
PostDotMenu,
PostMenu,
@@ -61,8 +69,12 @@ const components = {
PostReminderMenu,
EmojiGifPicker,
GenericConfirmModal,
ScheduledDraftMenu,
ScheduleMessageMenu,
ScheduleMessageModal,
ScheduledPostIndicator,
ScheduledDraftModal,
ScheduledPost,
SendMessageNowModal,
SystemConsoleSidebar,
SystemConsoleNavbar,
SystemUsers,
@@ -82,6 +94,7 @@ const components = {
export {
components,
GlobalHeader,
SearchPopover,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,
@@ -90,12 +103,36 @@ export {
ChannelsPostCreate,
ChannelsPostEdit,
ChannelsPost,
DraftPost,
FindChannelsModal,
DeletePostModal,
DeleteScheduledPostModal,
SettingsModal,
PostDotMenu,
PostMenu,
ThreadFooter,
Footer,
MainHeader,
PostReminderMenu,
EmojiGifPicker,
GenericConfirmModal,
ScheduleMessageMenu,
ScheduleMessageModal,
ScheduledPostIndicator,
ScheduledDraftModal,
ScheduledPost,
SendMessageNowModal,
SystemConsoleSidebar,
SystemConsoleNavbar,
SystemUsers,
SystemUsersFilterPopover,
SystemUsersFilterMenu,
SystemUsersColumnToggleMenu,
SystemConsoleFeatureDiscovery,
SystemConsoleMobileSecurity,
MessagePriority,
UserProfilePopover,
UserAccountMenu,
DeletePostConfirmationDialog,
RestorePostConfirmationDialog,
ProfileModal,

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

@@ -2,10 +2,11 @@
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {waitUntil} from 'async-wait-until';
import {ChannelsPost, components} from '@/ui/components';
import SettingsModal from '@/ui/components/channels/settings/settings_modal';
import {duration} from '@/util';
export default class ChannelsPage {
readonly channels = 'Channels';
@@ -15,7 +16,6 @@ export default class ChannelsPage {
readonly userAccountMenuButton;
readonly searchPopover;
readonly centerView;
readonly scheduledDraftDropdown;
readonly scheduledDraftModal;
readonly sidebarLeft;
readonly sidebarRight;
@@ -32,6 +32,8 @@ export default class ChannelsPage {
readonly postReminderMenu;
readonly userAccountMenu;
readonly emojiGifPickerPopup;
readonly scheduleMessageMenu;
readonly scheduleMessageModal;
constructor(page: Page) {
this.page = page;
@@ -56,11 +58,14 @@ export default class ChannelsPage {
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'}));
this.userAccountMenu = new components.UserAccountMenu(page.locator('#userAccountMenu'));
this.scheduleMessageMenu = new components.ScheduleMessageMenu(page.locator('#dropdown_send_post_options'));
// Popovers
this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker'));
this.scheduledDraftDropdown = new components.ScheduledDraftMenu(page.locator('#dropdown_send_post_options'));
this.scheduledDraftModal = new components.ScheduledDraftModal(page.locator('div.modal-content'));
this.scheduleMessageModal = new components.ScheduleMessageModal(
page.getByRole('dialog', {name: 'Schedule message'}),
);
this.userProfilePopover = new components.UserProfilePopover(page.locator('.user-profile-popover'));
// Posts
@@ -87,6 +92,8 @@ export default class ChannelsPage {
}
}
await this.page.goto(channelsUrl);
return channelsUrl;
}
/**
@@ -98,6 +105,30 @@ export default class ChannelsPage {
await this.centerView.postMessage(message, files);
}
async replyToLastPost(message: string) {
const rootPost = await this.getLastPost();
await rootPost.reply();
const sidebarRight = this.sidebarRight;
await sidebarRight.toBeVisible();
await sidebarRight.postMessage('Replying to a thread');
// * Verify the message has been sent
await waitUntil(
async () => {
const post = await this.sidebarRight.getLastPost();
const content = await post.container.textContent();
return content?.includes(message);
},
{timeout: duration.ten_sec},
);
const lastPost = await sidebarRight.getLastPost();
return {rootPost, sidebarRight, lastPost};
}
async openChannelSettings(): Promise<SettingsModal> {
await this.centerView.header.openChannelMenu();
await this.page.locator('#channelSettings[role="menuitem"]').click();
@@ -144,4 +175,28 @@ export default class ChannelsPage {
return popover;
}
async scheduleMessage(message: string, dayFromToday: number = 0, timeOptionIndex: number = 0) {
await this.centerView.postCreate.writeMessage(message);
await expect(this.centerView.postCreate.scheduleMessageButton).toBeVisible();
await this.centerView.postCreate.scheduleMessageButton.click();
await this.scheduleMessageMenu.toBeVisible();
await this.scheduleMessageMenu.selectCustomTime();
return await this.scheduleMessageModal.scheduleMessage(dayFromToday, timeOptionIndex);
}
async scheduleMessageFromThread(message: string, dayFromToday: number = 0, timeOptionIndex: number = 0) {
await this.sidebarRight.postCreate.writeMessage(message);
await expect(this.sidebarRight.postCreate.scheduleMessageButton).toBeVisible();
await this.sidebarRight.postCreate.scheduleMessageButton.click();
await this.scheduleMessageMenu.toBeVisible();
await this.scheduleMessageMenu.selectCustomTime();
return await this.scheduleMessageModal.scheduleMessage(dayFromToday, timeOptionIndex);
}
}

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

@@ -3,125 +3,52 @@
import {Page, expect} from '@playwright/test';
export default class DraftPage {
import {components} from '@/ui/components';
export default class DraftsPage {
readonly page: Page;
readonly draftsHeader;
readonly tab;
readonly confirmbutton;
readonly datePattern;
readonly deleteIcon;
readonly deleteIconToolTip;
readonly noscheduledDraftIcon;
readonly scheduleIcon;
readonly rescheduleIconToolTip;
readonly draftBody;
readonly scheduledDraftPageInfo;
readonly scheduledDraftPanel;
readonly scheduledDraftSendNowButton;
readonly scheduledDraftSendNowButtonToolTip;
readonly badge;
readonly noDrafts;
readonly scheduleMessageModal;
constructor(page: Page) {
this.page = page;
this.draftBody = page.locator('div.post__body');
this.scheduleIcon = page.locator('#draft_icon-clock-send-outline_reschedule');
this.datePattern =
/(Today|Tomorrow|(?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}) at \d{1,2}:\d{2} [AP]M/;
this.draftsHeader = page.locator('.Drafts__header');
this.tab = page.getByRole('tab', {name: 'Drafts'});
this.scheduledDraftPageInfo = page.locator('.PanelHeader__info');
this.scheduledDraftPanel = (messageContent: string) =>
page.locator(`article.Panel:has(div.post__body:has-text("${messageContent}"))`);
this.deleteIcon = page.locator('#draft_icon-trash-can-outline_delete');
this.deleteIconToolTip = page.locator('text=Delete scheduled post');
this.rescheduleIconToolTip = page.locator('text=Schedule draft');
this.noscheduledDraftIcon = page.locator('.no-results__wrapper');
this.scheduledDraftSendNowButton = page.locator('#draft_icon-send-outline_sendNow');
this.scheduledDraftSendNowButtonToolTip = page.locator('text=Send now');
this.confirmbutton = this.page.locator('button.btn-primary');
this.badge = this.tab.locator('span.MuiBadge-badge');
this.noDrafts = page.locator('.no-results__wrapper');
this.scheduleMessageModal = new components.ScheduleMessageModal(
page.getByRole('dialog', {name: 'Schedule message'}),
);
}
async goTo(teamName: string) {
async goto(teamName: string) {
await this.page.goto(`/${teamName}/drafts`);
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await expect(this.page).toHaveURL(/.*drafts/);
await this.draftsHeader.isVisible();
await expect(this.tab).toHaveAttribute('aria-selected', 'true');
}
async assertBadgeCountOnTab(badgeCount: string) {
await this.tab.isVisible();
async getBadgeCountOnTab() {
await expect(this.tab).toBeVisible();
const badge = this.tab.locator('span.MuiBadge-badge');
await expect(badge).toBeVisible();
await expect(badge).toHaveText(badgeCount);
return await badge.textContent();
}
async assertDraftBody(draftMessage: string) {
await expect(this.draftBody).toBeVisible();
await expect(this.draftBody).toHaveText(draftMessage);
}
async verifyOnHoverActionItems(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifyDeleteIcon();
await this.verifyScheduleIcon(messageContent);
await this.verifySendNowIcon();
}
async verifyDeleteIcon() {
await this.deleteIcon.isVisible();
await this.deleteIcon.hover();
await expect(this.deleteIconToolTip).toBeVisible();
await expect(this.deleteIconToolTip).toHaveText('Delete scheduled post');
}
async verifyScheduleIcon(messageContent: string) {
await this.scheduledDraftPanel(messageContent).hover();
await expect(this.scheduleIcon).toBeVisible();
await this.scheduleIcon.hover();
await expect(this.rescheduleIconToolTip).toBeVisible();
await expect(this.rescheduleIconToolTip).toHaveText('Schedule draft');
}
async verifySendNowIcon() {
await this.scheduledDraftSendNowButton.isVisible();
await this.scheduledDraftSendNowButton.hover();
await expect(this.scheduledDraftSendNowButtonToolTip).toBeVisible();
await expect(this.scheduledDraftSendNowButtonToolTip).toHaveText('Send now');
}
async getTimeStampOfMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
await this.scheduledDraftPanel(messageContent).isVisible();
return this.scheduledDraftPanel(messageContent).locator(this.scheduledDraftPageInfo).innerHTML();
}
async openScheduleModal(messageContent: string) {
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.scheduleIcon.hover();
await expect(this.rescheduleIconToolTip).toBeVisible();
await expect(this.rescheduleIconToolTip).toHaveText('Schedule draft');
await this.scheduleIcon.click();
}
async deleteScheduledMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifyDeleteIcon();
await this.deleteIcon.click();
expect(await this.confirmbutton.textContent()).toEqual('Yes, delete');
await this.confirmbutton.click();
}
async sendScheduledMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifySendNowIcon();
await this.scheduledDraftSendNowButton.click();
expect(await this.confirmbutton.textContent()).toEqual('Yes, send now');
await this.confirmbutton.click();
async getLastPost() {
const lastPost = this.page.getByTestId('draftView').last();
await lastPost.waitFor();
return new components.DraftPost(lastPost);
}
}

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

@@ -7,8 +7,8 @@ import LoginPage from './login';
import ResetPasswordPage from './reset_password';
import SignupPage from './signup';
import SystemConsolePage from './system_console';
import ScheduledDraftPage from './scheduled_draft';
import DraftPage from './drafts';
import ScheduledPostsPage from './scheduled_posts';
import DraftsPage from './drafts';
const pages = {
ChannelsPage,
@@ -16,19 +16,19 @@ const pages = {
LoginPage,
ResetPasswordPage,
SignupPage,
ScheduledDraftPage,
ScheduledPostsPage,
SystemConsolePage,
DraftPage,
DraftsPage,
};
export {
pages,
ChannelsPage,
DraftsPage,
LandingLoginPage,
LoginPage,
ResetPasswordPage,
SignupPage,
ScheduledDraftPage,
ScheduledPostsPage,
SystemConsolePage,
DraftPage,
};

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

@@ -1,161 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Page, expect} from '@playwright/test';
export default class ScheduledDraftPage {
readonly page: Page;
readonly tab;
readonly confirmbutton;
readonly copyIcon;
readonly copyIconToolTip;
readonly datePattern;
readonly deleteIcon;
readonly deleteIconToolTip;
readonly noscheduledDraftIcon;
readonly rescheduleIcon;
readonly rescheduleIconToolTip;
readonly scheduledDraftBody;
readonly scheduledDraftPageInfo;
readonly scheduledDraftPanel;
readonly scheduledDraftSendNowButton;
readonly scheduledDraftSendNowButtonToolTip;
readonly editIcon;
readonly editBox;
readonly editorSaveButton;
constructor(page: Page) {
this.page = page;
this.datePattern =
/(Today|Tomorrow|(?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}) at \d{1,2}:\d{2} [AP]M/;
this.scheduledDraftBody = page.locator('div.post__body');
this.tab = page.getByRole('tab', {name: 'Scheduled'});
this.scheduledDraftPageInfo = page.locator('.PanelHeader__info');
this.scheduledDraftPanel = (messageContent: string) =>
page.locator(`article.Panel:has(div.post__body:has-text("${messageContent}"))`);
this.deleteIcon = page.locator('#draft_icon-trash-can-outline_delete');
this.deleteIconToolTip = page.locator('text=Delete scheduled post');
this.copyIcon = page.locator('#draft_icon-content-copy_copy_text');
this.copyIconToolTip = page.locator('text=Copy text');
this.rescheduleIcon = page.locator('#draft_icon-clock-send-outline_reschedule');
this.rescheduleIconToolTip = page.locator('text=Reschedule post');
this.noscheduledDraftIcon = page.locator('.no-results__wrapper');
this.scheduledDraftSendNowButton = page.locator('#draft_icon-send-outline_sendNow');
this.scheduledDraftSendNowButtonToolTip = page.locator('text=Send now');
this.confirmbutton = this.page.locator('button.btn-primary');
this.editIcon = page.locator('#draft_icon-pencil-outline_edit');
this.editBox = page.locator('textarea#edit_textbox');
this.editorSaveButton = page.locator('button.save');
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await expect(this.page).toHaveURL(/.*scheduled_posts/);
}
async assertBadgeCountOnTab(badgeCount: string) {
await this.tab.isVisible();
const badge = this.tab.locator('span.MuiBadge-badge');
await expect(badge).toBeVisible();
await expect(badge).toHaveText(badgeCount);
}
async assertscheduledDraftBody(draftMessage: string) {
await expect(this.scheduledDraftBody).toBeVisible();
await expect(this.scheduledDraftBody).toHaveText(draftMessage);
}
async verifyOnHoverActionItems(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifyDeleteIcon();
await this.verifyCopyIcon();
await this.verifyRescheduleIcon();
await this.verifySendNowIcon();
}
async verifyDeleteIcon() {
await this.deleteIcon.isVisible();
await this.deleteIcon.hover();
await expect(this.deleteIconToolTip).toBeVisible();
await expect(this.deleteIconToolTip).toHaveText('Delete scheduled post');
}
async verifyCopyIcon() {
await this.copyIcon.isVisible();
await this.copyIcon.hover();
await expect(this.copyIconToolTip).toBeVisible();
await expect(this.copyIconToolTip).toHaveText('Copy text');
}
async verifyRescheduleIcon() {
await expect(this.rescheduleIcon).toBeVisible();
await this.rescheduleIcon.hover();
await expect(this.rescheduleIconToolTip).toBeVisible();
await expect(this.rescheduleIconToolTip).toHaveText('Reschedule post');
}
async verifySendNowIcon() {
await this.scheduledDraftSendNowButton.isVisible();
await this.scheduledDraftSendNowButton.hover();
await expect(this.scheduledDraftSendNowButtonToolTip).toBeVisible();
await expect(this.scheduledDraftSendNowButtonToolTip).toHaveText('Send now');
}
async getTimeStampOfMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
await this.scheduledDraftPanel(messageContent).isVisible();
return this.scheduledDraftPanel(messageContent).locator(this.scheduledDraftPageInfo).innerHTML();
}
async openRescheduleModal(messageContent: string) {
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.rescheduleIcon.hover();
await expect(this.rescheduleIconToolTip).toBeVisible();
await expect(this.rescheduleIconToolTip).toHaveText('Reschedule post');
await this.rescheduleIcon.click();
}
async deleteScheduledMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifyDeleteIcon();
await this.deleteIcon.click();
expect(await this.confirmbutton.textContent()).toEqual('Yes, delete');
await this.confirmbutton.click();
}
async sendScheduledMessage(messageContent: string) {
await this.scheduledDraftPanel(messageContent).isVisible();
await this.scheduledDraftPanel(messageContent).hover();
await this.verifySendNowIcon();
await this.scheduledDraftSendNowButton.click();
expect(await this.confirmbutton.textContent()).toEqual('Yes, send now');
await this.confirmbutton.click();
}
async goTo(teamName: string) {
await this.page.goto(`/${teamName}/scheduled_posts`);
}
async editText(newText: string) {
await this.editIcon.click();
await this.editBox.isVisible();
await this.editBox.fill(newText);
await this.editorSaveButton.isVisible();
await this.editorSaveButton.click();
await this.editBox.isHidden();
await this.scheduledDraftPanel(newText).isVisible();
}
async copyScheduledMessage(draftMessage: string) {
await this.scheduledDraftPanel(draftMessage).isVisible();
await this.scheduledDraftPanel(draftMessage).hover();
await this.verifyCopyIcon();
await this.copyIcon.click();
}
}

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

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Page, expect} from '@playwright/test';
import {components} from '@/ui/components';
import type {ScheduledPost} from '@/ui/components';
export default class ScheduledPostsPage {
readonly page: Page;
readonly draftsHeader;
readonly tab;
readonly badge;
readonly noScheduledDrafts;
readonly scheduleMessageModal;
readonly sendMessageNowModal;
readonly deleteScheduledPostModal;
constructor(page: Page) {
this.page = page;
this.draftsHeader = page.locator('.Drafts__header');
this.tab = page.getByRole('tab', {name: 'Scheduled'});
this.badge = this.tab.locator('span.MuiBadge-badge');
this.noScheduledDrafts = page.locator('.no-results__wrapper');
this.scheduleMessageModal = new components.ScheduleMessageModal(
page.getByRole('dialog', {name: 'Schedule message'}),
);
this.sendMessageNowModal = new components.SendMessageNowModal(
page.getByRole('dialog', {name: 'Send message now'}),
);
this.deleteScheduledPostModal = new components.DeleteScheduledPostModal(
page.getByRole('dialog', {name: 'Delete scheduled post'}),
);
}
async toBeVisible() {
await expect(this.page).toHaveURL(/.*scheduled_posts/);
await this.draftsHeader.isVisible();
await expect(this.tab).toHaveAttribute('aria-selected', 'true');
}
async getBadgeCountOnTab() {
await expect(this.tab).toBeVisible();
const badge = this.tab.locator('span.MuiBadge-badge');
await expect(badge).toBeVisible();
return await badge.textContent();
}
async getLastPost() {
const lastPost = this.page.getByTestId('scheduledPostView').last();
await lastPost.waitFor();
return new components.ScheduledPost(lastPost);
}
async getLastPostID() {
return this.page.getByTestId('scheduledPostView').last().getAttribute('data-postid');
}
async getNthPost(index: number) {
const nthPost = this.page.getByTestId('scheduledPostView').nth(index);
await nthPost.waitFor();
return new components.ScheduledPost(nthPost);
}
async rescheduleMessage(post: ScheduledPost, dayFromToday: number = 0, timeOptionIndex: number = 0) {
await post.hover();
await expect(post.rescheduleButton).toBeVisible();
await post.rescheduleButton.click();
return await this.scheduleMessageModal.scheduleMessage(dayFromToday, timeOptionIndex);
}
async goto(teamName: string) {
await this.page.goto(`/${teamName}/scheduled_posts`);
}
}

2508
e2e-tests/playwright/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -1,6 +1,8 @@
{
"name": "mattermost-playwright",
"workspaces": ["lib"],
"workspaces": [
"lib"
],
"scripts": {
"postinstall": "script/post_install.sh && npm run build",
"build": "npm run build --workspaces",
@@ -27,9 +29,11 @@
"@playwright/test": "1.52.0",
"cross-env": "7.0.3",
"dayjs": "1.11.13",
"luxon": "3.6.1",
"zod": "3.24.3"
},
"devDependencies": {
"@types/luxon": "3.6.2",
"@typescript-eslint/eslint-plugin": "8.30.1",
"eslint": "9.25.0",
"eslint-import-resolver-typescript": "4.3.3",

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

@@ -36,7 +36,7 @@ export default defineConfig({
slowMo: testConfig.slowMo,
},
screenshot: 'only-on-failure',
timezoneId: 'America/Los_Angeles',
timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,
trace: 'off',
video: 'retain-on-failure',
actionTimeout: duration.half_min,

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

@@ -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(), '', '');
const testUser2 = await adminClient.createUser(pw.random.user('other'), '', '');
await adminClient.addToTeam(team.id, testUser2.id);
// 1. Login as the first user
@@ -40,7 +40,7 @@ test('Profile popover should show correct fields after at-mention autocomplete @
const currentUserProfilePopover = channelsPage.userProfilePopover;
// * Verify all fields are visible for current user in the profile popover
await expect(currentUserProfilePopover.container.getByText(`@${user.username}`)).toBeVisible(); // TODO: Fix this
await expect(currentUserProfilePopover.container.getByText(`@${user.username}`)).toBeVisible();
await expect(currentUserProfilePopover.container.getByText(`${user.first_name} ${user.last_name}`)).toBeVisible();
await expect(currentUserProfilePopover.container.getByText(user.email)).toBeVisible();

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

@@ -1,411 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Page} from '@playwright/test';
import {expect, PlaywrightExtended, test} from '@mattermost/playwright-lib';
import type {ChannelsPage, ScheduledDraftPage} from '@mattermost/playwright-lib';
test.skip('MM-T5643_1 should create a scheduled message from a channel', async ({pw}) => {
test.setTimeout(pw.duration.four_min);
const draftMessage = 'Scheduled Draft';
// # Skip test if no license
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
const {page, channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage);
await scheduleMessage(channelsPage);
await channelsPage.centerView.verifyscheduledDraftChannelInfo();
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
// # Hover and verify options
await scheduledDraftPage.verifyOnHoverActionItems(draftMessage);
// # Go back and wait for message to arrive
await goBackToChannelAndWaitForMessageToArrive(pw, page);
// * Verify the message has been sent and there's no more scheduled messages
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(draftMessage);
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
});
test('MM-T5643_6 should create a scheduled message under a thread post ', async ({pw}) => {
const draftMessage = 'Scheduled Threaded Message';
// # Skip test if no license
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
// # Visit default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
await channelsPage.postMessage('Root Message');
// # Start a thread by clicking on reply menuitem from post options menu
const post = await channelsPage.getLastPost();
await replyToLastPost(post);
const sidebarRight = channelsPage.sidebarRight;
await sidebarRight.toBeVisible();
// # Post a message in the thread
await sidebarRight.postMessage('Replying to a thread');
// # Write a message in the reply thread but don't send it
await sidebarRight.postCreate.writeMessage(draftMessage);
await expect(sidebarRight.postCreate.input).toHaveText(draftMessage);
await sidebarRight.postCreate.scheduleDraftMessageButton.isVisible();
await sidebarRight.postCreate.scheduleDraftMessageButton.click();
await scheduleMessage(channelsPage);
await sidebarRight.postBoxIndicator.isVisible();
const messageLocator = sidebarRight.scheduledDraftChannelInfoMessage.first();
await expect(messageLocator).toContainText('Message scheduled for');
// Save the time displayed in the thread
const scheduledDraftThreadedPanelInfo = await sidebarRight.postBoxIndicator.innerText();
await channelsPage.sidebarRight.clickOnSeeAllscheduledDrafts();
const scheduledDraftPageInfo = await scheduledDraftPage.scheduledDraftPageInfo.innerHTML();
await channelsPage.sidebarLeft.assertscheduledDraftCountLHS('1');
await scheduledDraftPage.toBeVisible();
await scheduledDraftPage.assertBadgeCountOnTab('1');
await scheduledDraftPage.assertscheduledDraftBody(draftMessage);
await compareMessageTimestamps(scheduledDraftThreadedPanelInfo, scheduledDraftPageInfo, scheduledDraftPage);
// # Hover and verify options
await scheduledDraftPage.verifyOnHoverActionItems(draftMessage);
await scheduledDraftPage.sendScheduledMessage(draftMessage);
await sidebarRight.toBeVisible();
(await sidebarRight.getLastPost()).toContainText(draftMessage);
await expect(channelsPage.sidebarRight.scheduledDraftChannelInfoMessage.first()).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
});
test('MM-T5644 should reschedule a scheduled message', async ({pw}) => {
const draftMessage = 'Scheduled Draft';
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
const {channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage);
await scheduleMessage(channelsPage);
// * Verify the Initial Date and time of scheduled Draft
await channelsPage.centerView.verifyscheduledDraftChannelInfo();
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
await scheduledDraftPage.openRescheduleModal(draftMessage);
// # Reschedule it to 2 days from today
await channelsPage.scheduledDraftModal.selectDay(2);
await channelsPage.scheduledDraftModal.confirm();
// # Note the new Scheduled time
const scheduledDraftPageInfo = await scheduledDraftPage.getTimeStampOfMessage(draftMessage);
// # Go to Channel
await channelsPage.goto();
// * Verify the New Time reflecting in the channel
const rescheduledDraftChannelInfo = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await compareMessageTimestamps(rescheduledDraftChannelInfo, scheduledDraftPageInfo, scheduledDraftPage);
});
test('MM-T5645 should delete a scheduled message', async ({pw}) => {
const draftMessage = 'Scheduled Draft';
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
const {channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage);
await scheduleMessage(channelsPage);
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
await scheduledDraftPage.deleteScheduledMessage(draftMessage);
await expect(scheduledDraftPage.scheduledDraftPanel(draftMessage)).not.toBeVisible();
await expect(scheduledDraftPage.noscheduledDraftIcon).toBeVisible();
});
test('MM-T5643_9 should send a scheduled message immediately', async ({pw}) => {
const draftMessage = 'Scheduled Draft';
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
const {channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage);
await scheduleMessage(channelsPage);
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
await scheduledDraftPage.sendScheduledMessage(draftMessage);
await pw.wait(pw.duration.two_sec);
await expect(scheduledDraftPage.scheduledDraftPanel(draftMessage)).not.toBeVisible();
// Verify message has arrived
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(draftMessage);
});
test('MM-T5643_3 should create a scheduled message from a DM', async ({pw}) => {
const draftMessage = 'Scheduled Draft';
// # Skip test if no license
await pw.skipIfNoLicense();
const {user, team} = await pw.initSetup();
const {user: user2} = await pw.initSetup();
const {page, channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage, team.name, `@${user2.username}`);
await scheduleMessage(channelsPage);
await channelsPage.centerView.goToScheduledDraftsFromDMChannel();
// # Hover and verify options
await scheduledDraftPage.verifyOnHoverActionItems(draftMessage);
await scheduledDraftPage.sendScheduledMessage(draftMessage);
await page.waitForSelector(channelsPage.centerView.scheduledDraftChannelInfoMessageLocator, {state: 'hidden'});
// * Verify the message has been sent and there's no more scheduled messages
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(draftMessage);
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
});
test('MM-T5648 should create a draft and then schedule it', async ({pw}) => {
const draftMessage = 'Draft to be Scheduled';
await pw.skipIfNoLicense();
const {user, team} = await pw.initSetup();
const {channelsPage, draftPage, scheduledDraftPage} = await pw.testBrowser.login(user);
// await setupChannelPage(channelsPage, draftMessage);
await channelsPage.goto();
await channelsPage.toBeVisible();
await channelsPage.centerView.postCreate.writeMessage(draftMessage);
// go to drafts page
await draftPage.goTo(team.name);
await draftPage.toBeVisible();
await draftPage.assertBadgeCountOnTab('1');
await draftPage.assertDraftBody(draftMessage);
await draftPage.verifyScheduleIcon(draftMessage);
await draftPage.openScheduleModal(draftMessage);
// # Reschedule it to 2 days from today
await channelsPage.scheduledDraftModal.selectDay(2);
await channelsPage.scheduledDraftModal.confirm();
await scheduledDraftPage.goTo(team.name);
await scheduledDraftPage.toBeVisible();
await scheduledDraftPage.assertBadgeCountOnTab('1');
await scheduledDraftPage.assertscheduledDraftBody(draftMessage);
});
test('MM-T5644 should edit scheduled message', async ({pw}) => {
const draftMessage = 'Scheduled Draft';
// # Skip test if no license
await pw.skipIfNoLicense();
const {user} = await pw.initSetup();
const {page, channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage);
await scheduleMessage(channelsPage);
await channelsPage.centerView.verifyscheduledDraftChannelInfo();
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
// # Hover and verify options
await scheduledDraftPage.verifyOnHoverActionItems(draftMessage);
const updatedText = 'updated text';
await scheduledDraftPage.editText(updatedText);
await scheduledDraftPage.sendScheduledMessage(updatedText);
// * Verify the message has been sent and there's no more scheduled messages
await page.waitForSelector(channelsPage.centerView.scheduledDraftChannelInfoMessageLocator, {state: 'hidden'});
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(updatedText);
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
});
test('MM-T5650 should copy scheduled message', async ({pw, browserName}) => {
// Skip this test in Firefox clipboard permissions are not supported
test.skip(browserName === 'firefox', 'Test not supported in Firefox');
// # Skip test if no license
await pw.skipIfNoLicense();
const draftMessage = 'Scheduled Draft';
const {user} = await pw.initSetup();
const {page, channelsPage, scheduledDraftPage} = await pw.testBrowser.login(user);
await setupChannelPage(channelsPage, draftMessage, '', 'town-square');
await scheduleMessage(channelsPage);
await channelsPage.centerView.verifyscheduledDraftChannelInfo();
const postBoxIndicator = await channelsPage.centerView.scheduledDraftChannelInfoMessageText.innerText();
await verifyScheduledDraft(channelsPage, scheduledDraftPage, draftMessage, postBoxIndicator);
await scheduledDraftPage.copyScheduledMessage(draftMessage);
await page.goBack();
await channelsPage.centerView.postCreate.input.focus();
await page.keyboard.down('ControlOrMeta');
await page.keyboard.press('V');
await page.keyboard.up('ControlOrMeta');
// * Assert the message typed is same as the copied message
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
});
async function goBackToChannelAndWaitForMessageToArrive(pw: PlaywrightExtended, page: Page): Promise<void> {
await page.goBack();
await pw.wait(pw.duration.two_min);
await page.reload();
}
async function replyToLastPost(post: any): Promise<void> {
await post.hover();
await post.postMenu.toBeVisible();
await post.postMenu.reply();
}
async function setupChannelPage(
channelsPage: ChannelsPage,
draftMessage: string,
teamName?: string,
channelName?: string,
): Promise<void> {
await channelsPage.goto(teamName, channelName);
await channelsPage.toBeVisible();
await channelsPage.centerView.postCreate.writeMessage(draftMessage);
await channelsPage.centerView.postCreate.clickOnScheduleDraftDropdownButton();
}
/**
* Schedules a draft message by selecting a custom time and confirming.
*/
async function scheduleMessage(channelsPage: ChannelsPage): Promise<void> {
await channelsPage.scheduledDraftDropdown.toBeVisible();
await channelsPage.scheduledDraftDropdown.selectCustomTime();
await channelsPage.scheduledDraftModal.toBeVisible();
await channelsPage.scheduledDraftModal.selectDay();
await channelsPage.scheduledDraftModal.selectTime();
await channelsPage.scheduledDraftModal.confirm();
}
/**
* Extracts and verifies the scheduled message on the scheduled page and in the channel.
*/
async function verifyScheduledDraft(
channelsPage: ChannelsPage,
scheduledDraftPage: ScheduledDraftPage,
draftMessage: string,
postBoxIndicator: string,
): Promise<void> {
await verifyscheduledDraftCount(channelsPage, '1');
await scheduledDraftPage.toBeVisible();
await scheduledDraftPage.assertBadgeCountOnTab('1');
await scheduledDraftPage.assertscheduledDraftBody(draftMessage);
const scheduledDraftPageInfo = await scheduledDraftPage.getTimeStampOfMessage(draftMessage);
await compareMessageTimestamps(postBoxIndicator, scheduledDraftPageInfo, scheduledDraftPage);
}
/**
* Verifies the scheduled message count on the sidebar.
*/
async function verifyscheduledDraftCount(page: ChannelsPage, expectedCount: string): Promise<void> {
await page.centerView.clickOnSeeAllscheduledDrafts();
await page.sidebarLeft.assertscheduledDraftCountLHS(expectedCount);
}
/**
* Compares the time in the channel and the scheduled page to ensure consistency.
*/
async function compareMessageTimestamps(
timeInChannel: string,
scheduledDraftPageInfo: string,
scheduledDraftPage: ScheduledDraftPage,
): Promise<void> {
// Extract time from channel using the same date pattern
const matchedTimeInChannel = timeInChannel.match(scheduledDraftPage.datePattern);
const timeInSchedulePage = extractTimeFromHtml(scheduledDraftPageInfo, scheduledDraftPage);
if (!matchedTimeInChannel || !timeInSchedulePage) {
throw new Error('Could not extract date and time from one or both elements.');
}
const firstElementTime = matchedTimeInChannel[0];
const secondElementTime = timeInSchedulePage[0];
// Compare extracted times
expect(firstElementTime).toBe(secondElementTime);
}
/**
* Removes HTML tags from the scheduled message content and extracts the time pattern.
*/
function extractTimeFromHtml(htmlContent: string, scheduledDraftPage: ScheduledDraftPage): RegExpMatchArray | null {
// Remove all HTML tags and match the expected time pattern using the datePattern from scheduledDraftPage
const cleanedText = htmlContent.replace(/<\/?[^>]+(>|$)/g, '');
// Use the datePattern to extract the exact match for time
const matchedTime = cleanedText.match(scheduledDraftPage.datePattern);
return matchedTime;
}

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

@@ -0,0 +1,519 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test, ScheduledPostIndicator} from '@mattermost/playwright-lib';
import type {ChannelsPage, ScheduledPostsPage} from '@mattermost/playwright-lib';
test.beforeEach(async ({pw}) => {
// Ensure license but skip test if no license which is required for "Scheduled Drafts"
await pw.ensureLicense();
await pw.skipIfNoLicense();
});
/**
* @objective Verify the ability to create a scheduled message from a channel.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5643_1 should create a scheduled message from a channel', {tag: '@scheduled_messages'}, async ({pw}) => {
// Set test timeout to 4 mins to wait for the scheduled message to be sent
// which is expected within 2 mins.
test.setTimeout(pw.duration.four_min);
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user} = await pw.initSetup();
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
// * Verify scheduled post indicator with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Click "See all link" to navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`;
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
// 4. Go back to the channels page
await page.goBack();
// * Verify the message has been posted and there's no more scheduled messages
await pw.waitUntil(
async () => {
const post = await channelsPage.getLastPost();
const content = await post.container.textContent();
return content?.includes(draftMessage);
},
{timeout: pw.duration.two_min},
);
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
});
/**
* @objective Verify the ability to create a scheduled message in a thread.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5643_6 should create a scheduled message under a thread post', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Threaded Message ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Post a message
await channelsPage.postMessage('Root Message');
// 3. Reply to a message
const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread');
// 4. Create a scheduled message from the thread
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
// * Verify scheduled post indicator with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage);
// 5. Navigate to scheduled posts page
await sidebarRight.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 6. Hover over the scheduled post and send now
await scheduledPost.hover();
await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify the message has been posted and there's no more scheduled messages
await sidebarRight.toBeVisible();
const lastPost = await sidebarRight.getLastPost();
await expect(lastPost.body).toContainText(draftMessage);
await sidebarRight.scheduledPostIndicator.toBeNotVisible();
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
});
/**
* @objective Verify the ability to reschedule a scheduled message.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 4. Reschedule message to 2 days from today
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} = await scheduledPostsPage.rescheduleMessage(
scheduledPost,
2,
);
// 5. Return to channel page
await channelsPage.goto();
// * Verify the message shows updated scheduled time
const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage);
});
/**
* @objective Verify the ability to delete a scheduled message.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// 3. Navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 4. Delete the scheduled message
await scheduledPost.hover();
await scheduledPost.deleteButton.click();
await scheduledPostsPage.deleteScheduledPostModal.toBeVisible();
await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click();
// * Verify the scheduled message is removed from the scheduled posts page
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
});
/**
* @objective Verify the ability to send a scheduled message immediately.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5643_9 should send a scheduled message immediately', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user, townSquareUrl} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// 3. Navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 4. Send the scheduled message immediately
await scheduledPost.hover();
await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages
await expect(channelsPage.page).toHaveURL(townSquareUrl);
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toContainText(draftMessage);
});
/**
* @objective Verify the ability to create a scheduled message from a direct message (DM).
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5643_3 should create a scheduled message from a DM', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user and another user
const {user, team, adminClient} = await pw.initSetup();
const otherUser = await adminClient.createUser(pw.random.user(), '', '');
// 2. Login the first user and navigate to a DM channel with the other user
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, `@${otherUser.username}`);
await channelsPage.toBeVisible();
// 3. Create a scheduled message with 1 day offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time
let indicatorMessage;
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
indicatorMessage = 'You have one scheduled message.';
} else {
indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
}
await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage);
// 4. Navigate to scheduled posts page
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click();
} else {
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
}
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 5. Send the scheduled message immediately
await scheduledPost.hover();
await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the DM channel, message is posted and there's no more scheduled messages
await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toContainText(draftMessage);
});
/**
* @objective Verify the ability to convert a draft message to a scheduled message.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5648 should create a draft and then schedule it', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user, team} = await pw.initSetup();
const {channelsPage, draftsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a draft message
await channelsPage.centerView.postCreate.input.fill(draftMessage);
// 3. Go to drafts page
await draftsPage.goto(team.name);
await draftsPage.toBeVisible();
expect(await draftsPage.getBadgeCountOnTab()).toBe('1');
// * Verify draft message exists
const draftedPost = await draftsPage.getLastPost();
await expect(draftedPost.panelBody).toContainText(draftMessage);
// 4. Open schedule modal from draft and schedule it to the next 2 days
await draftedPost.hover();
await draftedPost.scheduleButton.click();
await draftsPage.scheduleMessageModal.toBeVisible();
const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2);
// 5. Navigate to scheduled posts page
await scheduledPostsPage.goto(team.name);
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
});
/**
* @objective Verify the ability to edit a scheduled message before it's sent.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user, townSquareUrl} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message with 2 days offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
// * Verify scheduled message indicator appears with correct date/time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 4. Hover and click edit button
await scheduledPost.hover();
await scheduledPost.editButton.click();
// 5. Edit the scheduled message
const updatedText = 'updated text';
await scheduledPost.editTextBox.fill(updatedText);
await scheduledPost.saveButton.click();
// 6. Verify the edited message appears in the channel
await expect(scheduledPost.panelBody).toContainText(updatedText);
await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`);
// 7. Send the message immediately
await scheduledPost.hover();
await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages
await expect(channelsPage.page).toHaveURL(townSquareUrl);
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(updatedText);
});
/**
* @objective Verify the ability to copy a scheduled message to clipboard.
*
* @precondition
* A test server with valid license to support scheduled message features
*/
test('MM-T5650 should copy scheduled message', {tag: '@scheduled_messages'}, async ({pw, browserName}) => {
// Skip this test in Firefox clipboard permissions are not supported
test.skip(browserName === 'firefox', 'Test not supported in Firefox');
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel
const {user} = await pw.initSetup();
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled post badge in left sidebar shows correct count
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage,
sendOnMessage,
badgeCountOnTab: 1,
});
// 4. Copy the scheduled message
await scheduledPost.hover();
await scheduledPost.copyTextButton.click();
// 5. Return to channel page
await page.goBack();
// 6. Paste the copied message in post creator
await channelsPage.centerView.postCreate.input.focus();
await page.keyboard.down('ControlOrMeta');
await page.keyboard.press('V');
await page.keyboard.up('ControlOrMeta');
// * Verify the copied message is pasted in the post input box
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
});
/**
* Verifies that the scheduled post indicator is visible and displays the correct date and time.
*
* @param scheduledPostIndicator - The ScheduledPostIndicator instance
* @param messageText - A post message
*/
async function verifyScheduledPostIndicator(scheduledPostIndicator: ScheduledPostIndicator, messageText: string) {
await scheduledPostIndicator.toBeVisible();
await expect(scheduledPostIndicator.icon).toBeVisible();
await expect(scheduledPostIndicator.messageText).toContainText(messageText);
}
async function verifyScheduledPostBadgeOnLeftSidebar(channelsPage: ChannelsPage, count: number) {
await channelsPage.sidebarLeft.scheduledPostBadge.isVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).toHaveText(count.toString());
}
async function verifyScheduledPost(
scheduledPostsPage: ScheduledPostsPage,
{
draftMessage,
sendOnMessage,
badgeCountOnTab,
}: {draftMessage: string; sendOnMessage: string; badgeCountOnTab: number},
) {
// * Verify scheduled posts page is visible
await scheduledPostsPage.toBeVisible();
// * Verify scheduled post badge on tab has correct count
expect(await scheduledPostsPage.getBadgeCountOnTab()).toBe(badgeCountOnTab.toString());
// * Verify scheduled post appears in scheduled posts page
const scheduledPost = await scheduledPostsPage.getLastPost();
await expect(scheduledPost.panelBody).toContainText(draftMessage);
await expect(scheduledPost.panelHeader).toContainText(sendOnMessage);
return scheduledPost;
}

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

@@ -13,8 +13,8 @@ test('should be able to scroll the RHS with page up and down', async ({pw}) => {
await channelsPage.toBeVisible();
await channelsPage.centerView.postCreate.postMessage('post');
const lastPost = await channelsPage.centerView.getLastPost();
await lastPost.openRhs();
const lastPost = await channelsPage.getLastPost();
await lastPost.reply();
for (let i = 0; i < 10; i++) {
await channelsPage.sidebarRight.postCreate.postMessage('a\n'.repeat(10));

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

@@ -98,7 +98,10 @@ export default function ScheduledPostIndicator({location, channelId, postId, rem
}
return (
<div className='ScheduledPostIndicator'>
<div
className='ScheduledPostIndicator'
data-testid='scheduledPostIndicator'
>
<i
data-testid='scheduledPostIcon'
className='icon icon-draft-indicator icon-clock-send-outline'

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

@@ -18,7 +18,10 @@ export function ShortScheduledPostIndicator({scheduledPostData, scheduledPostLin
}
return (
<div className='ScheduledPostIndicator'>
<div
className='ScheduledPostIndicator'
data-testid='scheduledPostIndicator'
>
<FormattedMessage
id='scheduled_post.channel_indicator.with_other_user_late_time'
defaultMessage='You have {count, plural, =1 {one} other {#}} <a>scheduled {count, plural, =1 {message} other {messages}}</a>.'

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

@@ -115,6 +115,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
const optionTomorrow = (
<Menu.Item
key={'scheduling_time_tomorrow_9_am'}
data-testid='scheduling_time_tomorrow_9_am'
onClick={tomorrowClickHandler}
labels={
<FormattedMessage
@@ -134,6 +135,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
const optionNextMonday = (
<Menu.Item
key={'scheduling_time_next_monday_9_am'}
data-testid='scheduling_time_next_monday_9_am'
onClick={nextMondayClickHandler}
labels={
<FormattedMessage
@@ -150,6 +152,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
const optionMonday = (
<Menu.Item
key={'scheduling_time_monday_9_am'}
data-testid='scheduling_time_monday_9_am'
onClick={nextMondayClickHandler}
labels={
<FormattedMessage

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

@@ -118,6 +118,7 @@ function RecentUsedCustomDate({handleOnSelect, userCurrentTimezone, nextMonday,
<Menu.Separator key='recent_custom_separator'/>
<Menu.Item
key='recently_used_custom_time'
data-testid='recently_used_custom_time'
onClick={handleRecentlyUsedCustomTime}
labels={timestamp}
className='core-menu-options'

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

@@ -204,6 +204,7 @@ const DateTimeInputContainer: React.FC<Props> = ({
onToggle={setIsInteracting}
>
<button
data-testid='time_button'
className='style--none'
ref={timeButtonRef}
>
@@ -228,6 +229,7 @@ const DateTimeInputContainer: React.FC<Props> = ({
<Menu.Group>
{Array.isArray(timeOptions) && timeOptions.map((option, index) => (
<Menu.ItemAction
id={`time_option_${index}`}
onClick={handleTimeChange.bind(this, option)}
key={index}
text={

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

@@ -58,6 +58,8 @@ type Props = {
isRemote?: boolean;
scrollIntoView?: boolean;
containerClassName?: string;
dataTestId?: string;
dataPostId?: string;
}
const mockLastBlurAt = {current: 0};
@@ -360,8 +362,12 @@ function DraftRow({
);
}
const kind = isScheduledPost ? 'scheduledPost' : 'draft';
return (
<Panel
dataTestId={`${kind}View`}
dataPostId={(item as ScheduledPost).id}
onClick={goToMessage}
hasError={Boolean(postError)}
innerRef={scrollIntoView ? alertRef : undefined}
@@ -371,7 +377,7 @@ function DraftRow({
{({hover}) => (
<>
<Header
kind={isScheduledPost ? 'scheduledPost' : 'draft'}
kind={kind}
hover={hover}
actions={actions}
title={title}

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

@@ -16,6 +16,8 @@ type Props = {
isHighlighted?: boolean;
style?: React.CSSProperties;
className?: string;
dataTestId?: string;
dataPostId?: string;
};
const isEligibleForClick = makeIsEligibleForClick('.hljs, code');
@@ -28,6 +30,8 @@ function Panel({
isHighlighted,
style,
className,
dataTestId,
dataPostId,
}: Props) {
const [hover, setHover] = useState(false);
@@ -47,6 +51,8 @@ function Panel({
return (
<article
data-testid={dataTestId}
data-postid={dataPostId}
className={classNames(
'Panel',
{