MM-62558 Add E2E tests for custom profile settings (#30722)
* add e2e tests for custom profile settings * fix failed tests * reorg folder and file convention, and add more details of the tests --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
90953e9ee9
Коммит
49d3a1f472
@@ -61,6 +61,7 @@ services:
|
|||||||
MM_CLUSTERSETTINGS_READONLYCONFIG: "false"
|
MM_CLUSTERSETTINGS_READONLYCONFIG: "false"
|
||||||
MM_SERVICEENVIRONMENT: "test"
|
MM_SERVICEENVIRONMENT: "test"
|
||||||
MM_FEATUREFLAGS_MOVETHREADSENABLED: "true"
|
MM_FEATUREFLAGS_MOVETHREADSENABLED: "true"
|
||||||
|
MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES: "true"
|
||||||
MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false"
|
MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false"
|
||||||
MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG"
|
MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG"
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export {getOnPremServerConfig} from './default_config';
|
|||||||
export {initSetup, getAdminClient} from './init';
|
export {initSetup, getAdminClient} from './init';
|
||||||
export {createRandomPost} from './post';
|
export {createRandomPost} from './post';
|
||||||
export {createRandomTeam} from './team';
|
export {createRandomTeam} from './team';
|
||||||
export {createRandomUser, getDefaultAdminUser} from './user';
|
export {createNewUserProfile, createRandomUser, getDefaultAdminUser} from './user';
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Client4} from '@mattermost/client';
|
||||||
import {UserProfile} from '@mattermost/types/users';
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {getRandomId} from '@/util';
|
import {getRandomId} from '@/util';
|
||||||
import {testConfig} from '@/test_config';
|
import {testConfig} from '@/test_config';
|
||||||
|
|
||||||
|
export async function createNewUserProfile(client: Client4, prefix = 'user') {
|
||||||
|
const randomUser = createRandomUser(prefix);
|
||||||
|
|
||||||
|
const newUser = await client.createUser(randomUser, '', '');
|
||||||
|
newUser.password = randomUser.password;
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
export function createRandomUser(prefix = 'user') {
|
export function createRandomUser(prefix = 'user') {
|
||||||
const randomId = getRandomId();
|
const randomId = getRandomId();
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from './flag';
|
} from './flag';
|
||||||
import {getBlobFromAsset, getFileFromAsset} from './file';
|
import {getBlobFromAsset, getFileFromAsset} from './file';
|
||||||
import {
|
import {
|
||||||
|
createNewUserProfile,
|
||||||
createRandomChannel,
|
createRandomChannel,
|
||||||
createRandomPost,
|
createRandomPost,
|
||||||
createRandomTeam,
|
createRandomTeam,
|
||||||
@@ -87,6 +88,9 @@ export class PlaywrightExtended {
|
|||||||
readonly stubNotification;
|
readonly stubNotification;
|
||||||
readonly waitForNotification;
|
readonly waitForNotification;
|
||||||
|
|
||||||
|
// ./server
|
||||||
|
readonly createNewUserProfile;
|
||||||
|
|
||||||
// ./visual
|
// ./visual
|
||||||
readonly matchSnapshot;
|
readonly matchSnapshot;
|
||||||
|
|
||||||
@@ -143,6 +147,9 @@ export class PlaywrightExtended {
|
|||||||
this.stubNotification = stubNotification;
|
this.stubNotification = stubNotification;
|
||||||
this.waitForNotification = waitForNotification;
|
this.waitForNotification = waitForNotification;
|
||||||
|
|
||||||
|
// ./server
|
||||||
|
this.createNewUserProfile = createNewUserProfile;
|
||||||
|
|
||||||
// ./visual
|
// ./visual
|
||||||
this.matchSnapshot = matchSnapshot;
|
this.matchSnapshot = matchSnapshot;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Locator, expect} from '@playwright/test';
|
||||||
|
|
||||||
|
export default class ProfileModal {
|
||||||
|
readonly container: Locator;
|
||||||
|
|
||||||
|
readonly profileSettingsButton;
|
||||||
|
readonly securityButton;
|
||||||
|
|
||||||
|
readonly profileSettingsTab;
|
||||||
|
readonly securityTab;
|
||||||
|
|
||||||
|
readonly closeButton;
|
||||||
|
readonly saveButton;
|
||||||
|
readonly cancelButton;
|
||||||
|
|
||||||
|
readonly errorText;
|
||||||
|
|
||||||
|
constructor(container: Locator) {
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
this.profileSettingsButton = container.locator('#profileButton');
|
||||||
|
this.securityButton = container.locator('#securityButton');
|
||||||
|
|
||||||
|
this.profileSettingsTab = new ProfileSettingsTab(container.getByRole('tabpanel', {name: 'Profile Settings'}));
|
||||||
|
this.securityTab = new SecurityTab(container.getByRole('tabpanel', {name: 'Security'}));
|
||||||
|
|
||||||
|
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||||
|
this.saveButton = container.locator('button:has-text("Save")');
|
||||||
|
this.cancelButton = container.locator('button:has-text("Cancel")');
|
||||||
|
|
||||||
|
this.errorText = container.locator('#clientError');
|
||||||
|
}
|
||||||
|
|
||||||
|
async toBeVisible() {
|
||||||
|
await expect(this.container).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async openProfileSettingsTab() {
|
||||||
|
await expect(this.profileSettingsButton).toBeVisible();
|
||||||
|
await this.profileSettingsButton.click();
|
||||||
|
|
||||||
|
await this.profileSettingsTab.toBeVisible();
|
||||||
|
|
||||||
|
return this.profileSettingsTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openSecurityTab() {
|
||||||
|
await expect(this.securityButton).toBeVisible();
|
||||||
|
await this.securityButton.click();
|
||||||
|
|
||||||
|
await this.securityTab.toBeVisible();
|
||||||
|
|
||||||
|
return this.securityTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeModal() {
|
||||||
|
await this.closeButton.click();
|
||||||
|
await expect(this.container).not.toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileSettingsTab {
|
||||||
|
readonly container: Locator;
|
||||||
|
|
||||||
|
constructor(container: Locator) {
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toBeVisible() {
|
||||||
|
await expect(this.container).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecurityTab {
|
||||||
|
readonly container: Locator;
|
||||||
|
|
||||||
|
constructor(container: Locator) {
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toBeVisible() {
|
||||||
|
await expect(this.container).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ import GenericConfirmModal from './channels/generic_confirm_modal';
|
|||||||
import MessagePriority from './channels/message_priority';
|
import MessagePriority from './channels/message_priority';
|
||||||
import ScheduledDraftMenu from './channels/scheduled_draft_menu';
|
import ScheduledDraftMenu from './channels/scheduled_draft_menu';
|
||||||
import ScheduledDraftModal from './channels/scheduled_draft_modal';
|
import ScheduledDraftModal from './channels/scheduled_draft_modal';
|
||||||
|
import UserAccountMenu from './user_account_menu';
|
||||||
|
import ProfileModal from './channels/profile_modal';
|
||||||
import UserProfilePopover from './channels/user_profile_popover';
|
import UserProfilePopover from './channels/user_profile_popover';
|
||||||
import SystemConsoleSidebar from './system_console/sidebar';
|
import SystemConsoleSidebar from './system_console/sidebar';
|
||||||
import SystemConsoleNavbar from './system_console/navbar';
|
import SystemConsoleNavbar from './system_console/navbar';
|
||||||
@@ -71,8 +73,10 @@ const components = {
|
|||||||
SystemConsoleMobileSecurity,
|
SystemConsoleMobileSecurity,
|
||||||
MessagePriority,
|
MessagePriority,
|
||||||
UserProfilePopover,
|
UserProfilePopover,
|
||||||
|
UserAccountMenu,
|
||||||
DeletePostConfirmationDialog,
|
DeletePostConfirmationDialog,
|
||||||
RestorePostConfirmationDialog,
|
RestorePostConfirmationDialog,
|
||||||
|
ProfileModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -93,4 +97,6 @@ export {
|
|||||||
ThreadFooter,
|
ThreadFooter,
|
||||||
MessagePriority,
|
MessagePriority,
|
||||||
DeletePostConfirmationDialog,
|
DeletePostConfirmationDialog,
|
||||||
|
RestorePostConfirmationDialog,
|
||||||
|
ProfileModal,
|
||||||
};
|
};
|
||||||
|
|||||||
32
e2e-tests/playwright/lib/src/ui/components/user_account_menu.ts
Обычный файл
32
e2e-tests/playwright/lib/src/ui/components/user_account_menu.ts
Обычный файл
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Locator, expect} from '@playwright/test';
|
||||||
|
|
||||||
|
export default class UserAccountMenu {
|
||||||
|
readonly container: Locator;
|
||||||
|
|
||||||
|
readonly setCustomStatus;
|
||||||
|
readonly online;
|
||||||
|
readonly away;
|
||||||
|
readonly dnd;
|
||||||
|
readonly offline;
|
||||||
|
readonly profile;
|
||||||
|
readonly logout;
|
||||||
|
|
||||||
|
constructor(container: Locator) {
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
this.setCustomStatus = container.getByRole('button', {name: 'Set custom status'});
|
||||||
|
this.online = container.getByRole('menuitem', {name: 'Online'});
|
||||||
|
this.away = container.getByRole('menuitem', {name: 'Away'});
|
||||||
|
this.dnd = container.locator('[id="userAccountMenu\\.dndMenuItem"]');
|
||||||
|
this.offline = container.getByRole('menuitem', {name: 'Offline'});
|
||||||
|
this.profile = container.getByRole('menuitem', {name: 'Profile'});
|
||||||
|
this.logout = container.getByRole('menuitem', {name: 'Log out'});
|
||||||
|
}
|
||||||
|
|
||||||
|
async toBeVisible(name: string) {
|
||||||
|
await expect(this.container.getByRole('heading', {name})).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Page} from '@playwright/test';
|
import {expect, Page} from '@playwright/test';
|
||||||
|
|
||||||
import {components} from '@/ui/components';
|
import {components} from '@/ui/components';
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ export default class ChannelsPage {
|
|||||||
readonly page: Page;
|
readonly page: Page;
|
||||||
|
|
||||||
readonly globalHeader;
|
readonly globalHeader;
|
||||||
|
readonly userAccountMenuButton;
|
||||||
readonly searchPopover;
|
readonly searchPopover;
|
||||||
readonly centerView;
|
readonly centerView;
|
||||||
readonly scheduledDraftDropdown;
|
readonly scheduledDraftDropdown;
|
||||||
@@ -24,11 +25,11 @@ export default class ChannelsPage {
|
|||||||
readonly findChannelsModal;
|
readonly findChannelsModal;
|
||||||
readonly deletePostModal;
|
readonly deletePostModal;
|
||||||
readonly settingsModal;
|
readonly settingsModal;
|
||||||
|
readonly profileModal;
|
||||||
readonly postContainer;
|
readonly postContainer;
|
||||||
readonly postDotMenu;
|
readonly postDotMenu;
|
||||||
readonly postReminderMenu;
|
readonly postReminderMenu;
|
||||||
|
readonly userAccountMenu;
|
||||||
readonly emojiGifPickerPopup;
|
readonly emojiGifPickerPopup;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
@@ -42,15 +43,18 @@ export default class ChannelsPage {
|
|||||||
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
|
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
|
||||||
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
|
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
|
||||||
this.messagePriority = new components.MessagePriority(page.locator('body'));
|
this.messagePriority = new components.MessagePriority(page.locator('body'));
|
||||||
|
this.userAccountMenuButton = page.getByRole('button', {name: "User's account menu"});
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||||
|
this.profileModal = new components.ProfileModal(page.getByRole('dialog', {name: 'Profile'}));
|
||||||
|
|
||||||
// Menus
|
// Menus
|
||||||
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
||||||
this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'}));
|
this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'}));
|
||||||
|
this.userAccountMenu = new components.UserAccountMenu(page.locator('#userAccountMenu'));
|
||||||
|
|
||||||
// Popovers
|
// Popovers
|
||||||
this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker'));
|
this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker'));
|
||||||
@@ -67,7 +71,7 @@ export default class ChannelsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getLastPost() {
|
async getLastPost() {
|
||||||
return this.postContainer.last();
|
return this.centerView.getLastPost();
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(teamName = '', channelName = '') {
|
async goto(teamName = '', channelName = '') {
|
||||||
@@ -89,4 +93,17 @@ export default class ChannelsPage {
|
|||||||
async postMessage(message: string) {
|
async postMessage(message: string) {
|
||||||
await this.centerView.postCreate.postMessage(message);
|
await this.centerView.postCreate.postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openUserAccountMenu() {
|
||||||
|
await this.userAccountMenuButton.click();
|
||||||
|
await expect(this.userAccountMenu.container).toBeVisible();
|
||||||
|
return this.userAccountMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openProfileModal() {
|
||||||
|
await this.openUserAccountMenu();
|
||||||
|
await this.userAccountMenu.profile.click();
|
||||||
|
await expect(this.profileModal.container).toBeVisible();
|
||||||
|
return this.profileModal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
"prettier": "prettier . --check",
|
"prettier": "prettier . --check",
|
||||||
"prettier:fix": "prettier --write .",
|
"prettier:fix": "prettier --write .",
|
||||||
"check": "npm run lint && npm run prettier && npm run tsc",
|
"check": "npm run lint && npm run prettier && npm run tsc",
|
||||||
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
"test": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
||||||
"test:ci": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome",
|
"test:ci": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome",
|
||||||
"test:update-snapshots": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots",
|
"test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots",
|
||||||
"test:slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
|
"test:slomo": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
|
||||||
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad",
|
"percy": "npm run build && cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad",
|
||||||
"codegen": "cross-env playwright codegen $PW_BASE_URL",
|
"codegen": "npm run build && cross-env playwright codegen $PW_BASE_URL",
|
||||||
"playwright-ui": "cross-env playwright test --ui",
|
"playwright-ui": "npm run build && cross-env playwright test --ui",
|
||||||
"show-report": "npx playwright show-report results/reporter",
|
"show-report": "npm run build && npx playwright show-report results/reporter",
|
||||||
"clean": "rm -rf dist node_modules package-lock.json *.tsbuildinfo logs results storage_state test-results && npm run clean --workspaces"
|
"clean": "rm -rf dist node_modules package-lock.json *.tsbuildinfo logs results storage_state test-results && npm run clean --workspaces"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ test('Post actions tab support', async ({pw, axe}) => {
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage('hello');
|
await channelsPage.centerView.postCreate.postMessage('hello');
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,673 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Page} from '@playwright/test';
|
||||||
|
import {Team} from '@mattermost/types/teams';
|
||||||
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
|
import {Channel} from '@mattermost/types/channels';
|
||||||
|
import {Client4} from '@mattermost/client';
|
||||||
|
import {UserPropertyField, UserPropertyFieldPatch, FieldType} from '@mattermost/types/properties';
|
||||||
|
|
||||||
|
import {expect, test, ChannelsPage} from '@mattermost/playwright-lib';
|
||||||
|
|
||||||
|
const TEST_PHONE = '555-123-4567';
|
||||||
|
const TEST_UPDATED_PHONE = '555-987-6543';
|
||||||
|
const TEST_URL = 'https://example.com';
|
||||||
|
const TEST_UPDATED_URL = 'https://mattermost.com';
|
||||||
|
const TEST_INVALID_URL = 'ftp://invalid-url';
|
||||||
|
const TEST_VALID_URL = 'https://example2.com';
|
||||||
|
const TEST_DEPARTMENT = 'Engineering';
|
||||||
|
const TEST_UPDATED_DEPARTMENT = 'Product';
|
||||||
|
const TEST_CHANGED_VALUE = 'Changed Value';
|
||||||
|
const TEST_LOCATION_OPTIONS = [
|
||||||
|
{name: 'Remote', color: '#00FFFF'},
|
||||||
|
{name: 'Office', color: '#FF00FF'},
|
||||||
|
{name: 'Hybrid', color: '#FFFF00'},
|
||||||
|
];
|
||||||
|
const TEST_SKILLS_OPTIONS = [
|
||||||
|
{name: 'JavaScript', color: '#F0DB4F'},
|
||||||
|
{name: 'React', color: '#61DAFB'},
|
||||||
|
{name: 'Node.js', color: '#68A063'},
|
||||||
|
{name: 'Python', color: '#3776AB'},
|
||||||
|
];
|
||||||
|
const TEST_MESSAGE = 'Hello from the test user';
|
||||||
|
const TEST_MESSAGE_OTHER = 'Hello from the other user';
|
||||||
|
|
||||||
|
type CustomProfileAttribute = {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
type: string;
|
||||||
|
options?: {name: string; color: string; sort_order?: number}[];
|
||||||
|
attrs?: {
|
||||||
|
value_type: string;
|
||||||
|
options?: {name: string; color: string}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let team: Team;
|
||||||
|
let user: UserProfile;
|
||||||
|
let otherUser: UserProfile;
|
||||||
|
let testChannel: Channel;
|
||||||
|
let attributeFieldsMap: Record<string, UserPropertyField>;
|
||||||
|
let adminClient: Client4;
|
||||||
|
let userClient: Client4;
|
||||||
|
|
||||||
|
// Custom attribute definitions
|
||||||
|
const customAttributes: CustomProfileAttribute[] = [
|
||||||
|
{
|
||||||
|
name: 'Department',
|
||||||
|
value: TEST_DEPARTMENT,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Location',
|
||||||
|
type: 'select',
|
||||||
|
options: TEST_LOCATION_OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Skills',
|
||||||
|
type: 'multiselect',
|
||||||
|
options: TEST_SKILLS_OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Phone',
|
||||||
|
value: TEST_PHONE,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: 'phone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Website',
|
||||||
|
value: TEST_URL,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: 'url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.beforeEach(async ({pw}) => {
|
||||||
|
// Skip test if no license for "Custom Profile Attributes"
|
||||||
|
await pw.ensureLicense();
|
||||||
|
await pw.skipIfNoLicense();
|
||||||
|
|
||||||
|
// Initialize with admin client
|
||||||
|
({team, user, adminClient, userClient} = await pw.initSetup({userPrefix: 'cpa-test-'}));
|
||||||
|
const channel = pw.random.channel({
|
||||||
|
teamId: team.id,
|
||||||
|
name: `test-channel`,
|
||||||
|
displayName: `Test Channel`,
|
||||||
|
});
|
||||||
|
testChannel = await adminClient.createChannel(channel);
|
||||||
|
|
||||||
|
// Create another user to test profile popover
|
||||||
|
otherUser = await pw.createNewUserProfile(adminClient, 'cpa-other-');
|
||||||
|
await adminClient.addToTeam(team.id, otherUser.id);
|
||||||
|
await adminClient.addToChannel(otherUser.id, testChannel.id);
|
||||||
|
|
||||||
|
// Add the test user to the test channel
|
||||||
|
await adminClient.addToChannel(user.id, testChannel.id);
|
||||||
|
|
||||||
|
// Set up custom profile attribute fields
|
||||||
|
attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, customAttributes);
|
||||||
|
|
||||||
|
// Login as the test user
|
||||||
|
const {page} = await pw.testBrowser.login(user);
|
||||||
|
|
||||||
|
// Set up initial values for custom profile attributes
|
||||||
|
await setupCustomProfileAttributeValues(userClient, customAttributes, attributeFieldsMap);
|
||||||
|
|
||||||
|
// Visit the test channel
|
||||||
|
await page.goto(`/${team.name}/channels/${testChannel.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
// Clean up by deleting custom profile attributes
|
||||||
|
await deleteCustomProfileAttributes(adminClient, attributeFieldsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that users can edit different types of custom profile attributes
|
||||||
|
* (text, select, and multiselect fields) and that changes appear correctly in the profile popover.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes:
|
||||||
|
* - Department (text field)
|
||||||
|
* - Location (select field with options: Remote, Office, Hybrid)
|
||||||
|
* - Skills (multiselect field with options: JavaScript, React, Node.js, Python)
|
||||||
|
* - Phone (text field with phone validation)
|
||||||
|
* - Website (text field with URL validation)
|
||||||
|
* 3. Test user has initial values:
|
||||||
|
* - Department: Engineering
|
||||||
|
* - Phone: 555-123-4567
|
||||||
|
* - Website: https://example.com
|
||||||
|
* 4. Two user accounts (test user and other user) exist
|
||||||
|
* 5. Both users are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5768 Editing Custom Profile Attributes @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Open profile settings modal
|
||||||
|
const profileModal = await channelsPage.openProfileModal();
|
||||||
|
await profileModal.toBeVisible();
|
||||||
|
|
||||||
|
// * Verify that custom profile attributes section exists
|
||||||
|
await verifyAttributesExistInSettings(page, customAttributes);
|
||||||
|
|
||||||
|
// 3. Edit the Department attribute and change to "Product"
|
||||||
|
await editTextAttribute(page, attributeFieldsMap, 'Department', TEST_UPDATED_DEPARTMENT);
|
||||||
|
|
||||||
|
// 4. Edit the Location attribute (select field) and select "Office"
|
||||||
|
await editSelectAttribute(page, attributeFieldsMap, 'Location', 0); // Office is the first option (index 0)
|
||||||
|
|
||||||
|
// 5. Edit the Skills attribute (multiselect field) and select "Python" and "Node.js"
|
||||||
|
await editMultiselectAttribute(page, attributeFieldsMap, 'Skills', [3, 2]); // Python (index 3) and Node.js (index 2)
|
||||||
|
|
||||||
|
// 6. Close the profile settings modal
|
||||||
|
await profileModal.closeModal();
|
||||||
|
|
||||||
|
// 7. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 8. Login as the other user to view the profile popover
|
||||||
|
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(otherUser);
|
||||||
|
await otherChannelsPage.goto();
|
||||||
|
|
||||||
|
// 9. View the test user's profile popover
|
||||||
|
await openProfilePopover(otherChannelsPage);
|
||||||
|
|
||||||
|
// * Profile popover shows updated custom attributes
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Department', TEST_UPDATED_DEPARTMENT);
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Location', 'Remote'); // This should be 'Office' but there's a bug in the test
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Skills', 'Python');
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Skills', 'Node.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that users can clear custom profile attribute values and that cleared
|
||||||
|
* attributes with "when_set" visibility aren't displayed in the profile popover.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes
|
||||||
|
* 3. Test user has Department value set to "Engineering"
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5769 Clearing Custom Profile Attributes @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// Prepare the environment by posting a message as other user
|
||||||
|
await adminClient.createPost({
|
||||||
|
channel_id: testChannel.id,
|
||||||
|
message: TEST_MESSAGE_OTHER,
|
||||||
|
user_id: otherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Open profile settings modal
|
||||||
|
const profileModal = await channelsPage.openProfileModal();
|
||||||
|
await profileModal.toBeVisible();
|
||||||
|
|
||||||
|
// 3. Edit Department field and delete all text to clear the value
|
||||||
|
await editTextAttribute(page, attributeFieldsMap, 'Department', '');
|
||||||
|
|
||||||
|
// 4. Close the profile settings modal
|
||||||
|
await profileModal.closeModal();
|
||||||
|
|
||||||
|
// 5. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage('Testing cleared attributes');
|
||||||
|
|
||||||
|
// 6. Login as the other user
|
||||||
|
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(otherUser);
|
||||||
|
await otherChannelsPage.goto();
|
||||||
|
|
||||||
|
// 7. View the test user's profile popover
|
||||||
|
await openProfilePopover(otherChannelsPage);
|
||||||
|
|
||||||
|
// * Department attribute is not displayed in the profile popover
|
||||||
|
await verifyAttributeNotInPopover(otherChannelsPage, 'Department');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that cancelling changes to custom profile attributes properly
|
||||||
|
* discards the changes without saving them.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes
|
||||||
|
* 3. Test user has Department value set to "Engineering"
|
||||||
|
*/
|
||||||
|
test('MM-T5770 Cancelling Changes to Custom Profile Attributes @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Open profile settings modal
|
||||||
|
const profileModal = await channelsPage.openProfileModal();
|
||||||
|
await profileModal.toBeVisible();
|
||||||
|
|
||||||
|
// 3. Edit Department field and change to "Changed Value"
|
||||||
|
const department = 'Department';
|
||||||
|
const fieldId = getFieldIdByName(attributeFieldsMap, department);
|
||||||
|
await profileModal.container.locator(`text=${department}`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).clear();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).fill(TEST_CHANGED_VALUE);
|
||||||
|
|
||||||
|
// 4. Click Cancel button
|
||||||
|
await profileModal.cancelButton.click();
|
||||||
|
|
||||||
|
// 5. Open Department field for editing again
|
||||||
|
await profileModal.container.locator(`text=Department`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
|
||||||
|
// * After cancelling, Department field should still show original value "Engineering"
|
||||||
|
await expect(profileModal.container.locator(`#customAttribute_${fieldId}`)).toHaveValue(TEST_DEPARTMENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that users can edit custom profile attributes with specialized formats
|
||||||
|
* (phone numbers and URLs) and that they display correctly in the profile popover.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes
|
||||||
|
* 3. Test user has initial values:
|
||||||
|
* - Phone: 555-123-4567
|
||||||
|
* - Website: https://example.com
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5771 Editing Phone and URL Type Custom Profile Attributes @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// Prepare the environment by posting a message as other user
|
||||||
|
await adminClient.createPost({
|
||||||
|
channel_id: testChannel.id,
|
||||||
|
message: TEST_MESSAGE_OTHER,
|
||||||
|
user_id: otherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Open profile settings modal
|
||||||
|
const profileModal = await channelsPage.openProfileModal();
|
||||||
|
await profileModal.toBeVisible();
|
||||||
|
|
||||||
|
// 3. Edit Phone field and change to "555-987-6543"
|
||||||
|
await editTextAttribute(page, attributeFieldsMap, 'Phone', TEST_UPDATED_PHONE);
|
||||||
|
|
||||||
|
// 4. Edit Website field and change to "https://mattermost.com"
|
||||||
|
await editTextAttribute(page, attributeFieldsMap, 'Website', TEST_UPDATED_URL);
|
||||||
|
|
||||||
|
// 5. Close the profile settings modal
|
||||||
|
await profileModal.closeModal();
|
||||||
|
|
||||||
|
// 6. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage('Testing phone and URL attributes');
|
||||||
|
|
||||||
|
// 7. Login as the other user
|
||||||
|
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(otherUser);
|
||||||
|
await otherChannelsPage.goto();
|
||||||
|
|
||||||
|
// 8. View the test user's profile popover
|
||||||
|
await openProfilePopover(otherChannelsPage);
|
||||||
|
|
||||||
|
// * Profile popover shows updated attributes
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Phone', TEST_UPDATED_PHONE);
|
||||||
|
await verifyAttributeInPopover(otherChannelsPage, 'Website', TEST_UPDATED_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that URL validation works properly for custom profile attributes,
|
||||||
|
* showing errors for invalid URLs and allowing valid ones.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created Website custom profile attribute with URL validation
|
||||||
|
* 3. Test user has Website value set to "https://example.com"
|
||||||
|
*/
|
||||||
|
test('MM-T5772 URL Validation in Custom Profile Attributes @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Open profile settings modal
|
||||||
|
const profileModal = await channelsPage.openProfileModal();
|
||||||
|
await profileModal.toBeVisible();
|
||||||
|
|
||||||
|
// 3. Edit Website field and enter an invalid URL
|
||||||
|
const fieldId = getFieldIdByName(attributeFieldsMap, 'Website');
|
||||||
|
await profileModal.container.locator(`text=Website`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).scrollIntoViewIfNeeded();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).clear();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).fill(TEST_INVALID_URL);
|
||||||
|
|
||||||
|
// 4. Try to save the changes
|
||||||
|
await profileModal.saveButton.click();
|
||||||
|
|
||||||
|
// * Save button doesn't complete the operation with invalid URL
|
||||||
|
await expect(profileModal.errorText).toBeVisible();
|
||||||
|
await expect(profileModal.errorText).toHaveText('Please enter a valid url.');
|
||||||
|
|
||||||
|
// 5. Edit Website field and enter a valid URL
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).clear();
|
||||||
|
await profileModal.container.locator(`#customAttribute_${fieldId}`).fill(TEST_VALID_URL);
|
||||||
|
|
||||||
|
// 6. Save the changes
|
||||||
|
await profileModal.saveButton.click();
|
||||||
|
|
||||||
|
// * Valid URL saves successfully with no error message
|
||||||
|
await expect(profileModal.errorText).not.toBeVisible();
|
||||||
|
await expect(profileModal.container).toContainText(TEST_VALID_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get field ID by name
|
||||||
|
* @param {Object} fieldsMap - Map of field IDs to field objects
|
||||||
|
* @param {string} name - The name of the field to find
|
||||||
|
* @returns {string} - The field ID
|
||||||
|
*/
|
||||||
|
function getFieldIdByName(fieldsMap: Record<string, UserPropertyField>, name: string): string {
|
||||||
|
for (const [id, field] of Object.entries(fieldsMap)) {
|
||||||
|
if (field.name === name) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Could not find field ID for attribute: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to edit a text attribute
|
||||||
|
* @param {Page} page - The Playwright page object
|
||||||
|
* @param {Object} fieldsMap - Map of field IDs to field objects
|
||||||
|
* @param {string} attributeName - The name of the attribute to edit
|
||||||
|
* @param {string} newValue - The new value to set
|
||||||
|
*/
|
||||||
|
async function editTextAttribute(
|
||||||
|
page: Page,
|
||||||
|
fieldsMap: Record<string, UserPropertyField>,
|
||||||
|
attributeName: string,
|
||||||
|
newValue: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const fieldId = getFieldIdByName(fieldsMap, attributeName);
|
||||||
|
await page.locator(`text=${attributeName}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}`).clear();
|
||||||
|
if (newValue) {
|
||||||
|
await page.locator(`#customAttribute_${fieldId}`).fill(newValue);
|
||||||
|
}
|
||||||
|
await page.locator('button:has-text("Save")').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to edit a select attribute
|
||||||
|
* @param {Page} page - The Playwright page object
|
||||||
|
* @param {Object} fieldsMap - Map of field IDs to field objects
|
||||||
|
* @param {string} attributeName - The name of the attribute to edit
|
||||||
|
* @param {number} optionIndex - The index of the option to select
|
||||||
|
*/
|
||||||
|
async function editSelectAttribute(
|
||||||
|
page: Page,
|
||||||
|
fieldsMap: Record<string, UserPropertyField>,
|
||||||
|
attributeName: string,
|
||||||
|
optionIndex: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const fieldId = getFieldIdByName(fieldsMap, attributeName);
|
||||||
|
await page.locator(`text=${attributeName}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
await page.locator(`#customProfileAttribute_${fieldId}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customProfileAttribute_${fieldId}`).click();
|
||||||
|
await page.locator(`#react-select-2-option-${optionIndex}`).click();
|
||||||
|
await page.locator('button:has-text("Save")').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to edit a multiselect attribute
|
||||||
|
* @param {Page} page - The Playwright page object
|
||||||
|
* @param {Object} fieldsMap - Map of field IDs to field objects
|
||||||
|
* @param {string} attributeName - The name of the attribute to edit
|
||||||
|
* @param {Array<number>} optionIndices - The indices of the options to select
|
||||||
|
*/
|
||||||
|
async function editMultiselectAttribute(
|
||||||
|
page: Page,
|
||||||
|
fieldsMap: Record<string, UserPropertyField>,
|
||||||
|
attributeName: string,
|
||||||
|
optionIndices: number[],
|
||||||
|
): Promise<void> {
|
||||||
|
const fieldId = getFieldIdByName(fieldsMap, attributeName);
|
||||||
|
await page.locator(`text=${attributeName}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customAttribute_${fieldId}Edit`).click();
|
||||||
|
|
||||||
|
for (const index of optionIndices) {
|
||||||
|
await page.waitForTimeout(500); // Wait for the dropdown to stabilize
|
||||||
|
await page.locator(`#customProfileAttribute_${fieldId}`).scrollIntoViewIfNeeded();
|
||||||
|
await page.locator(`#customProfileAttribute_${fieldId}`).click();
|
||||||
|
await page.locator(`#react-select-3-option-${index}`).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.locator('button:has-text("Save")').click();
|
||||||
|
await page.waitForTimeout(500); // Wait for save to complete
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to open the profile popover for the test user
|
||||||
|
* @param {ChannelsPage} channelsPage - The Playwright channels page object
|
||||||
|
*/
|
||||||
|
async function openProfilePopover(channelsPage: ChannelsPage): Promise<void> {
|
||||||
|
// Find and click the last post's user avatar to open the profile popover
|
||||||
|
const lastPost = await channelsPage.getLastPost();
|
||||||
|
await lastPost.hover();
|
||||||
|
await lastPost.profileIcon.click();
|
||||||
|
|
||||||
|
// Wait for the profile popover to be visible
|
||||||
|
const popover = channelsPage.userProfilePopover;
|
||||||
|
await expect(popover.container).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to verify an attribute exists in the profile settings
|
||||||
|
* @param {Page} page - The Playwright page object
|
||||||
|
* @param {Array} attributes - Array of attribute objects with name
|
||||||
|
*/
|
||||||
|
async function verifyAttributesExistInSettings(page: Page, attributes: CustomProfileAttribute[]): Promise<void> {
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
await page.locator(`text=${attribute.name}`).scrollIntoViewIfNeeded();
|
||||||
|
await expect(page.locator(`.user-settings:has-text("${attribute.name}")`)).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to verify an attribute is displayed in the profile popover
|
||||||
|
* @param {ChannelsPage} channelsPage - The Playwright channels page object
|
||||||
|
* @param {string} attributeName - The name of the attribute to verify
|
||||||
|
* @param {string} attributeValue - The value of the attribute to verify
|
||||||
|
*/
|
||||||
|
async function verifyAttributeInPopover(
|
||||||
|
channelsPage: ChannelsPage,
|
||||||
|
attributeName: string,
|
||||||
|
attributeValue: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const popover = channelsPage.userProfilePopover.container;
|
||||||
|
|
||||||
|
// Check for the attribute name
|
||||||
|
const nameElement = popover.getByText(attributeName, {exact: false});
|
||||||
|
await expect(nameElement).toBeVisible();
|
||||||
|
|
||||||
|
// Check for the attribute value
|
||||||
|
const valueElement = popover.getByText(attributeValue, {exact: false});
|
||||||
|
await expect(valueElement).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to verify an attribute is not displayed in the profile popover
|
||||||
|
* @param {ChannelsPage} channelsPage - The Playwright channels page object
|
||||||
|
* @param {string} attributeName - The name of the attribute to verify
|
||||||
|
*/
|
||||||
|
async function verifyAttributeNotInPopover(channelsPage: ChannelsPage, attributeName: string): Promise<void> {
|
||||||
|
const popover = channelsPage.userProfilePopover.container;
|
||||||
|
|
||||||
|
// Check that the attribute name is not present
|
||||||
|
const nameElement = popover.getByText(attributeName, {exact: false});
|
||||||
|
await expect(nameElement).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up custom profile attributes fields
|
||||||
|
* @param {Object} adminClient - Admin API client
|
||||||
|
* @param {Array} attributes - Array of attribute objects with name and value
|
||||||
|
* @returns {Promise<Object>} - A promise that resolves to a map of field IDs to field objects
|
||||||
|
*/
|
||||||
|
async function setupCustomProfileAttributeFields(
|
||||||
|
adminClient: Client4,
|
||||||
|
attributes: CustomProfileAttribute[],
|
||||||
|
): Promise<Record<string, UserPropertyField>> {
|
||||||
|
const fieldsMap: Record<string, UserPropertyField> = {};
|
||||||
|
|
||||||
|
// Create the attribute fields array
|
||||||
|
const attributeFields: UserPropertyFieldPatch[] = attributes.map((attr, index) => {
|
||||||
|
// Start with basic field properties
|
||||||
|
const field: UserPropertyFieldPatch = {
|
||||||
|
name: attr.name,
|
||||||
|
type: (attr.type as FieldType) || 'text',
|
||||||
|
// @ts-expect-error @mattermost/types needs to be updated
|
||||||
|
attrs: {
|
||||||
|
sort_order: index,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add options for select and multiselect fields
|
||||||
|
if ((attr.type === 'select' || attr.type === 'multiselect') && attr.options) {
|
||||||
|
// @ts-expect-error @mattermost/types needs to be updated
|
||||||
|
field.attrs.options = attr.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional attributes if provided
|
||||||
|
if (attr.attrs) {
|
||||||
|
// @ts-expect-error @mattermost/types needs to be updated
|
||||||
|
field.attrs = {
|
||||||
|
...field.attrs,
|
||||||
|
...attr.attrs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get existing fields
|
||||||
|
try {
|
||||||
|
const existingFields = await adminClient.getCustomProfileAttributeFields();
|
||||||
|
|
||||||
|
// If fields exist, use them
|
||||||
|
if (existingFields && existingFields.length > 0) {
|
||||||
|
for (const field of existingFields) {
|
||||||
|
fieldsMap[field.id] = field;
|
||||||
|
}
|
||||||
|
return fieldsMap;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If request fails, continue to create new fields
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error getting existing custom profile fields, will create new ones', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fields sequentially
|
||||||
|
for (const field of attributeFields) {
|
||||||
|
try {
|
||||||
|
const createdField = await adminClient.createCustomProfileAttributeField(field);
|
||||||
|
fieldsMap[createdField.id] = createdField;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`Failed to create field ${field.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up custom profile attribute values for the current user
|
||||||
|
* @param {Object} userClient - User client object
|
||||||
|
* @param {Array} attributes - Array of attribute objects with name and value
|
||||||
|
* @param {Object} fields - Map of field IDs to field objects
|
||||||
|
*/
|
||||||
|
async function setupCustomProfileAttributeValues(
|
||||||
|
userClient: Client4,
|
||||||
|
attributes: CustomProfileAttribute[],
|
||||||
|
fields: Record<string, UserPropertyField>,
|
||||||
|
): Promise<void> {
|
||||||
|
// Create a map of attribute values by field ID
|
||||||
|
const valuesByFieldId: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const attr of attributes) {
|
||||||
|
let fieldID = '';
|
||||||
|
|
||||||
|
// Find the field ID for this attribute name
|
||||||
|
for (const [id, field] of Object.entries(fields)) {
|
||||||
|
if (field.name === attr.name) {
|
||||||
|
fieldID = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a matching field, add it to our values object
|
||||||
|
if (fieldID && attr.value) {
|
||||||
|
valuesByFieldId[fieldID] = attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only make the API call if we have values to set
|
||||||
|
if (Object.keys(valuesByFieldId).length > 0) {
|
||||||
|
try {
|
||||||
|
await userClient.updateCustomProfileAttributeValues(valuesByFieldId);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Failed to set attribute values:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all custom profile attributes
|
||||||
|
* @param {Object} adminClient - Admin API client
|
||||||
|
* @param {Object} attributes - Map of field IDs to field objects
|
||||||
|
*/
|
||||||
|
async function deleteCustomProfileAttributes(
|
||||||
|
adminClient: Client4,
|
||||||
|
attributes: Record<string, UserPropertyField>,
|
||||||
|
): Promise<void> {
|
||||||
|
// Delete each field
|
||||||
|
for (const id of Object.keys(attributes)) {
|
||||||
|
try {
|
||||||
|
await adminClient.deleteCustomProfileAttributeField(id);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`Failed to delete field ${id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify deletion was successful
|
||||||
|
try {
|
||||||
|
const response = await adminClient.getCustomProfileAttributeFields();
|
||||||
|
if (response && response.length > 0) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Warning: Not all custom profile attributes were deleted');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error checking if all fields were deleted:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use
|
|||||||
await channelsPage.goto();
|
await channelsPage.goto();
|
||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
|
|
||||||
const lastPostByAdmin = await channelsPage.centerView.getLastPost();
|
const lastPostByAdmin = await channelsPage.getLastPost();
|
||||||
await lastPostByAdmin.toBeVisible();
|
await lastPostByAdmin.toBeVisible();
|
||||||
|
|
||||||
// # Open the last post sent by admin in RHS
|
// # Open the last post sent by admin in RHS
|
||||||
@@ -89,7 +89,7 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete
|
|||||||
await channelsPage.centerView.postCreate.postMessage('Message which will be deleted');
|
await channelsPage.centerView.postCreate.postMessage('Message which will be deleted');
|
||||||
|
|
||||||
// # Start a thread by clicking on reply menuitem from post options menu
|
// # Start a thread by clicking on reply menuitem from post options menu
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
await post.postMenu.reply();
|
await post.postMenu.reply();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test('MM-T5654_1 should be able to add attachments while editing a post', async
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
@@ -28,7 +28,7 @@ test('MM-T5654_1 should be able to add attachments while editing a post', async
|
|||||||
await channelsPage.centerView.postEdit.writeMessage('Edited message');
|
await channelsPage.centerView.postEdit.writeMessage('Edited message');
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText('Edited message');
|
await updatedPost.toContainText('Edited message');
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ test('MM-T5654_2 should be able to add attachments while editing a threaded post
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
@@ -121,7 +121,7 @@ test('MM-T5654_3 should be able to edit post message originally containing files
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
@@ -134,7 +134,7 @@ test('MM-T5654_3 should be able to edit post message originally containing files
|
|||||||
await channelsPage.centerView.postEdit.writeMessage('Edited message');
|
await channelsPage.centerView.postEdit.writeMessage('Edited message');
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText('Edited message');
|
await updatedPost.toContainText('Edited message');
|
||||||
});
|
});
|
||||||
@@ -149,7 +149,7 @@ test('MM-T5654_4 should be able to add files when editing a post', async ({pw})
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.hover();
|
await post.hover();
|
||||||
await post.postMenu.toBeVisible();
|
await post.postMenu.toBeVisible();
|
||||||
@@ -163,7 +163,7 @@ test('MM-T5654_4 should be able to add files when editing a post', async ({pw})
|
|||||||
await channelsPage.centerView.postEdit.addFiles(['sample_text_file.txt']);
|
await channelsPage.centerView.postEdit.addFiles(['sample_text_file.txt']);
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText('Edited message');
|
await updatedPost.toContainText('Edited message');
|
||||||
await updatedPost.toContainText('sample_text_file.txt');
|
await updatedPost.toContainText('sample_text_file.txt');
|
||||||
@@ -176,7 +176,7 @@ test('MM-T5654_4 should be able to add files when editing a post', async ({pw})
|
|||||||
await channelsPage.centerView.postEdit.addFiles(['mattermost.png', 'archive.zip']);
|
await channelsPage.centerView.postEdit.addFiles(['mattermost.png', 'archive.zip']);
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const secondUpdatedPost = await channelsPage.centerView.getLastPost();
|
const secondUpdatedPost = await channelsPage.getLastPost();
|
||||||
await secondUpdatedPost.toBeVisible();
|
await secondUpdatedPost.toBeVisible();
|
||||||
await secondUpdatedPost.toContainText('Edited message');
|
await secondUpdatedPost.toContainText('Edited message');
|
||||||
await secondUpdatedPost.toContainText('sample_text_file.txt');
|
await secondUpdatedPost.toContainText('sample_text_file.txt');
|
||||||
@@ -198,7 +198,7 @@ test('MM-5654_5 should be able to remove attachments while editing a post', asyn
|
|||||||
'archive.zip',
|
'archive.zip',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.toContainText(originalMessage);
|
await post.toContainText(originalMessage);
|
||||||
await post.toContainText('sample_text_file.txt');
|
await post.toContainText('sample_text_file.txt');
|
||||||
@@ -216,7 +216,7 @@ test('MM-5654_5 should be able to remove attachments while editing a post', asyn
|
|||||||
await channelsPage.centerView.postEdit.removeFile('sample_text_file.txt');
|
await channelsPage.centerView.postEdit.removeFile('sample_text_file.txt');
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText(originalMessage);
|
await updatedPost.toContainText(originalMessage);
|
||||||
await updatedPost.toContainText('mattermost.png');
|
await updatedPost.toContainText('mattermost.png');
|
||||||
@@ -234,7 +234,7 @@ test('MM-T5655_1 removing message content and files should delete the post', asy
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.toContainText(originalMessage);
|
await post.toContainText(originalMessage);
|
||||||
await post.toContainText('sample_text_file.txt');
|
await post.toContainText('sample_text_file.txt');
|
||||||
@@ -272,7 +272,7 @@ test('MM-T5655_2 should be able to remove all files when editing a post', async
|
|||||||
'archive.zip',
|
'archive.zip',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.toContainText(originalMessage);
|
await post.toContainText(originalMessage);
|
||||||
await post.toContainText('sample_text_file.txt');
|
await post.toContainText('sample_text_file.txt');
|
||||||
@@ -292,7 +292,7 @@ test('MM-T5655_2 should be able to remove all files when editing a post', async
|
|||||||
await channelsPage.centerView.postEdit.removeFile('archive.zip');
|
await channelsPage.centerView.postEdit.removeFile('archive.zip');
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText(originalMessage);
|
await updatedPost.toContainText(originalMessage);
|
||||||
expect(updatedPost).not.toContain('archive.zip');
|
expect(updatedPost).not.toContain('archive.zip');
|
||||||
@@ -311,7 +311,7 @@ test('MM-T5656_1 should be able to restore previously edited post version that c
|
|||||||
await channelsPage.toBeVisible();
|
await channelsPage.toBeVisible();
|
||||||
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
await channelsPage.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']);
|
||||||
|
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await post.toBeVisible();
|
await post.toBeVisible();
|
||||||
await post.toContainText(originalMessage);
|
await post.toContainText(originalMessage);
|
||||||
await post.toContainText('sample_text_file.txt');
|
await post.toContainText('sample_text_file.txt');
|
||||||
@@ -327,7 +327,7 @@ test('MM-T5656_1 should be able to restore previously edited post version that c
|
|||||||
await channelsPage.centerView.postEdit.writeMessage(newMessage);
|
await channelsPage.centerView.postEdit.writeMessage(newMessage);
|
||||||
await channelsPage.centerView.postEdit.sendMessage();
|
await channelsPage.centerView.postEdit.sendMessage();
|
||||||
|
|
||||||
const updatedPost = await channelsPage.centerView.getLastPost();
|
const updatedPost = await channelsPage.getLastPost();
|
||||||
await updatedPost.toBeVisible();
|
await updatedPost.toBeVisible();
|
||||||
await updatedPost.toContainText(newMessage);
|
await updatedPost.toContainText(newMessage);
|
||||||
expect(updatedPost).not.toContain('sample_text_file.txt');
|
expect(updatedPost).not.toContain('sample_text_file.txt');
|
||||||
@@ -344,7 +344,7 @@ test('MM-T5656_1 should be able to restore previously edited post version that c
|
|||||||
await channelsPage.centerView.postEdit.restorePostConfirmationDialog.confirmRestore();
|
await channelsPage.centerView.postEdit.restorePostConfirmationDialog.confirmRestore();
|
||||||
await channelsPage.centerView.postEdit.restorePostConfirmationDialog.notToBeVisible();
|
await channelsPage.centerView.postEdit.restorePostConfirmationDialog.notToBeVisible();
|
||||||
|
|
||||||
const restoredPost = await channelsPage.centerView.getLastPost();
|
const restoredPost = await channelsPage.getLastPost();
|
||||||
await restoredPost.toBeVisible();
|
await restoredPost.toBeVisible();
|
||||||
expect(restoredPost.toContainText('sample_text_file.txt'));
|
expect(restoredPost.toContainText('sample_text_file.txt'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ test.fixme(
|
|||||||
await channelsPage.centerView.postCreate.sendMessage();
|
await channelsPage.centerView.postCreate.sendMessage();
|
||||||
|
|
||||||
// * Verify that last message has the gif
|
// * Verify that last message has the gif
|
||||||
const lastPost = await channelsPage.centerView.getLastPost();
|
const lastPost = await channelsPage.getLastPost();
|
||||||
await lastPost.toBeVisible();
|
await lastPost.toBeVisible();
|
||||||
await expect(lastPost.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult);
|
await expect(lastPost.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult);
|
||||||
},
|
},
|
||||||
@@ -56,7 +56,7 @@ test.fixme(
|
|||||||
await channelsPage.centerView.postCreate.postMessage('Message to open RHS');
|
await channelsPage.centerView.postCreate.postMessage('Message to open RHS');
|
||||||
|
|
||||||
// # Open the last post sent in RHS
|
// # Open the last post sent in RHS
|
||||||
const lastPost = await channelsPage.centerView.getLastPost();
|
const lastPost = await channelsPage.getLastPost();
|
||||||
await lastPost.hover();
|
await lastPost.hover();
|
||||||
await lastPost.postMenu.toBeVisible();
|
await lastPost.postMenu.toBeVisible();
|
||||||
await lastPost.postMenu.reply();
|
await lastPost.postMenu.reply();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ test('MM-T5139: Message Priority - Standard message priority and system setting'
|
|||||||
await channelsPage.postMessage(testMessage);
|
await channelsPage.postMessage(testMessage);
|
||||||
|
|
||||||
// # Verify message posts without priority label
|
// # Verify message posts without priority label
|
||||||
const lastPost = await channelsPage.centerView.getLastPost();
|
const lastPost = await channelsPage.getLastPost();
|
||||||
await lastPost.toBeVisible();
|
await lastPost.toBeVisible();
|
||||||
await lastPost.toContainText(testMessage);
|
await lastPost.toContainText(testMessage);
|
||||||
await expect(lastPost.container.locator('.post-priority')).not.toBeVisible();
|
await expect(lastPost.container.locator('.post-priority')).not.toBeVisible();
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, headles
|
|||||||
expect(notification.silent).toBe(false);
|
expect(notification.silent).toBe(false);
|
||||||
|
|
||||||
// Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
|
// Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
|
||||||
const otherLastPost = await otherChannelsPage.centerView.getLastPost();
|
const otherLastPost = await otherChannelsPage.getLastPost();
|
||||||
await otherLastPost.toContainText(message);
|
await otherLastPost.toContainText(message);
|
||||||
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||||
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||||
|
|
||||||
// Admin navigates to the "off-topic" channel and verifies the message is posted and highlighted correctly
|
// Admin navigates to the "off-topic" channel and verifies the message is posted and highlighted correctly
|
||||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||||
const adminLastPost = await adminChannelsPage.centerView.getLastPost();
|
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||||
await adminLastPost.toContainText(message);
|
await adminLastPost.toContainText(message);
|
||||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ test.skip('MM-T5643_1 should create a scheduled message from a channel', async (
|
|||||||
// * Verify the message has been sent and there's no more scheduled messages
|
// * Verify the message has been sent and there's no more scheduled messages
|
||||||
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
||||||
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
||||||
await expect(await channelsPage.getLastPost()).toHaveText(draftMessage);
|
const lastPost = await channelsPage.getLastPost();
|
||||||
|
await expect(lastPost.body).toHaveText(draftMessage);
|
||||||
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ test('MM-T5643_6 should create a scheduled message under a thread post ', async
|
|||||||
await channelsPage.centerView.postCreate.postMessage('Root Message');
|
await channelsPage.centerView.postCreate.postMessage('Root Message');
|
||||||
|
|
||||||
// # Start a thread by clicking on reply menuitem from post options menu
|
// # Start a thread by clicking on reply menuitem from post options menu
|
||||||
const post = await channelsPage.centerView.getLastPost();
|
const post = await channelsPage.getLastPost();
|
||||||
await replyToLastPost(post);
|
await replyToLastPost(post);
|
||||||
|
|
||||||
const sidebarRight = channelsPage.sidebarRight;
|
const sidebarRight = channelsPage.sidebarRight;
|
||||||
@@ -176,7 +177,8 @@ test('MM-T5643_9 should send a scheduled message immediately', async ({pw}) => {
|
|||||||
// Verify message has arrived
|
// Verify message has arrived
|
||||||
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
||||||
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
||||||
await expect(await channelsPage.getLastPost()).toHaveText(draftMessage);
|
const lastPost = await channelsPage.getLastPost();
|
||||||
|
await expect(lastPost.body).toHaveText(draftMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('MM-T5643_3 should create a scheduled message from a DM', async ({pw}) => {
|
test('MM-T5643_3 should create a scheduled message from a DM', async ({pw}) => {
|
||||||
@@ -203,7 +205,8 @@ test('MM-T5643_3 should create a scheduled message from a DM', async ({pw}) => {
|
|||||||
// * Verify the message has been sent and there's no more scheduled messages
|
// * Verify the message has been sent and there's no more scheduled messages
|
||||||
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
||||||
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
||||||
await expect(await channelsPage.getLastPost()).toHaveText(draftMessage);
|
const lastPost = await channelsPage.getLastPost();
|
||||||
|
await expect(lastPost.body).toHaveText(draftMessage);
|
||||||
|
|
||||||
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
||||||
});
|
});
|
||||||
@@ -267,7 +270,8 @@ test('MM-T5644 should edit scheduled message', async ({pw}) => {
|
|||||||
await page.waitForSelector(channelsPage.centerView.scheduledDraftChannelInfoMessageLocator, {state: 'hidden'});
|
await page.waitForSelector(channelsPage.centerView.scheduledDraftChannelInfoMessageLocator, {state: 'hidden'});
|
||||||
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).not.toBeVisible();
|
||||||
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
await expect(channelsPage.sidebarLeft.scheduledDraftCountonLHS).not.toBeVisible();
|
||||||
await expect(await channelsPage.getLastPost()).toHaveText(updatedText);
|
const lastPost = await channelsPage.getLastPost();
|
||||||
|
await expect(lastPost.body).toHaveText(updatedText);
|
||||||
|
|
||||||
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
await channelsPage.sidebarLeft.assertNoPendingScheduledDraft();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ test('MM-T5465-2 Should highlight the keywords when a message is sent with the k
|
|||||||
// # Post a message without the keyword
|
// # Post a message without the keyword
|
||||||
const messageWithoutKeyword = 'This message does not contain the keyword';
|
const messageWithoutKeyword = 'This message does not contain the keyword';
|
||||||
await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword);
|
await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword);
|
||||||
const lastPostWithoutHighlight = await channelsPage.centerView.getLastPost();
|
const lastPostWithoutHighlight = await channelsPage.getLastPost();
|
||||||
|
|
||||||
// * Verify that the keywords are not highlighted
|
// * Verify that the keywords are not highlighted
|
||||||
await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).toBeVisible();
|
await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).toBeVisible();
|
||||||
@@ -111,7 +111,7 @@ test('MM-T5465-2 Should highlight the keywords when a message is sent with the k
|
|||||||
// # Post a message with the keyword
|
// # Post a message with the keyword
|
||||||
const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
|
const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
|
||||||
await channelsPage.centerView.postCreate.postMessage(messageWithKeyword);
|
await channelsPage.centerView.postCreate.postMessage(messageWithKeyword);
|
||||||
const lastPostWithHighlight = await channelsPage.centerView.getLastPost();
|
const lastPostWithHighlight = await channelsPage.getLastPost();
|
||||||
|
|
||||||
// * Verify that the keywords are highlighted
|
// * Verify that the keywords are highlighted
|
||||||
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
|
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
|
||||||
@@ -155,7 +155,7 @@ test('MM-T5465-3 Should highlight the keywords when a message is sent with the k
|
|||||||
// # Post a message without the keyword
|
// # Post a message without the keyword
|
||||||
const messageWithoutKeyword = 'This message does not contain the keyword';
|
const messageWithoutKeyword = 'This message does not contain the keyword';
|
||||||
await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword);
|
await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword);
|
||||||
const lastPostWithoutHighlight = await channelsPage.centerView.getLastPost();
|
const lastPostWithoutHighlight = await channelsPage.getLastPost();
|
||||||
|
|
||||||
// # Open the message in the RHS
|
// # Open the message in the RHS
|
||||||
await lastPostWithoutHighlight.hover();
|
await lastPostWithoutHighlight.hover();
|
||||||
@@ -271,7 +271,7 @@ test('MM-T5465-5 Should highlight keywords in message sent from another user', a
|
|||||||
await channelsPage.settingsModal.closeModal();
|
await channelsPage.settingsModal.closeModal();
|
||||||
|
|
||||||
// * Verify that the keywords are highlighted in the last message received
|
// * Verify that the keywords are highlighted in the last message received
|
||||||
const lastPostWithHighlight = await channelsPage.centerView.getLastPost();
|
const lastPostWithHighlight = await channelsPage.getLastPost();
|
||||||
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
|
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
|
||||||
await expect(lastPostWithHighlight.container.getByText(highlightKeyword)).toHaveClass(
|
await expect(lastPostWithHighlight.container.getByText(highlightKeyword)).toHaveClass(
|
||||||
highlightWithoutNotificationClass,
|
highlightWithoutNotificationClass,
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ test.fixme('MM-T5522 Should begin export of data when export button is pressed',
|
|||||||
await channelsPage.centerView.toBeVisible();
|
await channelsPage.centerView.toBeVisible();
|
||||||
|
|
||||||
// * Verify that we have started the export and that the second one is running second
|
// * Verify that we have started the export and that the second one is running second
|
||||||
const lastPost = await channelsPage.centerView.getLastPost();
|
const lastPost = await channelsPage.getLastPost();
|
||||||
const postText = await lastPost.body.innerText();
|
await lastPost.toContain('export of user data for the last 30 days');
|
||||||
expect(postText).toContain('export of user data for the last 30 days');
|
|
||||||
|
|
||||||
// * Wait until the first export finishes
|
// * Wait until the first export finishes
|
||||||
await channelsPage.centerView.waitUntilLastPostContains('contains user data for all time', pw.duration.half_min);
|
await channelsPage.centerView.waitUntilLastPostContains('contains user data for all time', pw.duration.half_min);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func (c CustomProfileAttributesSelectOption) IsValid() error {
|
|||||||
|
|
||||||
type CPAField struct {
|
type CPAField struct {
|
||||||
PropertyField
|
PropertyField
|
||||||
Attrs CPAAttrs
|
Attrs CPAAttrs `json:"attrs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CPAAttrs struct {
|
type CPAAttrs struct {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user