MM-63587 Custom Profile Attributes E2E Profile Popover Test (#30777)
* add e2e tests for custom profile settings * fix failed tests * reorg folder and file convention, and add more details of the tests * add e2e-tests for custom profile attributes in profile popover * cleanup * add test keys, move to folder of feature and move common function and constants to helper file --------- Co-authored-by: Saturnino Abril <5334504+saturninoabril@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e8685a5802
Коммит
10bff401ee
@@ -0,0 +1,362 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
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} from '@mattermost/types/properties';
|
||||||
|
|
||||||
|
import {expect, test} from '@mattermost/playwright-lib';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomProfileAttribute,
|
||||||
|
setupCustomProfileAttributeFields,
|
||||||
|
setupCustomProfileAttributeValues,
|
||||||
|
deleteCustomProfileAttributes,
|
||||||
|
verifyAttributeInPopover,
|
||||||
|
verifyAttributeNotInPopover,
|
||||||
|
openProfilePopover,
|
||||||
|
updateCustomProfileAttributeVisibility,
|
||||||
|
TEST_PHONE,
|
||||||
|
TEST_URL,
|
||||||
|
TEST_DEPARTMENT,
|
||||||
|
TEST_LOCATION,
|
||||||
|
TEST_UPDATED_DEPARTMENT,
|
||||||
|
TEST_UPDATED_LOCATION,
|
||||||
|
TEST_TITLE,
|
||||||
|
TEST_MESSAGE,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
// Custom attribute definitions
|
||||||
|
const customAttributes: CustomProfileAttribute[] = [
|
||||||
|
{
|
||||||
|
name: 'Department',
|
||||||
|
value: TEST_DEPARTMENT,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Location',
|
||||||
|
value: TEST_LOCATION,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Title',
|
||||||
|
value: TEST_TITLE,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Phone',
|
||||||
|
value: TEST_PHONE,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: 'phone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Website',
|
||||||
|
value: TEST_URL,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: 'url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let team: Team;
|
||||||
|
let user: UserProfile;
|
||||||
|
let otherUser: UserProfile;
|
||||||
|
let testChannel: Channel;
|
||||||
|
let attributeFieldsMap: Record<string, UserPropertyField>;
|
||||||
|
let adminClient: Client4;
|
||||||
|
let userClient: Client4;
|
||||||
|
|
||||||
|
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 custom profile attributes are displayed 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 values set for custom profile attributes
|
||||||
|
* 4. User is a member of a channel
|
||||||
|
*/
|
||||||
|
test('MM-T5773 Display custom profile attributes in profile popover @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 3. Open the profile popover for the user's post
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify each custom attribute is displayed correctly
|
||||||
|
for (const attribute of customAttributes) {
|
||||||
|
if (attribute.value) {
|
||||||
|
await verifyAttributeInPopover(channelsPage, attribute.name, attribute.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that custom profile attributes are not displayed in the profile popover
|
||||||
|
* if the user has no values set.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes
|
||||||
|
* 3. Test user has no values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5774 Do not display custom profile attributes if none exist @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the other user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(otherUser);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 3. Open the profile popover for the user's post
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify custom attributes are not displayed
|
||||||
|
for (const attribute of customAttributes) {
|
||||||
|
await verifyAttributeNotInPopover(channelsPage, attribute.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that custom profile attributes are updated correctly when changed
|
||||||
|
* and the changes are reflected 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. Other user has values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5775 Update custom profile attributes when changed @custom_profile_attributes', async ({pw}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 3. Open the profile popover for the user's post
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify each custom attribute is displayed correctly
|
||||||
|
for (const attribute of customAttributes) {
|
||||||
|
if (attribute.value) {
|
||||||
|
await verifyAttributeInPopover(channelsPage, attribute.name, attribute.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Close the profile popover
|
||||||
|
await channelsPage.page.click('body', {position: {x: 10, y: 10}});
|
||||||
|
|
||||||
|
// 5. Update custom profile attributes
|
||||||
|
const updatedAttributes: CustomProfileAttribute[] = [
|
||||||
|
{
|
||||||
|
name: 'Department',
|
||||||
|
value: TEST_UPDATED_DEPARTMENT,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Location',
|
||||||
|
value: TEST_UPDATED_LOCATION,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await setupCustomProfileAttributeValues(userClient, updatedAttributes, attributeFieldsMap);
|
||||||
|
|
||||||
|
// 6. Open the profile popover again
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify updated attributes are displayed correctly
|
||||||
|
for (const attribute of updatedAttributes) {
|
||||||
|
if (attribute.value) {
|
||||||
|
await verifyAttributeInPopover(channelsPage, attribute.name, attribute.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Verify non-updated attribute is still displayed correctly
|
||||||
|
await verifyAttributeInPopover(channelsPage, 'Title', TEST_TITLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that custom profile attributes with visibility set to hidden
|
||||||
|
* are not 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. Other user has values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5776 Hide custom profile attributes when visibility is set to hidden @custom_profile_attributes', async ({
|
||||||
|
pw,
|
||||||
|
}) => {
|
||||||
|
// 1. Update the visibility of the Department attribute to hidden
|
||||||
|
await updateCustomProfileAttributeVisibility(adminClient, attributeFieldsMap, 'Department', 'hidden');
|
||||||
|
|
||||||
|
// 2. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 3. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 4. Open the profile popover for the user's post
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify the Department attribute is not displayed
|
||||||
|
await verifyAttributeNotInPopover(channelsPage, 'Department');
|
||||||
|
|
||||||
|
// * Verify other attributes are still displayed
|
||||||
|
await verifyAttributeInPopover(channelsPage, 'Location', TEST_LOCATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that custom profile attributes with visibility set to always
|
||||||
|
* are displayed in the profile popover even if they have no value.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes
|
||||||
|
* 3. Other user has values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5777 Always display custom profile attributes with visibility set to always @custom_profile_attributes', async ({
|
||||||
|
pw,
|
||||||
|
}) => {
|
||||||
|
// 1. Update the visibility of the Title attribute to always
|
||||||
|
await updateCustomProfileAttributeVisibility(adminClient, attributeFieldsMap, 'Title', 'always');
|
||||||
|
|
||||||
|
// 2. Login as the other user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(otherUser);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 3. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 4. Open the profile popover for the user's post
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify custom attributes are displayed correctly
|
||||||
|
for (const attribute of customAttributes) {
|
||||||
|
if (attribute.name === 'Title') {
|
||||||
|
// * Verify the Title attribute is displayed even though it has no value
|
||||||
|
const popover = channelsPage.userProfilePopover.container;
|
||||||
|
const nameElement = popover.getByText('Title', {exact: false});
|
||||||
|
await expect(nameElement).toBeVisible();
|
||||||
|
} else {
|
||||||
|
await verifyAttributeNotInPopover(channelsPage, attribute.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that phone and URL type custom profile attributes are displayed
|
||||||
|
* 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 including phone and URL types
|
||||||
|
* 3. Other user has values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5778 Display phone and URL type custom profile attributes correctly @custom_profile_attributes', async ({
|
||||||
|
pw,
|
||||||
|
}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 3. Open the profile popover for the other user
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify the Phone attribute is displayed correctly
|
||||||
|
await verifyAttributeInPopover(channelsPage, 'Phone', TEST_PHONE);
|
||||||
|
|
||||||
|
// * Verify the Website attribute is displayed correctly
|
||||||
|
await verifyAttributeInPopover(channelsPage, 'Website', TEST_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that phone and URL type custom profile attributes are clickable
|
||||||
|
* in the profile popover.
|
||||||
|
*
|
||||||
|
* Precondition:
|
||||||
|
* 1. A test server with valid license to support 'Custom Profile Attributes'
|
||||||
|
* 2. Admin has created custom profile attributes including phone and URL types
|
||||||
|
* 3. Other user has values set for custom profile attributes
|
||||||
|
* 4. Two user accounts exist and are members of the same channel
|
||||||
|
*/
|
||||||
|
test('MM-T5779 Verify phone and URL attributes are clickable in profile popover @custom_profile_attributes', async ({
|
||||||
|
pw,
|
||||||
|
}) => {
|
||||||
|
// 1. Login as the test user
|
||||||
|
const {channelsPage} = await pw.testBrowser.login(user);
|
||||||
|
await channelsPage.goto();
|
||||||
|
|
||||||
|
// 2. Post a message to make the user visible in the channel
|
||||||
|
await channelsPage.postMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// 3. Open the profile popover for the other user
|
||||||
|
await openProfilePopover(channelsPage);
|
||||||
|
|
||||||
|
// * Verify the Phone attribute has a clickable link with tel: protocol
|
||||||
|
const popover = channelsPage.userProfilePopover.container;
|
||||||
|
const phoneLink = popover.getByText(TEST_PHONE, {exact: false});
|
||||||
|
await expect(phoneLink).toHaveAttribute('href', new RegExp(`^tel:`));
|
||||||
|
|
||||||
|
// * Verify the Website attribute has a clickable link with https: protocol
|
||||||
|
const urlLink = popover.getByText(TEST_URL, {exact: false});
|
||||||
|
await expect(urlLink).toHaveAttribute('href', new RegExp(`^https:`));
|
||||||
|
});
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Page} from '@playwright/test';
|
||||||
|
import {Client4} from '@mattermost/client';
|
||||||
|
import {UserPropertyField, UserPropertyFieldPatch, FieldType} from '@mattermost/types/properties';
|
||||||
|
|
||||||
|
import {expect, ChannelsPage} from '@mattermost/playwright-lib';
|
||||||
|
|
||||||
|
// Common test data constants
|
||||||
|
export const TEST_PHONE = '555-123-4567';
|
||||||
|
export const TEST_UPDATED_PHONE = '555-987-6543';
|
||||||
|
export const TEST_URL = 'https://example.com';
|
||||||
|
export const TEST_UPDATED_URL = 'https://mattermost.com';
|
||||||
|
export const TEST_INVALID_URL = 'ftp://invalid-url';
|
||||||
|
export const TEST_VALID_URL = 'https://example2.com';
|
||||||
|
export const TEST_DEPARTMENT = 'Engineering';
|
||||||
|
export const TEST_UPDATED_DEPARTMENT = 'Product';
|
||||||
|
export const TEST_LOCATION = 'Remote';
|
||||||
|
export const TEST_UPDATED_LOCATION = 'Office';
|
||||||
|
export const TEST_TITLE = 'Software Engineer';
|
||||||
|
export const TEST_CHANGED_VALUE = 'Changed Value';
|
||||||
|
export const TEST_MESSAGE = 'Hello from the test user';
|
||||||
|
export const TEST_MESSAGE_OTHER = 'Hello from the other user';
|
||||||
|
|
||||||
|
export const TEST_LOCATION_OPTIONS = [
|
||||||
|
{name: 'Remote', color: '#00FFFF'},
|
||||||
|
{name: 'Office', color: '#FF00FF'},
|
||||||
|
{name: 'Hybrid', color: '#FFFF00'},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEST_SKILLS_OPTIONS = [
|
||||||
|
{name: 'JavaScript', color: '#F0DB4F'},
|
||||||
|
{name: 'React', color: '#61DAFB'},
|
||||||
|
{name: 'Node.js', color: '#68A063'},
|
||||||
|
{name: 'Python', color: '#3776AB'},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a custom profile attribute definition
|
||||||
|
*/
|
||||||
|
export type CustomProfileAttribute = {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
type: string;
|
||||||
|
options?: {name: string; color: string; sort_order?: number}[];
|
||||||
|
attrs?: {
|
||||||
|
value_type?: string;
|
||||||
|
visibility?: string;
|
||||||
|
options?: {name: string; color: string}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom attribute definitions for user settings tests (with select/multiselect attributes)
|
||||||
|
export const userSettingsAttributes: 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Custom attribute definitions for custom attributes tests (with text attributes and Title)
|
||||||
|
export const customAttributesTestData: CustomProfileAttribute[] = [
|
||||||
|
{
|
||||||
|
name: 'Department',
|
||||||
|
value: TEST_DEPARTMENT,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Location',
|
||||||
|
value: TEST_LOCATION,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Title',
|
||||||
|
value: TEST_TITLE,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Phone',
|
||||||
|
value: TEST_PHONE,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: 'phone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Website',
|
||||||
|
value: TEST_URL,
|
||||||
|
type: 'text',
|
||||||
|
attrs: {
|
||||||
|
value_type: '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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
*/
|
||||||
|
export 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility property of a custom profile attribute field
|
||||||
|
* @param {Client4} adminClient - Admin API client
|
||||||
|
* @param {Object} fieldsMap - Map of field IDs to field objects
|
||||||
|
* @param {string} attributeName - The name of the attribute to update
|
||||||
|
* @param {string} visibility - The visibility value to set ('when_set', 'hidden', or 'always')
|
||||||
|
*/
|
||||||
|
export async function updateCustomProfileAttributeVisibility(
|
||||||
|
adminClient: Client4,
|
||||||
|
fieldsMap: Record<string, UserPropertyField>,
|
||||||
|
attributeName: string,
|
||||||
|
visibility: 'when_set' | 'hidden' | 'always',
|
||||||
|
): Promise<void> {
|
||||||
|
const fieldID = getFieldIdByName(fieldsMap, attributeName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the visibility property
|
||||||
|
const updatedField = await adminClient.patchCustomProfileAttributeField(fieldID, {
|
||||||
|
// @ts-expect-error The type definition requires more properties than we need to set
|
||||||
|
attrs: {
|
||||||
|
visibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the fieldsMap with the updated field
|
||||||
|
fieldsMap[updatedField.id] = updatedField;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`Failed to update visibility for attribute ${attributeName}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up custom profile attributes fields
|
||||||
|
* @param {Client4} 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
|
||||||
|
*/
|
||||||
|
export 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 {Client4} 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
|
||||||
|
*/
|
||||||
|
export 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 {Client4} adminClient - Admin API client
|
||||||
|
* @param {Object} attributes - Map of field IDs to field objects
|
||||||
|
*/
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +1,41 @@
|
|||||||
// 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 {Team} from '@mattermost/types/teams';
|
import {Team} from '@mattermost/types/teams';
|
||||||
import {UserProfile} from '@mattermost/types/users';
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import {Channel} from '@mattermost/types/channels';
|
import {Channel} from '@mattermost/types/channels';
|
||||||
import {Client4} from '@mattermost/client';
|
import {Client4} from '@mattermost/client';
|
||||||
import {UserPropertyField, UserPropertyFieldPatch, FieldType} from '@mattermost/types/properties';
|
import {UserPropertyField} from '@mattermost/types/properties';
|
||||||
|
|
||||||
import {expect, test, ChannelsPage} from '@mattermost/playwright-lib';
|
import {expect, test} from '@mattermost/playwright-lib';
|
||||||
|
|
||||||
const TEST_PHONE = '555-123-4567';
|
import {
|
||||||
const TEST_UPDATED_PHONE = '555-987-6543';
|
CustomProfileAttribute,
|
||||||
const TEST_URL = 'https://example.com';
|
setupCustomProfileAttributeFields,
|
||||||
const TEST_UPDATED_URL = 'https://mattermost.com';
|
setupCustomProfileAttributeValues,
|
||||||
const TEST_INVALID_URL = 'ftp://invalid-url';
|
deleteCustomProfileAttributes,
|
||||||
const TEST_VALID_URL = 'https://example2.com';
|
verifyAttributesExistInSettings,
|
||||||
const TEST_DEPARTMENT = 'Engineering';
|
verifyAttributeInPopover,
|
||||||
const TEST_UPDATED_DEPARTMENT = 'Product';
|
verifyAttributeNotInPopover,
|
||||||
const TEST_CHANGED_VALUE = 'Changed Value';
|
openProfilePopover,
|
||||||
const TEST_LOCATION_OPTIONS = [
|
editTextAttribute,
|
||||||
{name: 'Remote', color: '#00FFFF'},
|
editSelectAttribute,
|
||||||
{name: 'Office', color: '#FF00FF'},
|
editMultiselectAttribute,
|
||||||
{name: 'Hybrid', color: '#FFFF00'},
|
getFieldIdByName,
|
||||||
];
|
TEST_PHONE,
|
||||||
const TEST_SKILLS_OPTIONS = [
|
TEST_UPDATED_PHONE,
|
||||||
{name: 'JavaScript', color: '#F0DB4F'},
|
TEST_URL,
|
||||||
{name: 'React', color: '#61DAFB'},
|
TEST_UPDATED_URL,
|
||||||
{name: 'Node.js', color: '#68A063'},
|
TEST_INVALID_URL,
|
||||||
{name: 'Python', color: '#3776AB'},
|
TEST_VALID_URL,
|
||||||
];
|
TEST_DEPARTMENT,
|
||||||
const TEST_MESSAGE = 'Hello from the test user';
|
TEST_UPDATED_DEPARTMENT,
|
||||||
const TEST_MESSAGE_OTHER = 'Hello from the other user';
|
TEST_CHANGED_VALUE,
|
||||||
|
TEST_LOCATION_OPTIONS,
|
||||||
type CustomProfileAttribute = {
|
TEST_SKILLS_OPTIONS,
|
||||||
name: string;
|
TEST_MESSAGE,
|
||||||
value?: string;
|
TEST_MESSAGE_OTHER,
|
||||||
type: string;
|
} from './helpers';
|
||||||
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
|
// Custom attribute definitions
|
||||||
const customAttributes: CustomProfileAttribute[] = [
|
const customAttributes: CustomProfileAttribute[] = [
|
||||||
@@ -87,6 +72,14 @@ const customAttributes: CustomProfileAttribute[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let team: Team;
|
||||||
|
let user: UserProfile;
|
||||||
|
let otherUser: UserProfile;
|
||||||
|
let testChannel: Channel;
|
||||||
|
let attributeFieldsMap: Record<string, UserPropertyField>;
|
||||||
|
let adminClient: Client4;
|
||||||
|
let userClient: Client4;
|
||||||
|
|
||||||
test.beforeEach(async ({pw}) => {
|
test.beforeEach(async ({pw}) => {
|
||||||
// Skip test if no license for "Custom Profile Attributes"
|
// Skip test if no license for "Custom Profile Attributes"
|
||||||
await pw.ensureLicense();
|
await pw.ensureLicense();
|
||||||
@@ -370,304 +363,3 @@ test('MM-T5772 URL Validation in Custom Profile Attributes @custom_profile_attri
|
|||||||
await expect(profileModal.errorText).not.toBeVisible();
|
await expect(profileModal.errorText).not.toBeVisible();
|
||||||
await expect(profileModal.container).toContainText(TEST_VALID_URL);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user