E2E/Playwright: Refactor and fix tests for scheduled posts (#30871)
Этот коммит содержится в:
@@ -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();
|
||||
}
|
||||
}
|
||||
80
e2e-tests/playwright/lib/src/ui/pages/scheduled_posts.ts
Обычный файл
80
e2e-tests/playwright/lib/src/ui/pages/scheduled_posts.ts
Обычный файл
@@ -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
сгенерированный
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',
|
||||
{
|
||||
|
||||
Ссылка в новой задаче
Block a user