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>
Этот коммит содержится в:
@@ -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})`;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user