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 удалений

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

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