mirror of
https://github.com/hanzoai/team.git
synced 2026-11-01 07:55:31 +00:00
* Add weekly recurring scheduled posts. Extend scheduled posts so users can schedule weekly repeats and keep the series healthy across sends, reschedules, and UI updates instead of falling back to one-shot behavior. Made-with: Cursor * Add Playwright coverage for recurring scheduled posts. Cover weekly recurring scheduled messages in the scheduled-messages spec so the recurring UI and reschedule flow stay protected without adding a separate test surface. Made-with: Cursor * Fix recurring scheduled post CI failures. Resolve the initial lint and formatting issues and renumber the new scheduled-post migration so it no longer collides with master during Postgres-backed test setup. Made-with: Cursor * Sync recurring scheduled post translation files. Regenerate the affected English translation catalogs so the recurring scheduled post strings match the source extraction order expected by CI. Made-with: Cursor * Fix recurring scheduled post review follow-ups. Preserve overdue cleanup behavior during weekly catch-up, defer delete websocket events until deletion succeeds, and address the remaining migration and UI review nits. Made-with: Cursor * Address recurring scheduled post review feedback Made-with: Cursor * Fix recurring scheduled post CI checks Made-with: Cursor * Make pending scheduled post keyset cursor index-scannable EXPLAIN ANALYZE on a 5M-row ScheduledPosts table showed the pure OR-form cursor forced Postgres to scan idx_scheduledposts_pending_scheduled_at_id from the top on every page (~383ms/page, ~2M rows filtered). Keeping the ScheduledAt <= beforeTime bound outside the tie-break restores the index boundary (~0.12ms/page). Adds storetest coverage for cursor pagination. Co-authored-by: nick.misasi <[email protected]> * Address code quality review findings for recurring scheduled posts Server: - Replace silent-fallback AdvanceWeeklyScheduledNextOccurrence with error-returning ScheduledPost.ComputeNextScheduledAt; a recurring post whose timezone fails to load is now routed through the failed-post path instead of being reposted every job run or silently deleted - Add ScheduledPost.IsRecurring and partition batches in processScheduledPostBatch; advance/delete now run independently so a store failure in one path can't cause reposts in the other - Drop dead generality in UpdateRecurringScheduledPosts (only ScheduledAt varies per row; ErrorCode/ProcessedAt are constants) - Simplify redundant repeat-type condition in GetPendingScheduledPosts Webapp: - Add shared isRecurringScheduledPost helper, replacing six scattered repeat_type === 'weekly' literals - Recurrence timezone is now simply the scheduler's current timezone; removes initialRepeatTimezone/effectiveTimezone plumbing and the modal-label/picker timezone mismatch - Single enforcement point for hiding send-now on recurring posts - Use canonical getTeamIdByChannelId at both SCHEDULED_POST_UPDATED dispatch sites; rewrite errorsByTeamId update case as remove-then-add and add reducer tests Co-authored-by: nick.misasi <[email protected]> * Simplify recurring scheduled post code per review - End a recurring series when its channel no longer exists instead of advancing it forever (matches the one-shot channel-not-found handling) - Collapse the errorsByTeamId SCHEDULED_POST_UPDATED case into the identical SINGLE_SCHEDULED_POST_RECEIVED case (a scheduled post's team can't change) and combine duplicate byId cases; preserves state references on no-op updates - Drop no-op timezone conversions in ComputeNextScheduledAt - Remove redundant checkbox aria-label (label htmlFor already names it) - Schedule new job tests in the past instead of sleeping a real second - Remove redundant test assignment and e2e positional boolean Co-authored-by: nick.misasi <[email protected]> * Remove slop from recurring scheduled post changes - Drop the business-rule CHECK constraint from the recurrence migration; no other migration enforces model-layer validation in the database, and BaseIsValid plus the job's failure handling already own it - Fold the standalone repeat-validation test file into the existing TestScheduledPostBaseIsValid, matching its conventions - Revert unrelated benchmark modernization in utils_test.go - Drop an unneeded cast, unused fixture fields, and naming/assertion inconsistencies in tests Co-authored-by: nick.misasi <[email protected]> * Regenerate ScheduledPostStore mock with mockery ordering Co-authored-by: nick.misasi <[email protected]> * Address CodeRabbit review feedback - Reject the host-dependent 'Local' value for RepeatTimezone; recurring schedules need a fixed zone (UTC or IANA name) - Hide reschedule for deactivated DMs, matching send-now eligibility Co-authored-by: nick.misasi <[email protected]> * Resolve scheduled post team bucket from existing state when channel is unloaded An admin can reschedule before fetchMissingChannels resolves, in which case deriving the team from the channel returns undefined and the update was misfiled under directChannels. getScheduledPostTeamId falls back to the byTeamId bucket that already holds the post. Co-authored-by: nick.misasi <[email protected]> * Retrigger CI after transient enterprise npm network failure Co-authored-by: nick.misasi <[email protected]> * Clear hover/focus before asserting scheduled post header details in e2e The drafts panel hides its timestamp/tag info section while hovered or focus-within; after the reschedule modal closes, focus returns to the row and the 'Repeats weekly' tag assertion saw a hidden element. Co-authored-by: nick.misasi <[email protected]> * Move recurrence columns into baseColumns and dispatch ComputeNextScheduledAt on repeat type Co-authored-by: nick.misasi <[email protected]> * Preserve recurrence when scheduled post updates omit repeat fields Co-authored-by: nick.misasi <[email protected]> * Disallow file attachments on recurring scheduled posts Co-authored-by: nick.misasi <[email protected]> * Stop pinning the channel indicator for recurring-only scheduled posts Co-authored-by: nick.misasi <[email protected]> * Re-home nullable-Type comment onto baseColumns Co-authored-by: nick.misasi <[email protected]> * Gate recurring scheduled posts behind a default-off feature flag Co-authored-by: nick.misasi <[email protected]> * Move presence-preservation rationale to the copy site Co-authored-by: nick.misasi <[email protected]> * Extract draftHasAttachments helper and require allowRecurring at single-caller layers Co-authored-by: nick.misasi <[email protected]> * Return null from the indicator selector when nothing should show Co-authored-by: nick.misasi <[email protected]> * Gate only recurrence transitions and preserve existing series in the modal Co-authored-by: nick.misasi <[email protected]> * Avoid err shadowing in scheduled post update handler Co-authored-by: nick.misasi <[email protected]> * Disable the repeat weekly checkbox with a tooltip when the message has attachments Co-authored-by: nick.misasi <[email protected]> * Return an explicit disposition from postScheduledPost The batch loop inferred 'channel permanently gone' from the one return path with a nil error and an error code set - an invariant a future change could silently break, deleting recurring series by accident. postScheduledPost now returns posted/failed/unsendable explicitly, the switch refuses to delete on an unhandled disposition, and a test pins that both recurring and one-shot posts in a nonexistent channel are permanently deleted. Co-authored-by: nick.misasi <[email protected]> * Fix Repeat weekly attachments tooltip centering on the modal row (#37927) Shrink-wrap the repeat checkbox row so WithTooltip anchors to the control/label instead of the full modal body width. --------- Co-authored-by: Hanzo Dev <[email protected]>
741 lines
29 KiB
TypeScript
741 lines
29 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {expect, test} from '@mattermost/playwright-lib';
|
|
import type {ChannelsPage, ScheduledPostsPage, ScheduledPostIndicator} 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.fixme(
|
|
'MM-T5643_1 creates scheduled message from channel and posts at scheduled time',
|
|
{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()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message with short delay
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
|
|
|
|
// * Verify scheduled post indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// * Verify scheduled post badge in left sidebar shows count of 1
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
|
|
// # Navigate to scheduled posts page via "See all" link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
await verifyScheduledPost(scheduledPostsPage, {draftMessage, selectedDate, selectedTime, badgeCountOnTab: 1});
|
|
|
|
// # Return to the channels page
|
|
await page.goBack();
|
|
|
|
// * Verify scheduled message was posted successfully
|
|
await pw.waitUntil(
|
|
async () => {
|
|
const post = await channelsPage.getLastPost();
|
|
const content = await post.container.textContent();
|
|
|
|
return content?.includes(draftMessage);
|
|
},
|
|
{timeout: pw.duration.two_min},
|
|
);
|
|
|
|
// * Verify scheduled indicators are removed after posting
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
|
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @objective Verify that a weekly recurring scheduled message does not pin the channel indicator above the composer.
|
|
*
|
|
* @precondition
|
|
* A test server with valid license and the RecurringScheduledPosts feature flag enabled
|
|
*/
|
|
test(
|
|
'creates weekly recurring scheduled message from channel without showing the channel indicator',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
await pw.skipIfFeatureFlagNotSet('RecurringScheduledPosts', true);
|
|
|
|
const draftMessage = `Weekly Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize test user, login and navigate to a channel
|
|
const {user, team} = await pw.initSetup();
|
|
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
|
await channelsPage.goto();
|
|
await channelsPage.toBeVisible();
|
|
|
|
// # Create a weekly recurring scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1, 0, true);
|
|
|
|
// * Verify scheduled post badge appears with count of 1
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
|
|
// * Verify the channel indicator stays hidden since every scheduled post is recurring
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
|
|
// # Navigate to scheduled posts page
|
|
await scheduledPostsPage.goto(team.name);
|
|
|
|
// * Verify scheduled post appears with recurring weekly details
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
repeatWeekly: true,
|
|
});
|
|
|
|
// * Verify one-time send now control is not available for recurring scheduled posts
|
|
await expect(scheduledPost.sendNowButton).toHaveCount(0);
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @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 creates scheduled message in thread and posts in thread conversation',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Threaded Message ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a root message in the channel
|
|
await channelsPage.postMessage('Root Message');
|
|
|
|
// # Start a thread by replying to the message
|
|
const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread');
|
|
|
|
// # Create a scheduled message within the thread
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
|
|
|
|
// * Verify scheduled post indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// # Navigate to scheduled posts page using indicator link
|
|
await sidebarRight.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Send the scheduled message immediately
|
|
await scheduledPost.hover();
|
|
await scheduledPost.sendNowButton.click();
|
|
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
|
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
|
|
|
// * Verify message is posted in the thread
|
|
await sidebarRight.toBeVisible();
|
|
const lastPost = await sidebarRight.getLastPost();
|
|
await expect(lastPost.body).toContainText(draftMessage);
|
|
|
|
// * Verify all scheduled message indicators are removed
|
|
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_2 reschedules message to a future date from scheduled posts page',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
|
|
|
// * Verify scheduled message indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// * Verify scheduled post badge appears with count of 1
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
|
|
// # Navigate to scheduled posts page via indicator link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Reschedule the message to a different date (2 days from now)
|
|
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} =
|
|
await scheduledPostsPage.rescheduleMessage(scheduledPost, 2);
|
|
|
|
// # Return to channel page
|
|
await channelsPage.goto();
|
|
|
|
// * Verify indicator shows the updated scheduled time
|
|
await verifyScheduledPostIndicator(
|
|
channelsPage.centerView.scheduledPostIndicator,
|
|
newSelectedDate,
|
|
newSelectedTime,
|
|
);
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @objective Verify rescheduling a weekly recurring scheduled message keeps its weekly recurrence.
|
|
*
|
|
* @precondition
|
|
* A test server with valid license and the RecurringScheduledPosts feature flag enabled
|
|
*/
|
|
test(
|
|
'reschedules weekly recurring scheduled message from scheduled posts page and keeps weekly recurrence',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
await pw.skipIfFeatureFlagNotSet('RecurringScheduledPosts', true);
|
|
|
|
const draftMessage = `Weekly Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize test user, login and navigate to a channel
|
|
const {user, team} = await pw.initSetup();
|
|
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
|
await channelsPage.goto();
|
|
await channelsPage.toBeVisible();
|
|
|
|
// # Create a weekly recurring scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1, 0, true);
|
|
|
|
// # Navigate to scheduled posts page
|
|
await scheduledPostsPage.goto(team.name);
|
|
|
|
// * Verify scheduled post appears with recurring weekly details
|
|
let scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
repeatWeekly: true,
|
|
});
|
|
|
|
// # Open the reschedule modal for the recurring scheduled message
|
|
const scheduleMessageModal = await scheduledPostsPage.openRescheduleMessageModal(scheduledPost);
|
|
|
|
// * Verify the weekly recurrence option remains selected in the reschedule modal
|
|
await expect(scheduleMessageModal.repeatWeeklyCheckbox).toBeChecked();
|
|
|
|
// # Reschedule the recurring message to a different future date
|
|
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} =
|
|
await scheduleMessageModal.scheduleMessage(2);
|
|
|
|
// * Verify the rescheduled post still appears as a weekly recurring message
|
|
scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate: newSelectedDate,
|
|
selectedTime: newSelectedTime,
|
|
badgeCountOnTab: 1,
|
|
repeatWeekly: true,
|
|
});
|
|
await expect(scheduledPost.sendNowButton).toHaveCount(0);
|
|
|
|
// # Return to channel page
|
|
await channelsPage.goto();
|
|
|
|
// * Verify the channel indicator stays hidden since every scheduled post is recurring
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @objective Verify the ability to delete a scheduled message.
|
|
*
|
|
* @precondition
|
|
* A test server with valid license to support scheduled message features
|
|
*/
|
|
test(
|
|
'MM-T5645 deletes scheduled message from scheduled posts page and removes all indicators',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
|
|
|
// * Verify scheduled message indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// # Navigate to scheduled posts page via indicator link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Delete the scheduled message
|
|
await scheduledPost.hover();
|
|
await scheduledPost.deleteButton.click();
|
|
|
|
// # Confirm deletion in the modal
|
|
await scheduledPostsPage.deleteScheduledPostModal.toBeVisible();
|
|
await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click();
|
|
|
|
// * Verify the scheduled message is removed and no longer appears
|
|
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 sends scheduled message immediately from scheduled posts page',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
|
|
|
// * Verify scheduled message indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// # Navigate to scheduled posts page via indicator link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Send the scheduled message immediately instead of waiting
|
|
await scheduledPost.hover();
|
|
await scheduledPost.sendNowButton.click();
|
|
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
|
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
|
|
|
// * Verify page redirects to the channel
|
|
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
|
|
|
// * Verify scheduled indicators are removed
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
|
|
|
// * Verify message was posted in the channel
|
|
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 creates scheduled message from DM channel and posts at scheduled time',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize test setup with main user and create a second user
|
|
const {user, team, adminClient} = await pw.initSetup();
|
|
const otherUser = await adminClient.createUser(await pw.random.user(), '', '');
|
|
|
|
// # Login as first user and navigate to DM channel with second user
|
|
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
|
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
|
await channelsPage.toBeVisible();
|
|
|
|
// # Create a scheduled message for tomorrow in the DM
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
|
|
|
// * Verify appropriate scheduled message indicator appears
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
|
|
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
|
// Special case for timezone - expect generic message
|
|
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(
|
|
'You have one scheduled message.',
|
|
);
|
|
} else {
|
|
// Normal case - verify the scheduled indicator
|
|
await verifyScheduledPostIndicator(
|
|
channelsPage.centerView.scheduledPostIndicator,
|
|
selectedDate,
|
|
selectedTime,
|
|
);
|
|
}
|
|
|
|
// # Navigate to scheduled posts page using appropriate link
|
|
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
|
await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click();
|
|
} else {
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
}
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Send the scheduled message immediately instead of waiting
|
|
await scheduledPost.hover();
|
|
await scheduledPost.sendNowButton.click();
|
|
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
|
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
|
|
|
// * Verify page redirects to the DM channel
|
|
await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
|
|
|
|
// * Verify scheduled indicators are removed
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
|
|
|
// * Verify message was posted in the DM channel
|
|
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 converts draft message to scheduled message from drafts page',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a draft message without sending it
|
|
await channelsPage.centerView.postCreate.input.fill(draftMessage);
|
|
|
|
// # Navigate to the drafts page
|
|
await draftsPage.goto(team.name);
|
|
await draftsPage.toBeVisible();
|
|
|
|
// * Verify draft count badge shows one draft
|
|
expect(await draftsPage.getBadgeCountOnTab()).toBe('1');
|
|
|
|
// * Verify draft message content appears correctly
|
|
const draftedPost = await draftsPage.getLastPost();
|
|
await expect(draftedPost.panelBody).toContainText(draftMessage);
|
|
|
|
// # Schedule the draft for 2 days in the future
|
|
await draftedPost.hover();
|
|
await draftedPost.scheduleButton.click();
|
|
await draftsPage.scheduleMessageModal.toBeVisible();
|
|
const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2);
|
|
|
|
// # Navigate to scheduled posts page
|
|
await scheduledPostsPage.goto(team.name);
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
await verifyScheduledPost(scheduledPostsPage, {draftMessage, selectedDate, selectedTime, 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_1 edits scheduled message content while preserving scheduled time',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw}) => {
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message for 2 days in the future
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
|
|
|
|
// * Verify scheduled message indicator shows correct date and time
|
|
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
|
|
|
// * Verify scheduled post badge shows count of 1
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
|
|
// # Navigate to scheduled posts page via indicator link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Edit the scheduled message content
|
|
await scheduledPost.hover();
|
|
await scheduledPost.editButton.click();
|
|
const updatedText = 'updated text';
|
|
await scheduledPost.editTextBox.fill(updatedText);
|
|
await scheduledPost.saveButton.click();
|
|
|
|
// * Verify the edited message content is updated
|
|
await expect(scheduledPost.panelBody).toContainText(updatedText);
|
|
|
|
// * Verify scheduled date/time remains unchanged
|
|
await expect(scheduledPost.panelHeader).toContainText(selectedTime);
|
|
|
|
// # Send the edited message immediately
|
|
await scheduledPost.hover();
|
|
await scheduledPost.sendNowButton.click();
|
|
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
|
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
|
|
|
// * Verify page redirects to the channel
|
|
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
|
|
|
// * Verify scheduled indicators are removed
|
|
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
|
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
|
|
|
// * Verify edited message was posted in the channel
|
|
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 copies scheduled message text to clipboard for reuse',
|
|
{tag: '@scheduled_messages'},
|
|
async ({pw, browserName}) => {
|
|
// # Skip this test in Firefox since clipboard permissions are not supported
|
|
test.skip(browserName === 'firefox', 'Test not supported in Firefox');
|
|
|
|
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
|
|
|
// # Initialize 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();
|
|
|
|
// # Create a scheduled message for tomorrow
|
|
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
|
|
|
// * Verify scheduled post badge shows count of 1
|
|
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
|
|
|
// # Navigate to scheduled posts page via indicator link
|
|
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
|
|
|
// * Verify scheduled post appears with correct information
|
|
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
|
draftMessage,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab: 1,
|
|
});
|
|
|
|
// # Copy the scheduled message text to clipboard
|
|
await scheduledPost.hover();
|
|
await scheduledPost.copyTextButton.click();
|
|
|
|
// # Return to the channel page
|
|
await page.goBack();
|
|
|
|
// # Paste the copied message into the post input box
|
|
await channelsPage.centerView.postCreate.input.focus();
|
|
await page.keyboard.down('ControlOrMeta');
|
|
await page.keyboard.press('V');
|
|
await page.keyboard.up('ControlOrMeta');
|
|
|
|
// * Verify the clipboard content was pasted correctly
|
|
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Verifies that the scheduled post indicator shows the correct date and time.
|
|
*/
|
|
async function verifyScheduledPostIndicator(
|
|
scheduledPostIndicator: ScheduledPostIndicator,
|
|
selectedDate: string,
|
|
selectedTime: string | null,
|
|
) {
|
|
await scheduledPostIndicator.toBeVisible();
|
|
await expect(scheduledPostIndicator.icon).toBeVisible();
|
|
|
|
if (!selectedTime) {
|
|
throw new Error('selectedTime is required');
|
|
}
|
|
|
|
// Verify the indicator contains both the time and a valid date
|
|
const messageText = await scheduledPostIndicator.messageText.textContent();
|
|
await expect(scheduledPostIndicator.messageText).toContainText(selectedTime);
|
|
const datePatterns = [
|
|
selectedDate, // Original date
|
|
'Today',
|
|
'Tomorrow',
|
|
];
|
|
|
|
const hasValidDate = datePatterns.some((pattern) => messageText?.toLowerCase().includes(pattern.toLowerCase()));
|
|
|
|
if (!hasValidDate) {
|
|
throw new Error(
|
|
`Indicator text "${messageText}" does not contain any expected date pattern: ${datePatterns.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
selectedDate,
|
|
selectedTime,
|
|
badgeCountOnTab,
|
|
repeatWeekly = false,
|
|
}: {
|
|
draftMessage: string;
|
|
selectedDate: string;
|
|
selectedTime: string | null;
|
|
badgeCountOnTab: number;
|
|
repeatWeekly?: boolean;
|
|
},
|
|
) {
|
|
// * Verify scheduled posts page is visible
|
|
await scheduledPostsPage.toBeVisible();
|
|
|
|
// Clear hover and focus (e.g. left behind by the reschedule modal); the panel hides its
|
|
// timestamp/tag info section while hovered or focused.
|
|
await scheduledPostsPage.page.mouse.move(0, 0);
|
|
await scheduledPostsPage.page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
|
|
|
|
// * 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);
|
|
|
|
if (!selectedTime) {
|
|
throw new Error('selectedTime is required');
|
|
}
|
|
|
|
// Verify the header contains both the time and a valid date
|
|
const headerText = await scheduledPost.panelHeader.textContent();
|
|
await expect(scheduledPost.panelHeader).toContainText(selectedTime);
|
|
const datePatterns = [
|
|
selectedDate, // Original date
|
|
'Today',
|
|
'Tomorrow',
|
|
];
|
|
|
|
const hasValidDate = datePatterns.some((pattern) => headerText?.toLowerCase().includes(pattern.toLowerCase()));
|
|
|
|
if (!hasValidDate) {
|
|
throw new Error(
|
|
`Header "${headerText}" does not contain any expected date pattern: ${datePatterns.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
if (repeatWeekly) {
|
|
await expect(scheduledPost.repeatsWeeklyTag).toBeVisible();
|
|
}
|
|
|
|
return scheduledPost;
|
|
}
|