MM61173 - settings modal base creation (#30338)

* MM-61173 - channel settings modal: base modal, initial commit, file creation and base component

* new enhancements to the base modal creation

* revert changes on textbox_links and edit channel header

* fix types and add back unintentioned deleted value

* add the preview textbox component

* extract logic for info tab into its own component

* add the purpose input to the window

* move other component logic to its own component and code clean up

* ability to update channel type

* more advances on the archive channel tab

* fix unit test in textbox

* fix translations

* do not show the archive modal in default channel

* fix issue with url editor not being resetted on undo action

* adjust text and styling for the header and purpose inputs

* remove textboxlinks and use button eye icon

* adjust test and preview button style

* add unit test to channel patch

* move logic from parent modal to info tab component

* fix border issues and focus back to preview textareas

* prevent saving changes when pressing enter when selecting an icon

* enhance input component to cover limits validations and enhances tests

* set default error message for save changes panel

* add props to provide custom value to the buttons

* remove channel input errors on reset button click

* create new component settings textbox

* rename component to advanced textbox and add unit tests

* styling of the info tab and add error state to advanced textbox

* add logic to prevent tab switch with unsaved changes

* adjust url error logic and code clean up

* code clean up and enhance comments

* add char min length to advanced textbox logic

* add the channel settings modal to the new menu

* add new test files and fix reset error

* remove unused error variables

* adjust translations and remove unncesary import

* enhance permissions for archive channels and manage channel settings

* Adjust permission tree so channel admins can convert from private to public

* enhance the test suit around channel conversion type

* fix some e2e tests and solve channel input name issue

* fix unit test by interacting first with the input element

* adjust e2e tests to channel settings modal changes

* remove commented tests and implement pr feedback

* adjust more pr feedback to the code

* more pr feedback enhancements

* further enhancements to tab navigation, and adjust more e2e tests

* remove unused components and fix e2e tests

* revert unnecessary permissions changes

* Add name label to textboxes

* adjust e2e and unit tests

* revert min lenght change value and adjust tests and snapshots

* Channel banner settings (#30721)

* Added channel banner setting header

* Updated section styling

* handled animation

* handled min and max lengths

* cleanup

* color change fix

* general improvements

* Fixed API test

* removed unused param className

* added e2e tests

* test: add channel settings configuration tab test file

* Based on the context, here's a concise commit message for this change:

feat: Add comprehensive tests for ChannelSettingsConfigurationTab

* added some more tests

* CI

* reverted package-lock.json changes in Playwright

* remove extra border from advaced textbox

* adjust styling for name label in advance texbox and restart preview state on modal close

* sync package.lock in playwright

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Этот коммит содержится в:
Pablo Vélez
2025-04-23 12:49:54 +02:00
коммит произвёл GitHub
родитель f0dfe1c49f
Коммит 6ae0efd285
84 изменённых файлов: 7479 добавлений и 2464 удалений

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

@@ -1,18 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
import {Locator, expect, Page} from '@playwright/test';
import ChannelsHeader from './header';
import ChannelsPostCreate from './post_create';
import ChannelsPostEdit from './post_edit';
import ChannelsPost from './post';
import {duration} from '@/util';
import {duration, hexToRgb} from '@/util';
import {waitUntil} from '@/test_action';
export default class ChannelsCenterView {
readonly container: Locator;
readonly page: Page;
readonly header;
readonly postCreate;
@@ -27,9 +28,12 @@ export default class ChannelsCenterView {
readonly scheduledDraftSeeAllLink;
readonly postEdit;
readonly editedPostIcon;
readonly channelBanner;
constructor(container: Locator) {
constructor(container: Locator, page: Page) {
this.container = container;
this.page = page;
this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")';
this.scheduledDraftDMChannelLocatorString = 'div.ScheduledPostIndicator span a';
this.header = new ChannelsHeader(this.container.locator('.channel-header'));
@@ -43,6 +47,7 @@ export default class ChannelsCenterView {
this.scheduledDraftDMChannelLocator = container.locator(this.scheduledDraftDMChannelLocatorString);
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
this.editedPostIcon = (postID: string) => container.locator(`#postEdited_${postID}`);
this.channelBanner = container.getByTestId('channel_banner_container');
}
async toBeVisible() {
@@ -156,4 +161,45 @@ export default class ChannelsCenterView {
await this.editedPostIcon(postID).click();
}
}
async assertChannelBanner(text: string, backgroundColor: string) {
await expect(this.channelBanner).toBeVisible();
const actualText = await this.channelBanner.textContent();
expect(actualText).toBe(text);
const actualBackgroundColor = await this.channelBanner.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(actualBackgroundColor).toBe(hexToRgb(backgroundColor));
}
async assertChannelBannerNotVisible() {
await expect(this.channelBanner).not.toBeVisible();
}
async assertChannelBannerHasBoldText(text: string) {
const boldText = await this.channelBanner.locator('strong');
expect(boldText).toBeVisible();
const actualText = await boldText.textContent();
expect(actualText).toBe(text);
}
async assertChannelBannerHasItalicText(text: string) {
const italicText = await this.channelBanner.locator('em');
expect(italicText).toBeVisible();
const actualText = await italicText.textContent();
expect(actualText).toBe(text);
}
async assertChannelBannerHasStrikethroughText(text: string) {
const strikethroughText = await this.channelBanner.locator('del');
expect(strikethroughText).toBeVisible();
const actualText = await strikethroughText.textContent();
expect(actualText).toBe(text);
}
}

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

@@ -6,11 +6,20 @@ import {Locator, expect} from '@playwright/test';
export default class ChannelsHeader {
readonly container: Locator;
readonly channelMenuDropdown;
constructor(container: Locator) {
this.container = container;
this.channelMenuDropdown = container.locator('[aria-controls="channelHeaderDropdownMenu"]');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async openChannelMenu() {
await this.channelMenuDropdown.isVisible();
await this.channelMenuDropdown.click();
}
}

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

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, expect} from '@playwright/test';
export default class ConfigurationSettings {
readonly container: Locator;
constructor(container: Locator) {
this.container = container;
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async save() {
const saveButton = this.container.getByTestId('SaveChangesPanel__save-btn');
await expect(saveButton).toBeVisible();
await saveButton.click();
}
async enableChannelBanner() {
const toggleButton = await this.container.getByTestId('channelBannerToggle-button');
const classes = await toggleButton.getAttribute('class');
if (!classes?.includes('active')) {
await toggleButton.click();
}
}
async disableChannelBanner() {
const toggleButton = await this.container.getByTestId('channelBannerToggle-button');
const classes = await toggleButton.getAttribute('class');
if (classes?.includes('active')) {
await toggleButton.click();
}
}
async setChannelBannerText(text: string) {
const textBox = await this.container.getByTestId('channel_banner_banner_text_textbox');
await expect(textBox).toBeVisible();
await textBox.fill(text);
}
async setChannelBannerTextColor(color: string) {
const colorInput = await this.container.locator(
'#channel_banner_banner_background_color_picker-inputColorValue',
);
await expect(colorInput).toBeVisible();
await colorInput.fill(color);
}
}

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

@@ -6,11 +6,16 @@ import {Locator, expect} from '@playwright/test';
import DisplaySettings from './display_settings';
import NotificationsSettings from './notification_settings';
import ConfigurationSettings from '@/ui/components/channels/settings/configuration_settings';
export default class SettingsModal {
readonly container: Locator;
readonly notificationsSettingsTab;
readonly configurationSettingsTab;
readonly notificationsSettings;
readonly configurationSettings;
readonly displaySettingsTab;
readonly displaySettings;
@@ -19,7 +24,12 @@ export default class SettingsModal {
this.container = container;
this.notificationsSettingsTab = container.locator('#notificationsButton');
this.configurationSettingsTab = container.locator('#configurationButton');
this.notificationsSettings = new NotificationsSettings(container.locator('#notificationsSettings'));
this.configurationSettings = new ConfigurationSettings(
container.locator('.ChannelSettingsModal__configurationTab'),
);
this.displaySettingsTab = container.locator('#displayButton');
this.displaySettings = new DisplaySettings(container.locator('#displaySettings'));
@@ -52,4 +62,19 @@ export default class SettingsModal {
await expect(this.container).not.toBeVisible();
}
async openConfigurationTab(): Promise<ConfigurationSettings> {
await expect(this.configurationSettingsTab).toBeVisible();
await this.configurationSettingsTab.click();
await this.configurationSettings.toBeVisible();
return this.configurationSettings;
}
async close() {
const closeButton = this.container.locator('button.close');
await expect(closeButton).toBeVisible();
await closeButton.click();
}
}

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

@@ -4,6 +4,7 @@
import {expect, Page} from '@playwright/test';
import {ChannelsPost, components} from '@/ui/components';
import SettingsModal from '@/ui/components/channels/settings/settings_modal';
export default class ChannelsPage {
readonly channels = 'Channels';
@@ -38,7 +39,7 @@ export default class ChannelsPage {
// The main areas of the app
this.globalHeader = new components.GlobalHeader(this, page.locator('#global-header'));
this.searchPopover = new components.SearchPopover(page.locator('#searchPopover'));
this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'));
this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'), page);
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
@@ -64,6 +65,8 @@ export default class ChannelsPage {
// Posts
this.postContainer = page.locator('div.post-message__text');
page.locator('#channelHeaderDropdownMenu');
}
async toBeVisible() {
@@ -95,6 +98,28 @@ export default class ChannelsPage {
await this.centerView.postMessage(message, files);
}
async openChannelSettings(): Promise<SettingsModal> {
await this.centerView.header.openChannelMenu();
await this.page.locator('#channelSettings[role="menuitem"]').click();
await this.settingsModal.toBeVisible();
return this.settingsModal;
}
async newChannel(name: string, channelType: string) {
await this.page.locator('#browseOrAddChannelMenuButton').click();
await this.page.locator('#createNewChannelMenuItem').click();
await this.page.locator('#input_new-channel-modal-name').fill(name);
if (channelType === 'P') {
await this.page.locator('#public-private-selector-button-P').click();
} else {
await this.page.locator('#public-private-selector-button-O').click();
}
await this.page.getByText('Create channel').click();
}
async openUserAccountMenu() {
await this.userAccountMenuButton.click();
await expect(this.userAccountMenu.container).toBeVisible();

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

@@ -46,3 +46,16 @@ export const defaultTeam = {name: 'ad-1', displayName: 'eligendi', type: 'O'};
export const illegalRe = /[/?<>\\:*|":&();]/g;
export const simpleEmailRe = /\S+@\S+\.\S+/;
export function hexToRgb(hex: string): string {
// Remove the # if present
hex = hex.replace(/^#/, '');
// Parse the hex values
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Return the RGB string
return `rgb(${r}, ${g}, ${b})`;
}