mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
MM-66742 - add BoR e2e tests (#34829)
* MM-66742 - add BoR e2e tests * polish bor test code * fix linter issues * Align BoR e2e tests with Playwright page object patterns * fix linter * Fix sender timer assertion and sidebar nav after reload
This commit is contained in:
parent
f0b2a36dbc
commit
2ce50d7c8d
16 changed files with 2328 additions and 2 deletions
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class BurnOnReadBadge {
|
||||
readonly container: Locator;
|
||||
readonly flameIcon: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.flameIcon = container.locator('svg');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeHidden() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.container.click();
|
||||
}
|
||||
|
||||
async hover() {
|
||||
await this.container.hover();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tooltip/label text
|
||||
* Uses aria-label which contains the same information as the visible tooltip
|
||||
*/
|
||||
async getTooltipText(): Promise<string> {
|
||||
// The aria-label contains the full tooltip information
|
||||
// e.g., "Click to delete message for everyone. Read by 1 of 2 recipients"
|
||||
const ariaLabel = await this.container.getAttribute('aria-label');
|
||||
if (ariaLabel) {
|
||||
return ariaLabel;
|
||||
}
|
||||
|
||||
// Fallback: try to get text content
|
||||
return (await this.container.textContent()) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aria-label for accessibility testing
|
||||
*/
|
||||
async getAriaLabel(): Promise<string> {
|
||||
return (await this.container.getAttribute('aria-label')) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse recipient count from tooltip
|
||||
* Returns {revealed: number, total: number}
|
||||
*/
|
||||
async getRecipientCount(): Promise<{revealed: number; total: number}> {
|
||||
const tooltipText = await this.getTooltipText();
|
||||
const match = tooltipText.match(/Read by (\d+) of (\d+)/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Could not parse recipient count from tooltip: ${tooltipText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
revealed: parseInt(match[1], 10),
|
||||
total: parseInt(match[2], 10),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class BurnOnReadConcealedPlaceholder {
|
||||
readonly container: Locator;
|
||||
readonly icon: Locator;
|
||||
readonly text: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
// The container itself is the button - no need for nested locator
|
||||
this.icon = container.locator('.BurnOnReadConcealedPlaceholder__icon');
|
||||
this.text = container.locator('.BurnOnReadConcealedPlaceholder__text');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeHidden() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click to reveal the concealed message
|
||||
* The container itself is the clickable button
|
||||
*/
|
||||
async clickToReveal() {
|
||||
await this.container.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the reveal process to complete
|
||||
* The placeholder should disappear after successful reveal
|
||||
*/
|
||||
async waitForReveal(timeout = 5000) {
|
||||
await expect(this.container).not.toBeVisible({timeout});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the placeholder text (e.g., "View message")
|
||||
*/
|
||||
async getText(): Promise<string> {
|
||||
return (await this.text.textContent()) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aria-label of the button
|
||||
*/
|
||||
async getAriaLabel(): Promise<string> {
|
||||
return (await this.container.getAttribute('aria-label')) || '';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class BurnOnReadConfirmationModal {
|
||||
readonly container: Locator;
|
||||
readonly title: Locator;
|
||||
readonly message: Locator;
|
||||
readonly deleteButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
readonly dontShowAgainCheckbox: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
// Modal elements
|
||||
this.title = container.locator('.modal-title, h1, [role="heading"]').first();
|
||||
this.message = container.locator('.modal-body, .modal-message').first();
|
||||
|
||||
// Action buttons - use flexible selectors
|
||||
this.deleteButton = container.getByRole('button', {name: /delete|burn|confirm/i});
|
||||
this.cancelButton = container.getByRole('button', {name: /cancel/i});
|
||||
|
||||
// Checkbox for "don't show again" preference
|
||||
this.dontShowAgainCheckbox = container.getByRole('checkbox');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeHidden() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm deletion without checking "don't show again"
|
||||
*/
|
||||
async confirm() {
|
||||
await this.deleteButton.click();
|
||||
await this.toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm deletion and check "don't show again"
|
||||
*/
|
||||
async confirmWithDontShowAgain() {
|
||||
await this.dontShowAgainCheckbox.check();
|
||||
await this.deleteButton.click();
|
||||
await this.toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the deletion
|
||||
*/
|
||||
async cancel() {
|
||||
await this.cancelButton.click();
|
||||
await this.toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modal title text
|
||||
*/
|
||||
async getTitleText(): Promise<string> {
|
||||
return (await this.title.textContent()) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modal message text
|
||||
*/
|
||||
async getMessageText(): Promise<string> {
|
||||
return (await this.message.textContent()) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "don't show again" checkbox is present
|
||||
*/
|
||||
async hasDontShowAgainOption(): Promise<boolean> {
|
||||
return await this.dontShowAgainCheckbox.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "don't show again" is already checked
|
||||
*/
|
||||
async isDontShowAgainChecked(): Promise<boolean> {
|
||||
return await this.dontShowAgainCheckbox.isChecked();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
export default class BurnOnReadTimerChip {
|
||||
readonly container: Locator;
|
||||
readonly flameIcon: Locator;
|
||||
readonly timerText: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.flameIcon = container.locator('.BurnOnReadTimerChip__icon');
|
||||
this.timerText = container.locator('.BurnOnReadTimerChip__time');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeHidden() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.container.click();
|
||||
}
|
||||
|
||||
async hover() {
|
||||
await this.container.hover();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayed time remaining (e.g., "0:45", "1:30")
|
||||
*/
|
||||
async getTimeRemaining(): Promise<string> {
|
||||
return (await this.timerText.textContent()) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse time remaining into seconds
|
||||
*/
|
||||
async getTimeRemainingInSeconds(): Promise<number> {
|
||||
const timeText = await this.getTimeRemaining();
|
||||
const parts = timeText.split(':');
|
||||
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(`Invalid timer format: ${timeText}`);
|
||||
}
|
||||
|
||||
const minutes = parseInt(parts[0], 10);
|
||||
const seconds = parseInt(parts[1], 10);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timer is in warning state (last 30 seconds)
|
||||
*/
|
||||
async isWarning(): Promise<boolean> {
|
||||
const className = await this.container.getAttribute('class');
|
||||
return className?.includes('BurnOnReadTimerChip--warning') || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timer has expired
|
||||
*/
|
||||
async isExpired(): Promise<boolean> {
|
||||
const className = await this.container.getAttribute('class');
|
||||
return className?.includes('BurnOnReadTimerChip--expired') || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tooltip text
|
||||
*/
|
||||
async getTooltipText(): Promise<string> {
|
||||
await this.hover();
|
||||
|
||||
// Wait for tooltip to appear
|
||||
const tooltip = this.container.page().locator('[role="tooltip"]').first();
|
||||
await tooltip.waitFor({state: 'visible', timeout: 2000});
|
||||
return (await tooltip.textContent()) || '';
|
||||
}
|
||||
}
|
||||
|
|
@ -97,9 +97,12 @@ export default class ChannelsCenterView {
|
|||
/**
|
||||
* Returns the Center post by post's id
|
||||
* @param postId Just the ID without the prefix
|
||||
* Note: Handles both simple posts (post_id) and combined posts (post_id:timestamp)
|
||||
*/
|
||||
async getPostById(id: string) {
|
||||
const postById = this.container.locator(`[id="post_${id}"]`);
|
||||
// Match either exact ID or ID with timestamp suffix (for combined posts)
|
||||
// Use CSS selector that matches: post_id OR post_id:*
|
||||
const postById = this.container.locator(`[id="post_${id}"], [id^="post_${id}:"]`).first();
|
||||
await postById.waitFor();
|
||||
return new ChannelsPost(postById);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import BurnOnReadBadge from './burn_on_read_badge';
|
||||
import BurnOnReadConcealedPlaceholder from './burn_on_read_concealed_placeholder';
|
||||
import BurnOnReadTimerChip from './burn_on_read_timer_chip';
|
||||
import PostMenu from './post_menu';
|
||||
import ThreadFooter from './thread_footer';
|
||||
|
||||
|
|
@ -17,6 +20,11 @@ export default class ChannelsPost {
|
|||
readonly postMenu;
|
||||
readonly threadFooter;
|
||||
|
||||
// Burn-on-Read elements
|
||||
readonly burnOnReadBadge;
|
||||
readonly burnOnReadTimerChip;
|
||||
readonly concealedPlaceholder;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
|
|
@ -28,6 +36,13 @@ export default class ChannelsPost {
|
|||
|
||||
this.postMenu = new PostMenu(container.locator('.post-menu'));
|
||||
this.threadFooter = new ThreadFooter(container.locator('.ThreadFooter'));
|
||||
|
||||
// Burn-on-Read components
|
||||
this.burnOnReadBadge = new BurnOnReadBadge(container.locator('.BurnOnReadBadge'));
|
||||
this.burnOnReadTimerChip = new BurnOnReadTimerChip(container.locator('.BurnOnReadTimerChip'));
|
||||
this.concealedPlaceholder = new BurnOnReadConcealedPlaceholder(
|
||||
container.locator('.BurnOnReadConcealedPlaceholder'),
|
||||
);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
|
@ -44,7 +59,10 @@ export default class ChannelsPost {
|
|||
async getId() {
|
||||
const id = await this.container.getAttribute('id');
|
||||
expect(id, 'No post ID found.').toBeTruthy();
|
||||
return (id || '').substring('post_'.length);
|
||||
// Remove 'post_' prefix and any timestamp suffix (format: postId:timestamp for combined posts)
|
||||
const postIdWithPossibleTimestamp = (id || '').substring('post_'.length);
|
||||
// Return just the post ID (before any colon)
|
||||
return postIdWithPossibleTimestamp.split(':')[0];
|
||||
}
|
||||
|
||||
async getProfileImage(username: string) {
|
||||
|
|
@ -93,4 +111,28 @@ export default class ChannelsPost {
|
|||
async toNotContainText(text: string) {
|
||||
await expect(this.container).not.toContainText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a burn-on-read post
|
||||
*/
|
||||
async isBurnOnReadPost(): Promise<boolean> {
|
||||
// Check if BoR badge or timer chip is present
|
||||
const hasBadge = await this.burnOnReadBadge.container.isVisible();
|
||||
const hasTimer = await this.burnOnReadTimerChip.container.isVisible();
|
||||
return hasBadge || hasTimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the BoR post is concealed (not yet revealed)
|
||||
*/
|
||||
async isConcealed(): Promise<boolean> {
|
||||
return await this.concealedPlaceholder.container.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the BoR post is revealed
|
||||
*/
|
||||
async isRevealed(): Promise<boolean> {
|
||||
return !(await this.isConcealed());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export default class ChannelsPostCreate {
|
|||
readonly suggestionList;
|
||||
readonly filePreview;
|
||||
|
||||
// Burn-on-Read elements
|
||||
readonly burnOnReadButton;
|
||||
readonly burnOnReadLabel;
|
||||
|
||||
constructor(container: Locator, isRHS = false) {
|
||||
this.container = container;
|
||||
|
||||
|
|
@ -37,6 +41,11 @@ export default class ChannelsPostCreate {
|
|||
this.priorityButton = container.getByLabel('Message priority');
|
||||
this.suggestionList = container.getByRole('listbox', {name: 'Suggestions'});
|
||||
this.filePreview = container.locator('.file-preview__container');
|
||||
|
||||
// Burn-on-Read elements
|
||||
// Use a flexible locator that matches the aria-label pattern
|
||||
this.burnOnReadButton = container.getByRole('button', {name: /Burn-on-read/i});
|
||||
this.burnOnReadLabel = container.locator('.BurnOnReadLabel');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
|
@ -128,4 +137,20 @@ export default class ChannelsPostCreate {
|
|||
{timeout},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the burn-on-read feature for the message
|
||||
*/
|
||||
async toggleBurnOnRead() {
|
||||
await expect(this.burnOnReadButton).toBeVisible();
|
||||
await this.burnOnReadButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if burn-on-read is currently enabled
|
||||
* BoR is considered enabled if the label is visible above the input
|
||||
*/
|
||||
async isBurnOnReadEnabled(): Promise<boolean> {
|
||||
return await this.burnOnReadLabel.isVisible();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ import TeamMenu from './channels/team_menu';
|
|||
import TeamSettingsModal from './channels/team_settings/team_settings_modal';
|
||||
import ThreadFooter from './channels/thread_footer';
|
||||
import UserProfilePopover from './channels/user_profile_popover';
|
||||
// Burn-on-Read Components
|
||||
import BurnOnReadBadge from './channels/burn_on_read_badge';
|
||||
import BurnOnReadTimerChip from './channels/burn_on_read_timer_chip';
|
||||
import BurnOnReadConcealedPlaceholder from './channels/burn_on_read_concealed_placeholder';
|
||||
import BurnOnReadConfirmationModal from './channels/burn_on_read_confirmation_modal';
|
||||
// System Console Components
|
||||
import {AdminSectionPanel, DropdownSetting, RadioSetting, TextInputSetting} from './system_console/base_components';
|
||||
import DelegatedGranularAdministration from './system_console/sections/user_management/delegated_granular_administration';
|
||||
|
|
@ -112,6 +117,12 @@ const components = {
|
|||
ThreadFooter,
|
||||
UserProfilePopover,
|
||||
|
||||
// Burn-on-Read
|
||||
BurnOnReadBadge,
|
||||
BurnOnReadTimerChip,
|
||||
BurnOnReadConcealedPlaceholder,
|
||||
BurnOnReadConfirmationModal,
|
||||
|
||||
// System Console
|
||||
AdminSectionPanel,
|
||||
DelegatedGranularAdministration,
|
||||
|
|
@ -183,6 +194,12 @@ export {
|
|||
ThreadFooter,
|
||||
UserProfilePopover,
|
||||
|
||||
// Burn-on-Read
|
||||
BurnOnReadBadge,
|
||||
BurnOnReadTimerChip,
|
||||
BurnOnReadConcealedPlaceholder,
|
||||
BurnOnReadConfirmationModal,
|
||||
|
||||
// System Console
|
||||
AdminSectionPanel,
|
||||
DelegatedGranularAdministration,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator, Page} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> Site Configuration -> Posts -> Self-Deleting Messages
|
||||
*/
|
||||
export default class SelfDeletingMessages {
|
||||
readonly page: Page;
|
||||
readonly container: Locator;
|
||||
|
||||
readonly enableToggleTrue: Locator;
|
||||
readonly enableToggleFalse: Locator;
|
||||
readonly durationDropdown: Locator;
|
||||
readonly maxTimeToLiveDropdown: Locator;
|
||||
readonly saveButton: Locator;
|
||||
|
||||
constructor(container: Locator, page: Page) {
|
||||
this.container = container;
|
||||
this.page = page;
|
||||
|
||||
this.enableToggleTrue = this.container.getByTestId('ServiceSettings.EnableBurnOnReadtrue');
|
||||
this.enableToggleFalse = this.container.getByTestId('ServiceSettings.EnableBurnOnReadfalse');
|
||||
this.durationDropdown = this.container.getByTestId('ServiceSettings.BurnOnReadDurationSecondsdropdown');
|
||||
this.maxTimeToLiveDropdown = this.container.getByTestId(
|
||||
'ServiceSettings.BurnOnReadMaximumTimeToLiveSecondsdropdown',
|
||||
);
|
||||
this.saveButton = this.container.getByRole('button', {name: 'Save'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async clickEnableToggleTrue() {
|
||||
await this.enableToggleTrue.click();
|
||||
}
|
||||
|
||||
async clickEnableToggleFalse() {
|
||||
await this.enableToggleFalse.click();
|
||||
}
|
||||
|
||||
async selectDuration(value: string) {
|
||||
await this.durationDropdown.selectOption(value);
|
||||
}
|
||||
|
||||
async selectMaxTimeToLive(value: string) {
|
||||
await this.maxTimeToLiveDropdown.selectOption(value);
|
||||
}
|
||||
|
||||
async getDurationValue(): Promise<string> {
|
||||
return await this.durationDropdown.inputValue();
|
||||
}
|
||||
|
||||
async getMaxTimeToLiveValue(): Promise<string> {
|
||||
return await this.maxTimeToLiveDropdown.inputValue();
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return await this.enableToggleTrue.isChecked();
|
||||
}
|
||||
|
||||
async isDurationDropdownDisabled(): Promise<boolean> {
|
||||
return await this.durationDropdown.isDisabled();
|
||||
}
|
||||
|
||||
async isMaxTimeToLiveDropdownDisabled(): Promise<boolean> {
|
||||
return await this.maxTimeToLiveDropdown.isDisabled();
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ export default class ChannelsPage {
|
|||
readonly teamSettingsModal;
|
||||
readonly scheduledDraftModal;
|
||||
readonly scheduleMessageModal;
|
||||
readonly burnOnReadConfirmationModal;
|
||||
readonly archivedChannelMessage;
|
||||
|
||||
readonly postContainer;
|
||||
|
|
@ -79,6 +80,9 @@ export default class ChannelsPage {
|
|||
this.profileModal = new components.ProfileModal(page.getByRole('dialog', {name: 'Profile'}));
|
||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||
this.teamSettingsModal = new components.TeamSettingsModal(page.getByRole('dialog', {name: 'Team Settings'}));
|
||||
this.burnOnReadConfirmationModal = new components.BurnOnReadConfirmationModal(
|
||||
page.getByRole('dialog').filter({hasText: /burn|delete/i}),
|
||||
);
|
||||
|
||||
// Menus
|
||||
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,409 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {BOR_TAG, setupBorTest, createSecondUser} from './support';
|
||||
|
||||
test.describe('Burn-on-Read in DMs and GMs', () => {
|
||||
test('MM-66742_1 BoR toggle is available in DM channel', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create second user for DM
|
||||
const otherUser = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([user.id, otherUser.id]);
|
||||
|
||||
// # Login and navigate to DM
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
||||
|
||||
// * Verify BoR toggle button is available
|
||||
await expect(channelsPage.centerView.postCreate.burnOnReadButton).toBeVisible();
|
||||
|
||||
// # Toggle BoR on
|
||||
await channelsPage.centerView.postCreate.toggleBurnOnRead();
|
||||
|
||||
// * Verify BoR label appears indicating it's enabled
|
||||
await expect(channelsPage.centerView.postCreate.burnOnReadLabel).toBeVisible();
|
||||
});
|
||||
|
||||
test('MM-66742_2 complete BoR flow in DM between two users', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and navigate to DM
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
|
||||
// # Enable BoR and send message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `DM secret ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Get sender's view of the post
|
||||
const senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify sender sees the message content (not concealed)
|
||||
await expect(senderPost.body).toContainText(secretMessage);
|
||||
|
||||
// * Verify sender sees flame badge
|
||||
await expect(senderPost.burnOnReadBadge.container).toBeVisible();
|
||||
|
||||
// # Login as receiver and navigate to DM
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name);
|
||||
await receiverPage.toBeVisible();
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
|
||||
// # Get receiver's view of the post
|
||||
const receiverPost = await receiverPage.getLastPost();
|
||||
|
||||
// * Verify receiver sees concealed placeholder
|
||||
await expect(receiverPost.concealedPlaceholder.container).toBeVisible();
|
||||
|
||||
// * Verify receiver does NOT see the message content
|
||||
await expect(receiverPost.body).not.toContainText(secretMessage);
|
||||
|
||||
// # Reveal the message
|
||||
await receiverPost.concealedPlaceholder.clickToReveal();
|
||||
await receiverPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify receiver now sees the message content
|
||||
await expect(receiverPost.body).toContainText(secretMessage);
|
||||
|
||||
// * Verify timer chip appears (wait for WebSocket update)
|
||||
await expect(receiverPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
});
|
||||
|
||||
test(
|
||||
'MM-66742_3 BoR message in group message with multiple recipients',
|
||||
{tag: [BOR_TAG]},
|
||||
async ({pw}, testInfo) => {
|
||||
testInfo.setTimeout(120000);
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create two other users for the group
|
||||
const recipient1 = await createSecondUser(pw, adminClient, team);
|
||||
const recipient2 = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create a private channel with exactly 3 members (sender + 2 recipients)
|
||||
// This gives us control over the exact member count
|
||||
const channelSuffix = Date.now().toString(36);
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: `bor-gm-test-${channelSuffix}`,
|
||||
displayName: `BoR GM Test ${channelSuffix}`,
|
||||
type: 'P', // Private channel
|
||||
}),
|
||||
);
|
||||
|
||||
// Add all test users to the channel
|
||||
await adminClient.addToChannel(sender.id, channel.id);
|
||||
await adminClient.addToChannel(recipient1.id, channel.id);
|
||||
await adminClient.addToChannel(recipient2.id, channel.id);
|
||||
|
||||
// Remove admin from channel (auto-added as creator)
|
||||
const adminUser = await adminClient.getMe();
|
||||
await adminClient.removeFromChannel(adminUser.id, channel.id);
|
||||
|
||||
// # Login as sender and navigate to the channel
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, channel.name);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Enable BoR and send message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `GM secret ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Get sender's view
|
||||
const senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify sender sees flame badge
|
||||
await expect(senderPost.burnOnReadBadge.container).toBeVisible();
|
||||
|
||||
// * Verify tooltip shows 0 of 2 recipients (channel has 3 members - 1 sender = 2 recipients)
|
||||
const tooltipText = await senderPost.burnOnReadBadge.getTooltipText();
|
||||
expect(tooltipText).toContain('Read by 0 of 2');
|
||||
|
||||
// # Login as first recipient and reveal
|
||||
const {channelsPage: recipient1Page} = await pw.testBrowser.login(recipient1);
|
||||
await recipient1Page.goto(team.name, channel.name);
|
||||
await recipient1Page.toBeVisible();
|
||||
|
||||
const recipient1Post = await recipient1Page.getLastPost();
|
||||
await recipient1Post.concealedPlaceholder.clickToReveal();
|
||||
await recipient1Post.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify first recipient sees message and timer (wait for WebSocket update)
|
||||
await expect(recipient1Post.body).toContainText(secretMessage);
|
||||
await expect(recipient1Post.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Refresh sender and verify updated count
|
||||
await senderPage.page.reload();
|
||||
await senderPage.toBeVisible();
|
||||
const updatedSenderPost = await senderPage.getLastPost();
|
||||
await updatedSenderPost.burnOnReadBadge.hover();
|
||||
const updatedTooltip = await updatedSenderPost.burnOnReadBadge.getTooltipText();
|
||||
expect(updatedTooltip).toContain('Read by 1 of 2');
|
||||
|
||||
// # Login as second recipient and reveal
|
||||
const {channelsPage: recipient2Page} = await pw.testBrowser.login(recipient2);
|
||||
await recipient2Page.goto(team.name, channel.name);
|
||||
await recipient2Page.toBeVisible();
|
||||
|
||||
const recipient2Post = await recipient2Page.getLastPost();
|
||||
await recipient2Post.concealedPlaceholder.clickToReveal();
|
||||
await recipient2Post.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify second recipient sees message and timer (wait for WebSocket update)
|
||||
await expect(recipient2Post.body).toContainText(secretMessage);
|
||||
await expect(recipient2Post.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
},
|
||||
);
|
||||
|
||||
test('MM-66742_4 sender deletes BoR in DM and recipient cannot see it', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and navigate to DM
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
|
||||
// # Enable BoR and send message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `To be deleted ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Get the post ID
|
||||
const senderPost = await senderPage.getLastPost();
|
||||
const postId = await senderPost.getId();
|
||||
|
||||
// # Sender clicks flame badge to delete
|
||||
await senderPost.burnOnReadBadge.click();
|
||||
|
||||
// * Verify confirmation modal appears
|
||||
await expect(senderPage.burnOnReadConfirmationModal.container).toBeVisible();
|
||||
|
||||
// # Confirm deletion
|
||||
await senderPage.burnOnReadConfirmationModal.confirm();
|
||||
|
||||
// * Verify post is removed from sender's view
|
||||
const deletedPostSender = senderPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostSender).not.toBeVisible();
|
||||
|
||||
// # Login as receiver and verify message is not there
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name);
|
||||
await receiverPage.toBeVisible();
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
|
||||
// * Verify the deleted message is not visible to receiver
|
||||
const deletedPostReceiver = receiverPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostReceiver).not.toBeVisible();
|
||||
|
||||
// * Verify message content is not in the channel
|
||||
const channelContent = await receiverPage.centerView.container.textContent();
|
||||
expect(channelContent).not.toContain(secretMessage);
|
||||
});
|
||||
|
||||
test('MM-66742_5 receiver burns revealed BoR in DM via timer chip', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and navigate to DM
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Enable BoR and send message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `Receiver will burn ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Reveal the message
|
||||
const receiverPost = await receiverPage.getLastPost();
|
||||
const postId = await receiverPost.getId();
|
||||
await receiverPost.concealedPlaceholder.clickToReveal();
|
||||
await receiverPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify message is revealed
|
||||
await expect(receiverPost.body).toContainText(secretMessage);
|
||||
|
||||
// * Wait for timer chip to appear (WebSocket update)
|
||||
await expect(receiverPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Click timer to burn
|
||||
await receiverPost.burnOnReadTimerChip.click();
|
||||
|
||||
// * Verify confirmation modal
|
||||
await expect(receiverPage.burnOnReadConfirmationModal.container).toBeVisible();
|
||||
|
||||
// # Confirm
|
||||
await receiverPage.burnOnReadConfirmationModal.confirm();
|
||||
|
||||
// * Verify post is removed from receiver's view
|
||||
const deletedPostReceiver = receiverPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostReceiver).not.toBeVisible({timeout: 15000});
|
||||
|
||||
// * Verify message content is not visible in the channel
|
||||
await expect(receiverPage.centerView.container).not.toContainText(secretMessage);
|
||||
});
|
||||
|
||||
test('MM-66742_6 DM shows correct recipient count of 1', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and navigate to DM
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
|
||||
// # Enable BoR and send message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `Count test ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Get sender's view
|
||||
const senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify tooltip shows exactly 1 recipient
|
||||
await senderPost.burnOnReadBadge.hover();
|
||||
const recipientCount = await senderPost.burnOnReadBadge.getRecipientCount();
|
||||
expect(recipientCount.total).toBe(1);
|
||||
expect(recipientCount.revealed).toBe(0);
|
||||
});
|
||||
|
||||
test('MM-66742_7 multiple BoR messages in same DM conversation', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Send first BoR message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message1 = `First BoR ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message1);
|
||||
|
||||
// # Send second BoR message (toggle again to ensure BoR is on)
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message2 = `Second BoR ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message2);
|
||||
|
||||
// # Login as receiver and verify they can see concealed messages
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// Wait for posts to load - at least one concealed placeholder should be visible
|
||||
await expect(receiverPage.centerView.container.locator('.BurnOnReadConcealedPlaceholder').first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// * Get all concealed placeholders
|
||||
const concealedPlaceholders = receiverPage.centerView.container.locator('.BurnOnReadConcealedPlaceholder');
|
||||
const count = await concealedPlaceholders.count();
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// # Reveal all concealed messages
|
||||
for (let i = 0; i < count; i++) {
|
||||
const placeholder = concealedPlaceholders.nth(i);
|
||||
if (await placeholder.isVisible()) {
|
||||
await placeholder.click();
|
||||
// Wait for reveal animation
|
||||
await receiverPage.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
// * Verify at least one of the messages is visible after revealing
|
||||
const pageContent = await receiverPage.centerView.container.textContent();
|
||||
const hasMessage1 = pageContent?.includes(message1);
|
||||
const hasMessage2 = pageContent?.includes(message2);
|
||||
expect(hasMessage1 || hasMessage2).toBe(true);
|
||||
});
|
||||
|
||||
test('MM-66742_8 BoR toggle resets after sending message', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM channel
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Enable BoR toggle
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
|
||||
// * Verify BoR is enabled
|
||||
const isEnabledBefore = await senderPage.centerView.postCreate.isBurnOnReadEnabled();
|
||||
expect(isEnabledBefore).toBe(true);
|
||||
|
||||
// # Send a message
|
||||
const message1 = `BoR message ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message1);
|
||||
|
||||
// * Verify BoR is disabled after sending (toggle resets)
|
||||
const isEnabledAfter = await senderPage.centerView.postCreate.isBurnOnReadEnabled();
|
||||
expect(isEnabledAfter).toBe(false);
|
||||
|
||||
// * Verify the sent message has BoR badge (was sent as BoR)
|
||||
const lastPost = await senderPage.getLastPost();
|
||||
await expect(lastPost.burnOnReadBadge.container).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {BOR_TAG, setupBorTest, createSecondUser} from './support';
|
||||
|
||||
test.describe('Burn-on-Read Receiver Flow', () => {
|
||||
test('MM-66742_9 receiver sees concealed placeholder and reveals message', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create a DM channel between sender and receiver for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and navigate to DM
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Post BoR message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const secretMessage = `Secret message ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(secretMessage);
|
||||
|
||||
// # Login as receiver in new context
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Get the BoR post
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
|
||||
// * Verify post is concealed (placeholder shown)
|
||||
await expect(borPost.concealedPlaceholder.container).toBeVisible();
|
||||
|
||||
// * Verify the actual message is NOT visible
|
||||
await expect(borPost.body).not.toContainText(secretMessage);
|
||||
|
||||
// * Verify concealed placeholder has correct text
|
||||
const placeholderText = await borPost.concealedPlaceholder.getText();
|
||||
expect(placeholderText).toContain('View message');
|
||||
|
||||
// # Click to reveal
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
|
||||
// * Wait for reveal to complete
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify message is now visible
|
||||
await expect(borPost.body).toContainText(secretMessage);
|
||||
|
||||
// * Verify concealed placeholder is no longer visible
|
||||
await expect(borPost.concealedPlaceholder.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('MM-66742_10 receiver manually burns revealed message via timer chip', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `To be burned ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Get the BoR post and reveal it
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
const postId = await borPost.getId();
|
||||
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify message is revealed
|
||||
await expect(borPost.body).toContainText(message);
|
||||
|
||||
// * Wait for timer chip to appear (WebSocket update)
|
||||
await expect(borPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Click timer chip to manually burn
|
||||
await borPost.burnOnReadTimerChip.click();
|
||||
|
||||
// * Verify confirmation modal appears
|
||||
await expect(receiverPage.burnOnReadConfirmationModal.container).toBeVisible();
|
||||
|
||||
// # Confirm deletion
|
||||
await receiverPage.burnOnReadConfirmationModal.confirm();
|
||||
|
||||
// * Verify post is removed from receiver's view
|
||||
const deletedPostLocator = receiverPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostLocator).not.toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
'MM-66742_11 receiver uses dont show again preference for burn confirmation',
|
||||
{tag: [BOR_TAG]},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Test message ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Reveal the message
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Wait for timer chip to be visible
|
||||
await expect(borPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Click timer to burn
|
||||
await borPost.burnOnReadTimerChip.click();
|
||||
|
||||
// * Verify confirmation modal appears
|
||||
await expect(receiverPage.burnOnReadConfirmationModal.container).toBeVisible();
|
||||
|
||||
// * Verify "don't show again" checkbox is available
|
||||
await expect(receiverPage.burnOnReadConfirmationModal.dontShowAgainCheckbox).toBeVisible();
|
||||
|
||||
// # Check "don't show again" and confirm (combined action)
|
||||
await receiverPage.burnOnReadConfirmationModal.confirmWithDontShowAgain();
|
||||
|
||||
// * Verify post is deleted
|
||||
await expect(borPost.container).not.toBeVisible({timeout: 10000});
|
||||
},
|
||||
);
|
||||
|
||||
test('MM-66742_12 timer chip displays countdown after reveal', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with 60 second duration
|
||||
const {
|
||||
user: sender,
|
||||
team,
|
||||
adminClient,
|
||||
} = await setupBorTest(pw, {
|
||||
durationSeconds: 60,
|
||||
});
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Timer test ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Get the BoR post and reveal it
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify timer chip is visible (wait for WebSocket update)
|
||||
await expect(borPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// * Get initial time
|
||||
const initialTime = await borPost.burnOnReadTimerChip.getTimeRemaining();
|
||||
|
||||
// * Verify time format is correct (MM:SS or M:SS)
|
||||
expect(initialTime).toMatch(/^\d+:\d{2}$/);
|
||||
|
||||
// # Wait 2 seconds
|
||||
await receiverPage.page.waitForTimeout(2000);
|
||||
|
||||
// * Get updated time
|
||||
const updatedTime = await borPost.burnOnReadTimerChip.getTimeRemaining();
|
||||
|
||||
// * Verify time has decreased
|
||||
expect(updatedTime).toMatch(/^\d+:\d{2}$/);
|
||||
// Parse times to compare
|
||||
const parseTime = (t: string) => {
|
||||
const [m, s] = t.split(':').map(Number);
|
||||
return m * 60 + s;
|
||||
};
|
||||
expect(parseTime(updatedTime)).toBeLessThan(parseTime(initialTime));
|
||||
});
|
||||
|
||||
test('MM-66742_13 message auto-deletes after timer expires', {tag: [BOR_TAG]}, async ({pw}, testInfo) => {
|
||||
testInfo.setTimeout(120000);
|
||||
// # Initialize setup with very short duration (10 seconds)
|
||||
const {
|
||||
user: sender,
|
||||
team,
|
||||
adminClient,
|
||||
} = await setupBorTest(pw, {
|
||||
durationSeconds: 10,
|
||||
maxTTLSeconds: 300,
|
||||
});
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Auto-delete test ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
// # Get the BoR post and reveal it
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
const postId = await borPost.getId();
|
||||
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify message is visible and timer is running (wait for WebSocket update)
|
||||
await expect(borPost.body).toContainText(message);
|
||||
await expect(borPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Wait for timer to expire (10 seconds + buffer)
|
||||
// Use polling to check for post removal
|
||||
await expect(async () => {
|
||||
const postLocator = receiverPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(postLocator).not.toBeVisible();
|
||||
}).toPass({
|
||||
timeout: 20000,
|
||||
intervals: [1000],
|
||||
});
|
||||
|
||||
// * Verify message is no longer visible
|
||||
const deletedPostLocator = receiverPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostLocator).not.toBeVisible();
|
||||
|
||||
// * Verify message text is not in the channel
|
||||
const pageContent = await receiverPage.centerView.container.textContent();
|
||||
expect(pageContent).not.toContain(message);
|
||||
});
|
||||
|
||||
test('MM-66742_14 receiver sees flame badge on concealed message', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create receiver
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create DM for controlled environment
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Badge test ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Login as receiver
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name);
|
||||
await receiverPage.toBeVisible();
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
|
||||
// # Get the BoR post (still concealed)
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
|
||||
// * Verify concealed placeholder is visible
|
||||
await expect(borPost.concealedPlaceholder.container).toBeVisible();
|
||||
|
||||
// * Verify flame badge is also visible on concealed post
|
||||
await expect(borPost.burnOnReadBadge.container).toBeVisible();
|
||||
|
||||
// * Verify badge tooltip shows appropriate message for receiver
|
||||
const ariaLabel = await borPost.burnOnReadBadge.getAriaLabel();
|
||||
expect(ariaLabel).toContain('Burn-on-read message');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {BOR_TAG, setupBorTest, createSecondUser} from './support';
|
||||
|
||||
test.describe('Burn-on-Read Restrictions', () => {
|
||||
test('MM-66742_19 no reply option in dot menu for BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No reply test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// # Open dot menu via page object
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Reply option is NOT present
|
||||
await expect(receiverPage.postDotMenu.replyMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_20 no pin option in dot menu for BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No pin test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Pin option is NOT present
|
||||
await expect(receiverPage.postDotMenu.pinToChannelMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_21 no edit option in dot menu for BoR post (sender)', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No edit test ${await pw.random.id()}`);
|
||||
|
||||
const borPost = await senderPage.getLastPost();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Edit option is NOT present (even for sender)
|
||||
await expect(senderPage.postDotMenu.editMenuItem).not.toBeVisible();
|
||||
|
||||
await senderPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_22 no forward option in dot menu for BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No forward test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Forward option is NOT present
|
||||
await expect(receiverPage.postDotMenu.forwardMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_23 no copy text option in dot menu for BoR post (receiver)', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No copy test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Copy Text option is NOT present for receiver
|
||||
await expect(receiverPage.postDotMenu.copyTextMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_24 no copy link option for receiver of BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No copy link test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Copy Link option is NOT present for receiver
|
||||
await expect(receiverPage.postDotMenu.copyLinkMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_25 sender can copy link to own BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`Sender copy link test ${await pw.random.id()}`);
|
||||
|
||||
const borPost = await senderPage.getLastPost();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Copy Link option IS present for sender
|
||||
await expect(senderPage.postDotMenu.copyLinkMenuItem).toBeVisible();
|
||||
|
||||
await senderPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_26 no follow thread option for BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`No follow test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Follow Thread option is NOT present
|
||||
await expect(receiverPage.postDotMenu.followMessageMenuItem).not.toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
|
||||
test('MM-66742_27 keyboard shortcut Shift+UP does not open reply for BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Keyboard test ${await pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// # Focus on the message input box and press Shift+UP
|
||||
await receiverPage.centerView.postCreate.input.click();
|
||||
await receiverPage.page.keyboard.press('Shift+ArrowUp');
|
||||
|
||||
// * Verify RHS does not open with the BoR message
|
||||
await expect(receiverPage.sidebarRight.container)
|
||||
.toBeHidden({timeout: 2000})
|
||||
.catch(async () => {
|
||||
await expect(receiverPage.sidebarRight.container).not.toContainText(message);
|
||||
});
|
||||
});
|
||||
|
||||
test('MM-66742_28 delete option available for revealed BoR post', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
const {user: sender, team, adminClient} = await setupBorTest(pw);
|
||||
const receiver = await createSecondUser(pw, adminClient, team);
|
||||
await adminClient.createDirectChannel([sender.id, receiver.id]);
|
||||
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(sender);
|
||||
await senderPage.goto(team.name, `@${receiver.username}`);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
await senderPage.postMessage(`Delete test ${await pw.random.id()}`);
|
||||
|
||||
const {channelsPage: receiverPage} = await pw.testBrowser.login(receiver);
|
||||
await receiverPage.goto(team.name, `@${sender.username}`);
|
||||
await receiverPage.toBeVisible();
|
||||
|
||||
const borPost = await receiverPage.getLastPost();
|
||||
await borPost.concealedPlaceholder.clickToReveal();
|
||||
await borPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
await borPost.hover();
|
||||
await borPost.postMenu.openDotMenu();
|
||||
|
||||
// * Verify Delete option IS present
|
||||
await expect(receiverPage.postDotMenu.deleteMenuItem).toBeVisible();
|
||||
|
||||
await receiverPage.page.keyboard.press('Escape');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {BOR_TAG, setupBorTest, createSecondUser} from './support';
|
||||
|
||||
test.describe('Burn-on-Read Sender Flow', () => {
|
||||
test('MM-66742_15 sends BoR message and views sent status with recipient count', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create second user as recipient (needed for BoR badge count)
|
||||
await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Login as sender
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Enable BoR toggle in composer
|
||||
await channelsPage.centerView.postCreate.toggleBurnOnRead();
|
||||
|
||||
// * Verify BoR label appears
|
||||
await expect(channelsPage.centerView.postCreate.burnOnReadLabel).toBeVisible();
|
||||
|
||||
// # Send BoR message
|
||||
const message = `BoR Test ${pw.random.id()}`;
|
||||
await channelsPage.postMessage(message);
|
||||
|
||||
// # Get the posted message
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
|
||||
// * Verify post has BoR badge
|
||||
await expect(lastPost.burnOnReadBadge.container).toBeVisible();
|
||||
|
||||
// * Verify flame icon is displayed
|
||||
await expect(lastPost.burnOnReadBadge.flameIcon).toBeVisible();
|
||||
|
||||
// # Hover over badge to see tooltip
|
||||
await lastPost.burnOnReadBadge.hover();
|
||||
|
||||
// * Verify tooltip shows recipient info
|
||||
const tooltipText = await lastPost.burnOnReadBadge.getTooltipText();
|
||||
expect(tooltipText).toContain('Read by 0 of');
|
||||
expect(tooltipText).toContain('Click to delete');
|
||||
});
|
||||
|
||||
test('MM-66742_16 sender sees read receipts in tooltip', {tag: [BOR_TAG]}, async ({pw}, testInfo) => {
|
||||
testInfo.setTimeout(120000);
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create two recipients
|
||||
const recipient1 = await createSecondUser(pw, adminClient, team);
|
||||
const recipient2 = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create a private channel with exactly these 3 users (sender + 2 recipients)
|
||||
// Use timestamp to ensure unique channel name across test runs
|
||||
const channelSuffix = Date.now().toString(36);
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: `bor-test-${channelSuffix}`,
|
||||
displayName: `BoR Test ${channelSuffix}`,
|
||||
type: 'P', // Private channel
|
||||
}),
|
||||
);
|
||||
|
||||
// Add all test users to the channel
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
await adminClient.addToChannel(recipient1.id, channel.id);
|
||||
await adminClient.addToChannel(recipient2.id, channel.id);
|
||||
|
||||
// Remove admin from channel (they were auto-added as creator)
|
||||
// This ensures exactly 3 members: sender + 2 recipients
|
||||
const adminUser = await adminClient.getMe();
|
||||
await adminClient.removeFromChannel(adminUser.id, channel.id);
|
||||
|
||||
// # Login as sender and navigate to the new channel
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(user);
|
||||
await senderPage.goto(team.name, channel.name);
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// # Post BoR message
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Secret message ${pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Get sender's post
|
||||
let senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify initial state shows 0 recipients revealed
|
||||
await senderPost.burnOnReadBadge.hover();
|
||||
let recipientCount = await senderPost.burnOnReadBadge.getRecipientCount();
|
||||
expect(recipientCount.revealed).toBe(0);
|
||||
expect(recipientCount.total).toBe(2); // Should be 2 recipients (channel has 3 members - 1 sender)
|
||||
|
||||
// # Login as first recipient and reveal the message
|
||||
const {channelsPage: recipient1Page} = await pw.testBrowser.login(recipient1);
|
||||
await recipient1Page.goto(team.name, channel.name);
|
||||
await recipient1Page.toBeVisible();
|
||||
const recipient1Post = await recipient1Page.getLastPost();
|
||||
await recipient1Post.concealedPlaceholder.clickToReveal();
|
||||
await recipient1Post.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// # Refresh sender's view and check updated count
|
||||
await senderPage.page.reload();
|
||||
await senderPage.toBeVisible();
|
||||
senderPost = await senderPage.getLastPost();
|
||||
await expect(senderPost.burnOnReadBadge.container).toBeVisible({timeout: 15000});
|
||||
await senderPost.burnOnReadBadge.hover();
|
||||
recipientCount = await senderPost.burnOnReadBadge.getRecipientCount();
|
||||
expect(recipientCount.revealed).toBe(1);
|
||||
|
||||
// # Login as second recipient and reveal the message
|
||||
const {channelsPage: recipient2Page} = await pw.testBrowser.login(recipient2);
|
||||
await recipient2Page.goto(team.name, channel.name);
|
||||
await recipient2Page.toBeVisible();
|
||||
const recipient2Post = await recipient2Page.getLastPost();
|
||||
await recipient2Post.concealedPlaceholder.clickToReveal();
|
||||
await recipient2Post.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// # Refresh sender's view — all recipients revealed, sender timer should start
|
||||
await senderPage.page.reload();
|
||||
await senderPage.toBeVisible();
|
||||
senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * After all recipients reveal, badge transitions to timer chip
|
||||
await expect(senderPost.burnOnReadTimerChip.container).toBeVisible({timeout: 15000});
|
||||
});
|
||||
|
||||
test('MM-66742_17 sender manually deletes for all recipients', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with BoR enabled
|
||||
const {user, team, adminClient} = await setupBorTest(pw);
|
||||
|
||||
// # Create recipient
|
||||
const recipient = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Login as sender and post BoR message
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(user);
|
||||
await senderPage.goto(team.name, 'town-square');
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `To be deleted ${pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Get the post and its ID for verification later
|
||||
const senderPost = await senderPage.getLastPost();
|
||||
const postId = await senderPost.getId();
|
||||
|
||||
// # Click flame badge to delete
|
||||
await senderPost.burnOnReadBadge.click();
|
||||
|
||||
// * Verify confirmation modal appears
|
||||
await expect(senderPage.burnOnReadConfirmationModal.container).toBeVisible();
|
||||
|
||||
// # Confirm deletion
|
||||
await senderPage.burnOnReadConfirmationModal.confirm();
|
||||
|
||||
// * Verify the specific post is removed from sender's view
|
||||
// Use attribute selector to handle special characters in post ID (colons, etc.)
|
||||
const deletedPostLocator = senderPage.page.locator(`[id="post_${postId}"]`);
|
||||
await expect(deletedPostLocator).not.toBeVisible();
|
||||
|
||||
// # Login as recipient and verify post is not visible
|
||||
const {channelsPage: recipientPage} = await pw.testBrowser.login(recipient);
|
||||
await recipientPage.goto(team.name, 'town-square');
|
||||
|
||||
// * Verify the deleted message is not in the channel
|
||||
const posts = await recipientPage.centerView.container.locator('.post').all();
|
||||
for (const post of posts) {
|
||||
const text = await post.textContent();
|
||||
expect(text).not.toContain(message);
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-66742_18 sender sees timer after all recipients reveal', {tag: [BOR_TAG]}, async ({pw}) => {
|
||||
// # Initialize setup with short duration
|
||||
const {user, team, adminClient} = await setupBorTest(pw, {
|
||||
durationSeconds: 60,
|
||||
});
|
||||
|
||||
// # Create one recipient
|
||||
const recipient = await createSecondUser(pw, adminClient, team);
|
||||
|
||||
// # Create a private channel with exactly 2 members (sender + recipient)
|
||||
const channelSuffix = Date.now().toString(36);
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: `bor-timer-test-${channelSuffix}`,
|
||||
displayName: `BoR Timer Test ${channelSuffix}`,
|
||||
type: 'P',
|
||||
}),
|
||||
);
|
||||
|
||||
// Add sender and recipient to the channel
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
await adminClient.addToChannel(recipient.id, channel.id);
|
||||
|
||||
// Remove admin from channel (they were auto-added as creator)
|
||||
const adminUser = await adminClient.getMe();
|
||||
await adminClient.removeFromChannel(adminUser.id, channel.id);
|
||||
|
||||
// # Login as sender and post BoR message in the controlled channel
|
||||
const {channelsPage: senderPage} = await pw.testBrowser.login(user);
|
||||
await senderPage.goto(team.name, channel.name);
|
||||
await senderPage.toBeVisible();
|
||||
await senderPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const message = `Timer test ${pw.random.id()}`;
|
||||
await senderPage.postMessage(message);
|
||||
|
||||
// # Get sender's post
|
||||
let senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify sender sees badge with exactly 1 recipient (not timer yet)
|
||||
await expect(senderPost.burnOnReadBadge.container).toBeVisible();
|
||||
await expect(senderPost.burnOnReadTimerChip.container).not.toBeVisible();
|
||||
|
||||
// * Verify recipient count is exactly 1 (our controlled channel)
|
||||
const initialCount = await senderPost.burnOnReadBadge.getRecipientCount();
|
||||
expect(initialCount.total).toBe(1);
|
||||
expect(initialCount.revealed).toBe(0);
|
||||
|
||||
// # Login as recipient and reveal
|
||||
const {channelsPage: recipientPage} = await pw.testBrowser.login(recipient);
|
||||
await recipientPage.goto(team.name, channel.name);
|
||||
await recipientPage.toBeVisible();
|
||||
|
||||
// Get the BoR post (last post in our controlled channel)
|
||||
const recipientPost = await recipientPage.getLastPost();
|
||||
await recipientPost.concealedPlaceholder.clickToReveal();
|
||||
await recipientPost.concealedPlaceholder.waitForReveal();
|
||||
|
||||
// * Verify recipient sees timer chip (their countdown started)
|
||||
await expect(recipientPost.burnOnReadTimerChip.container).toBeVisible();
|
||||
|
||||
// # Refresh sender's view
|
||||
await senderPage.page.reload();
|
||||
await senderPage.toBeVisible();
|
||||
|
||||
// Get the BoR post (last post in our controlled channel)
|
||||
senderPost = await senderPage.getLastPost();
|
||||
|
||||
// * Verify sender now sees timer chip (all 1 recipient revealed, so sender timer starts)
|
||||
await expect(senderPost.burnOnReadTimerChip.container).toBeVisible();
|
||||
await expect(senderPost.burnOnReadBadge.container).not.toBeVisible();
|
||||
|
||||
// * Verify timer is counting down
|
||||
const timeRemaining = await senderPost.burnOnReadTimerChip.getTimeRemaining();
|
||||
expect(timeRemaining).toMatch(/\d+:\d+/);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {PlaywrightExtended} from '@mattermost/playwright-lib';
|
||||
|
||||
export const BOR_TAG = '@burn_on_read';
|
||||
|
||||
export type BorSetup = {
|
||||
adminClient: Client4;
|
||||
adminUser: UserProfile;
|
||||
user: UserProfile;
|
||||
userClient: Client4;
|
||||
team: Team;
|
||||
offTopicUrl: string;
|
||||
townSquareUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup test environment with BoR feature enabled
|
||||
* @param pw Playwright extended fixture
|
||||
* @param options Configuration options
|
||||
* @returns Setup data including users, team, and clients
|
||||
*/
|
||||
export async function setupBorTest(
|
||||
pw: PlaywrightExtended,
|
||||
options: {
|
||||
durationSeconds?: number;
|
||||
maxTTLSeconds?: number;
|
||||
} = {},
|
||||
): Promise<BorSetup> {
|
||||
const {durationSeconds = 60, maxTTLSeconds = 300} = options;
|
||||
|
||||
// Ensure prerequisites
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// Initialize setup
|
||||
const setup = await pw.initSetup();
|
||||
|
||||
// Enable BoR via API - patch config instead of full update
|
||||
const currentConfig = await setup.adminClient.getConfig();
|
||||
await setup.adminClient.patchConfig({
|
||||
ServiceSettings: {
|
||||
...currentConfig.ServiceSettings,
|
||||
EnableBurnOnRead: true,
|
||||
BurnOnReadDurationSeconds: durationSeconds,
|
||||
BurnOnReadMaximumTimeToLiveSeconds: maxTTLSeconds,
|
||||
},
|
||||
});
|
||||
|
||||
return setup as BorSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a second user and add to team
|
||||
* @param pw Playwright extended fixture
|
||||
* @param adminClient Admin client for user creation
|
||||
* @param team Team to add user to
|
||||
* @returns Created user with password
|
||||
*/
|
||||
export async function createSecondUser(
|
||||
pw: PlaywrightExtended,
|
||||
adminClient: Client4,
|
||||
team: Team,
|
||||
): Promise<UserProfile & {password: string}> {
|
||||
const randomUser = await pw.random.user();
|
||||
const user = await adminClient.createUser(randomUser, '', '');
|
||||
(user as any).password = randomUser.password;
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
return user as UserProfile & {password: string};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple users and add to team
|
||||
* @param pw Playwright extended fixture
|
||||
* @param adminClient Admin client for user creation
|
||||
* @param team Team to add users to
|
||||
* @param count Number of users to create
|
||||
* @returns Array of created users with passwords
|
||||
*/
|
||||
export async function createMultipleUsers(
|
||||
pw: PlaywrightExtended,
|
||||
adminClient: Client4,
|
||||
team: Team,
|
||||
count: number,
|
||||
): Promise<Array<UserProfile & {password: string}>> {
|
||||
const users: Array<UserProfile & {password: string}> = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const user = await createSecondUser(pw, adminClient, team);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse recipient count from tooltip text
|
||||
* @param tooltipText Tooltip text containing recipient info
|
||||
* @returns Object with revealed and total counts
|
||||
*/
|
||||
export function parseRecipientCount(tooltipText: string): {revealed: number; total: number} {
|
||||
const match = tooltipText.match(/Read by (\d+) of (\d+)/);
|
||||
if (!match) {
|
||||
throw new Error(`Could not parse recipient count from: ${tooltipText}`);
|
||||
}
|
||||
return {
|
||||
revealed: parseInt(match[1], 10),
|
||||
total: parseInt(match[2], 10),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse time remaining from timer text
|
||||
* @param timerText Timer text (e.g., "0:45", "1:30")
|
||||
* @returns Seconds remaining
|
||||
*/
|
||||
export function parseTimeRemaining(timerText: string): number {
|
||||
const parts = timerText.split(':');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(`Invalid timer format: ${timerText}`);
|
||||
}
|
||||
const minutes = parseInt(parts[0], 10);
|
||||
const seconds = parseInt(parts[1], 10);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
|
@ -0,0 +1,468 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test.describe('System Console > Self-Deleting Messages', () => {
|
||||
test('admin can enable and disable self-deleting messages', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify Posts section is visible
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
await expect(postsSection).toBeVisible();
|
||||
|
||||
// Get BoR setting elements
|
||||
const enableToggleTrue = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadtrue');
|
||||
const enableToggleFalse = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadfalse');
|
||||
const durationDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadDurationSecondsdropdown');
|
||||
const maxTTLDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadMaximumTimeToLiveSecondsdropdown');
|
||||
const saveButton = postsSection.getByRole('button', {name: 'Save'});
|
||||
|
||||
// # If feature is enabled, disable it first
|
||||
if (await enableToggleTrue.isChecked()) {
|
||||
await enableToggleFalse.click();
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
}
|
||||
|
||||
// * Verify dropdowns are disabled when feature is off
|
||||
expect(await durationDropdown.isDisabled()).toBe(true);
|
||||
expect(await maxTTLDropdown.isDisabled()).toBe(true);
|
||||
|
||||
// # Enable the feature
|
||||
await enableToggleTrue.click();
|
||||
|
||||
// * Verify feature is enabled
|
||||
expect(await enableToggleTrue.isChecked()).toBe(true);
|
||||
|
||||
// * Verify dropdowns are now enabled
|
||||
expect(await durationDropdown.isDisabled()).toBe(false);
|
||||
expect(await maxTTLDropdown.isDisabled()).toBe(false);
|
||||
|
||||
// # Save settings
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate away and back to verify persistence
|
||||
await systemConsolePage.sidebar.userManagement.users.click();
|
||||
await systemConsolePage.users.toBeVisible();
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify feature is still enabled
|
||||
expect(await enableToggleTrue.isChecked()).toBe(true);
|
||||
});
|
||||
|
||||
test('admin can configure message duration', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Ensure BoR is enabled via API
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = true;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const durationDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadDurationSecondsdropdown');
|
||||
const saveButton = postsSection.getByRole('button', {name: 'Save'});
|
||||
|
||||
// # Select 60 seconds duration
|
||||
await durationDropdown.selectOption('60');
|
||||
|
||||
// # Save settings
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate away and back
|
||||
await systemConsolePage.sidebar.userManagement.users.click();
|
||||
await systemConsolePage.users.toBeVisible();
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify duration is still 60 seconds
|
||||
expect(await durationDropdown.inputValue()).toBe('60');
|
||||
});
|
||||
|
||||
test('admin can configure maximum time to live', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Ensure BoR is enabled via API
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = true;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const maxTTLDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadMaximumTimeToLiveSecondsdropdown');
|
||||
const saveButton = postsSection.getByRole('button', {name: 'Save'});
|
||||
|
||||
// # Select 1 day (86400 seconds) max TTL
|
||||
await maxTTLDropdown.selectOption('86400');
|
||||
|
||||
// # Save settings
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate away and back
|
||||
await systemConsolePage.sidebar.userManagement.users.click();
|
||||
await systemConsolePage.users.toBeVisible();
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify max TTL is still 1 day
|
||||
expect(await maxTTLDropdown.inputValue()).toBe('86400');
|
||||
});
|
||||
|
||||
test('dropdowns are disabled when feature is disabled', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Disable BoR via API to start with a known state
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = false;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const enableToggleTrue = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadtrue');
|
||||
const enableToggleFalse = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadfalse');
|
||||
const durationDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadDurationSecondsdropdown');
|
||||
const maxTTLDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadMaximumTimeToLiveSecondsdropdown');
|
||||
|
||||
// * Verify feature is disabled (from API config)
|
||||
expect(await enableToggleFalse.isChecked()).toBe(true);
|
||||
|
||||
// * Verify dropdowns are disabled when feature is off
|
||||
expect(await durationDropdown.isDisabled()).toBe(true);
|
||||
expect(await maxTTLDropdown.isDisabled()).toBe(true);
|
||||
|
||||
// # Enable the feature (just toggle, don't save)
|
||||
await enableToggleTrue.click();
|
||||
|
||||
// * Verify dropdowns are now enabled
|
||||
expect(await durationDropdown.isDisabled()).toBe(false);
|
||||
expect(await maxTTLDropdown.isDisabled()).toBe(false);
|
||||
|
||||
// # Toggle back to disabled
|
||||
await enableToggleFalse.click();
|
||||
|
||||
// * Verify dropdowns are disabled again
|
||||
expect(await durationDropdown.isDisabled()).toBe(true);
|
||||
expect(await maxTTLDropdown.isDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
test('settings persist after page reload', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Configure BoR via API with specific values (using valid dropdown options)
|
||||
// Duration: 300 (5 minutes), Max TTL: 259200 (3 days)
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = true;
|
||||
config.ServiceSettings.BurnOnReadDurationSeconds = 300;
|
||||
config.ServiceSettings.BurnOnReadMaximumTimeToLiveSeconds = 259200;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const enableToggleTrue = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadtrue');
|
||||
const durationDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadDurationSecondsdropdown');
|
||||
const maxTTLDropdown = postsSection.getByTestId('ServiceSettings.BurnOnReadMaximumTimeToLiveSecondsdropdown');
|
||||
|
||||
// * Verify configured values are displayed
|
||||
expect(await enableToggleTrue.isChecked()).toBe(true);
|
||||
expect(await durationDropdown.inputValue()).toBe('300');
|
||||
expect(await maxTTLDropdown.inputValue()).toBe('259200');
|
||||
|
||||
// # Reload directly to Posts section
|
||||
await page.goto('/admin_console/site_config/posts');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify values persist after reload
|
||||
expect(await enableToggleTrue.isChecked()).toBe(true);
|
||||
expect(await durationDropdown.inputValue()).toBe('300');
|
||||
expect(await maxTTLDropdown.inputValue()).toBe('259200');
|
||||
});
|
||||
|
||||
test('BoR toggle appears in channels when feature is enabled in System Console', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # First, disable BoR via API to start clean
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = false;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const enableToggleTrue = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadtrue');
|
||||
const saveButton = postsSection.getByRole('button', {name: 'Save'});
|
||||
|
||||
// # Enable BoR feature
|
||||
await enableToggleTrue.click();
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate to Channels by going to the team URL
|
||||
await page.goto(`/${team.name}/channels/off-topic`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify BoR toggle is visible in post create area
|
||||
const borButton = page.getByRole('button', {name: /Burn-on-read/i});
|
||||
await expect(borButton).toBeVisible({timeout: 10000});
|
||||
});
|
||||
|
||||
test('BoR toggle is hidden when feature is disabled in System Console', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # First, enable BoR via API
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = true;
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage, page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Navigate to Posts section
|
||||
await systemConsolePage.sidebar.siteConfiguration.posts.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const postsSection = page.getByTestId('sysconsole_section_PostSettings');
|
||||
const enableToggleFalse = postsSection.getByTestId('ServiceSettings.EnableBurnOnReadfalse');
|
||||
const saveButton = postsSection.getByRole('button', {name: 'Save'});
|
||||
|
||||
// # Disable BoR feature
|
||||
await enableToggleFalse.click();
|
||||
await saveButton.click();
|
||||
await pw.waitUntil(async () => (await saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate to Channels by going to the team URL
|
||||
await page.goto(`/${team.name}/channels/off-topic`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// * Verify BoR toggle is NOT visible in post create area
|
||||
const borButton = page.getByRole('button', {name: /Burn-on-read/i});
|
||||
await expect(borButton).not.toBeVisible({timeout: 5000});
|
||||
});
|
||||
|
||||
test('configured duration affects timer countdown in channels', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced' && license.short_sku_name !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Configure BoR with 5 minute (300 seconds) duration
|
||||
const config = await adminClient.getConfig();
|
||||
config.ServiceSettings.EnableBurnOnRead = true;
|
||||
config.ServiceSettings.BurnOnReadDurationSeconds = 300; // 5 minutes
|
||||
config.ServiceSettings.BurnOnReadMaximumTimeToLiveSeconds = 604800; // 7 days (so max TTL doesn't interfere)
|
||||
await adminClient.patchConfig(config);
|
||||
|
||||
// # Create a second user to receive the message
|
||||
const randomUser = await pw.random.user();
|
||||
const receiver = await adminClient.createUser(randomUser, '', '');
|
||||
(receiver as any).password = randomUser.password;
|
||||
await adminClient.addToTeam(team.id, receiver.id);
|
||||
|
||||
// # Create a private channel with sender and receiver
|
||||
const channelName = `bor-test-${Date.now().toString(36)}`;
|
||||
const channel = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: channelName,
|
||||
display_name: `BoR Duration Test ${channelName}`,
|
||||
type: 'P',
|
||||
} as any);
|
||||
await adminClient.addToChannel(receiver.id, channel.id);
|
||||
|
||||
// # Login as admin (sender) and post BoR message
|
||||
const {channelsPage: senderChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await senderChannelsPage.goto(team.name, channelName);
|
||||
await senderChannelsPage.toBeVisible();
|
||||
|
||||
// # Toggle BoR on and post message
|
||||
await senderChannelsPage.centerView.postCreate.toggleBurnOnRead();
|
||||
const messageContent = `Duration test ${Date.now()}`;
|
||||
await senderChannelsPage.postMessage(messageContent);
|
||||
|
||||
// # Login as receiver and reveal the message
|
||||
const {channelsPage: receiverChannelsPage, page: receiverPage} = await pw.testBrowser.login(receiver as any);
|
||||
await receiverChannelsPage.goto(team.name, channelName);
|
||||
await receiverChannelsPage.toBeVisible();
|
||||
|
||||
// # Wait for the concealed placeholder to be visible and enabled (not loading)
|
||||
const concealedPlaceholder = receiverPage.locator('.BurnOnReadConcealedPlaceholder').first();
|
||||
await expect(concealedPlaceholder).toBeVisible({timeout: 10000});
|
||||
|
||||
// Wait for it to not be in loading state
|
||||
await expect(concealedPlaceholder).not.toHaveClass(/BurnOnReadConcealedPlaceholder--loading/, {timeout: 10000});
|
||||
await expect(concealedPlaceholder).toBeEnabled({timeout: 5000});
|
||||
|
||||
// # Click to reveal the concealed message
|
||||
await concealedPlaceholder.click();
|
||||
|
||||
// # Confirm reveal in modal if it appears
|
||||
const confirmModal = receiverPage.locator('.BurnOnReadConfirmationModal');
|
||||
if (await confirmModal.isVisible({timeout: 2000}).catch(() => false)) {
|
||||
const confirmButton = confirmModal.getByRole('button', {name: /reveal/i});
|
||||
await confirmButton.click();
|
||||
}
|
||||
|
||||
// * Verify timer chip shows approximately 5 minutes (between 4:10 and 5:00)
|
||||
const timerChip = receiverPage.locator('.BurnOnReadTimerChip').first();
|
||||
await expect(timerChip).toBeVisible({timeout: 15000});
|
||||
|
||||
const timerText = await timerChip.textContent();
|
||||
// Timer format is "M:SS" or "MM:SS", should be close to 5:00
|
||||
const match = timerText?.match(/(\d+):(\d{2})/);
|
||||
expect(match).not.toBeNull();
|
||||
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1], 10);
|
||||
const seconds = parseInt(match[2], 10);
|
||||
const totalSeconds = minutes * 60 + seconds;
|
||||
|
||||
// Should be between 4:10 (250s) and 5:00 (300s) accounting for test execution time
|
||||
expect(totalSeconds).toBeGreaterThanOrEqual(250);
|
||||
expect(totalSeconds).toBeLessThanOrEqual(300);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue