diff --git a/e2e-tests/.ci/server.generate.sh b/e2e-tests/.ci/server.generate.sh index 1ee1288bf2..43d5f435df 100755 --- a/e2e-tests/.ci/server.generate.sh +++ b/e2e-tests/.ci/server.generate.sh @@ -61,6 +61,7 @@ services: MM_CLUSTERSETTINGS_READONLYCONFIG: "false" MM_SERVICEENVIRONMENT: "test" MM_FEATUREFLAGS_MOVETHREADSENABLED: "true" + MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES: "true" MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false" MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG" network_mode: host diff --git a/e2e-tests/playwright/lib/src/server/index.ts b/e2e-tests/playwright/lib/src/server/index.ts index dd0d3b08fd..b177d2bac8 100644 --- a/e2e-tests/playwright/lib/src/server/index.ts +++ b/e2e-tests/playwright/lib/src/server/index.ts @@ -7,4 +7,4 @@ export {getOnPremServerConfig} from './default_config'; export {initSetup, getAdminClient} from './init'; export {createRandomPost} from './post'; export {createRandomTeam} from './team'; -export {createRandomUser, getDefaultAdminUser} from './user'; +export {createNewUserProfile, createRandomUser, getDefaultAdminUser} from './user'; diff --git a/e2e-tests/playwright/lib/src/server/user.ts b/e2e-tests/playwright/lib/src/server/user.ts index 6f8c931590..a6330b349e 100644 --- a/e2e-tests/playwright/lib/src/server/user.ts +++ b/e2e-tests/playwright/lib/src/server/user.ts @@ -1,11 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Client4} from '@mattermost/client'; import {UserProfile} from '@mattermost/types/users'; import {getRandomId} from '@/util'; 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') { const randomId = getRandomId(); diff --git a/e2e-tests/playwright/lib/src/test_fixture.ts b/e2e-tests/playwright/lib/src/test_fixture.ts index d33feb440e..721a3ab733 100644 --- a/e2e-tests/playwright/lib/src/test_fixture.ts +++ b/e2e-tests/playwright/lib/src/test_fixture.ts @@ -18,6 +18,7 @@ import { } from './flag'; import {getBlobFromAsset, getFileFromAsset} from './file'; import { + createNewUserProfile, createRandomChannel, createRandomPost, createRandomTeam, @@ -87,6 +88,9 @@ export class PlaywrightExtended { readonly stubNotification; readonly waitForNotification; + // ./server + readonly createNewUserProfile; + // ./visual readonly matchSnapshot; @@ -143,6 +147,9 @@ export class PlaywrightExtended { this.stubNotification = stubNotification; this.waitForNotification = waitForNotification; + // ./server + this.createNewUserProfile = createNewUserProfile; + // ./visual this.matchSnapshot = matchSnapshot; diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/profile_modal.ts b/e2e-tests/playwright/lib/src/ui/components/channels/profile_modal.ts new file mode 100644 index 0000000000..a10d0a9dd6 --- /dev/null +++ b/e2e-tests/playwright/lib/src/ui/components/channels/profile_modal.ts @@ -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(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/components/index.ts b/e2e-tests/playwright/lib/src/ui/components/index.ts index 7074a6bc3d..dbae2409a7 100644 --- a/e2e-tests/playwright/lib/src/ui/components/index.ts +++ b/e2e-tests/playwright/lib/src/ui/components/index.ts @@ -24,6 +24,8 @@ import GenericConfirmModal from './channels/generic_confirm_modal'; import MessagePriority from './channels/message_priority'; import ScheduledDraftMenu from './channels/scheduled_draft_menu'; 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 SystemConsoleSidebar from './system_console/sidebar'; import SystemConsoleNavbar from './system_console/navbar'; @@ -71,8 +73,10 @@ const components = { SystemConsoleMobileSecurity, MessagePriority, UserProfilePopover, + UserAccountMenu, DeletePostConfirmationDialog, RestorePostConfirmationDialog, + ProfileModal, }; export { @@ -93,4 +97,6 @@ export { ThreadFooter, MessagePriority, DeletePostConfirmationDialog, + RestorePostConfirmationDialog, + ProfileModal, }; diff --git a/e2e-tests/playwright/lib/src/ui/components/user_account_menu.ts b/e2e-tests/playwright/lib/src/ui/components/user_account_menu.ts new file mode 100644 index 0000000000..ccc9aa5fcf --- /dev/null +++ b/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(); + } +} diff --git a/e2e-tests/playwright/lib/src/ui/pages/channels.ts b/e2e-tests/playwright/lib/src/ui/pages/channels.ts index 4e4bda789b..7e900c01eb 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/channels.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/channels.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Page} from '@playwright/test'; +import {expect, Page} from '@playwright/test'; import {components} from '@/ui/components'; @@ -11,6 +11,7 @@ export default class ChannelsPage { readonly page: Page; readonly globalHeader; + readonly userAccountMenuButton; readonly searchPopover; readonly centerView; readonly scheduledDraftDropdown; @@ -24,11 +25,11 @@ export default class ChannelsPage { readonly findChannelsModal; readonly deletePostModal; readonly settingsModal; - + readonly profileModal; readonly postContainer; readonly postDotMenu; readonly postReminderMenu; - + readonly userAccountMenu; readonly emojiGifPickerPopup; constructor(page: Page) { @@ -42,15 +43,18 @@ export default class ChannelsPage { this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); this.messagePriority = new components.MessagePriority(page.locator('body')); + this.userAccountMenuButton = page.getByRole('button', {name: "User's account menu"}); // Modals this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'})); this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal')); this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'})); + this.profileModal = new components.ProfileModal(page.getByRole('dialog', {name: 'Profile'})); // Menus 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.userAccountMenu = new components.UserAccountMenu(page.locator('#userAccountMenu')); // Popovers this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker')); @@ -67,7 +71,7 @@ export default class ChannelsPage { } async getLastPost() { - return this.postContainer.last(); + return this.centerView.getLastPost(); } async goto(teamName = '', channelName = '') { @@ -89,4 +93,17 @@ export default class ChannelsPage { async postMessage(message: string) { 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; + } } diff --git a/e2e-tests/playwright/package.json b/e2e-tests/playwright/package.json index 3a5369beee..57f79cdedc 100644 --- a/e2e-tests/playwright/package.json +++ b/e2e-tests/playwright/package.json @@ -10,14 +10,14 @@ "prettier": "prettier . --check", "prettier:fix": "prettier --write .", "check": "npm run lint && npm run prettier && npm run tsc", - "test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test", - "test:ci": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome", - "test:update-snapshots": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots", - "test:slomo": "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", - "codegen": "cross-env playwright codegen $PW_BASE_URL", - "playwright-ui": "cross-env playwright test --ui", - "show-report": "npx playwright show-report results/reporter", + "test": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test", + "test:ci": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome", + "test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots", + "test:slomo": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test", + "percy": "npm run build && cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad", + "codegen": "npm run build && cross-env playwright codegen $PW_BASE_URL", + "playwright-ui": "npm run build && cross-env playwright test --ui", + "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" }, "dependencies": { diff --git a/e2e-tests/playwright/specs/accessibility/channels/intro_channel.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/intro_channel.spec.ts index 4b6b588a93..ae46f33c05 100644 --- a/e2e-tests/playwright/specs/accessibility/channels/intro_channel.spec.ts +++ b/e2e-tests/playwright/specs/accessibility/channels/intro_channel.spec.ts @@ -37,7 +37,7 @@ test('Post actions tab support', async ({pw, axe}) => { await channelsPage.toBeVisible(); await channelsPage.centerView.postCreate.postMessage('hello'); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.hover(); await post.postMenu.toBeVisible(); diff --git a/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/user_settings.spec.ts b/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/user_settings.spec.ts new file mode 100644 index 0000000000..4f1883be27 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/user_settings.spec.ts @@ -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; +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, 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, + attributeName: string, + newValue: string, +): Promise { + 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, + attributeName: string, + optionIndex: number, +): Promise { + 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} optionIndices - The indices of the options to select + */ +async function editMultiselectAttribute( + page: Page, + fieldsMap: Record, + attributeName: string, + optionIndices: number[], +): Promise { + 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 { + // 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 { + 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 { + 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 { + 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} - A promise that resolves to a map of field IDs to field objects + */ +async function setupCustomProfileAttributeFields( + adminClient: Client4, + attributes: CustomProfileAttribute[], +): Promise> { + const fieldsMap: Record = {}; + + // 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, +): Promise { + // Create a map of attribute values by field ID + const valuesByFieldId: Record = {}; + + 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, +): Promise { + // 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); + } +} diff --git a/e2e-tests/playwright/specs/functional/channels/drafts/drafts_on_deleted_message.spec.ts b/e2e-tests/playwright/specs/functional/channels/drafts/drafts_on_deleted_message.spec.ts index 35d32bbe0a..a19653979c 100644 --- a/e2e-tests/playwright/specs/functional/channels/drafts/drafts_on_deleted_message.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/drafts/drafts_on_deleted_message.spec.ts @@ -30,7 +30,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use await channelsPage.goto(); await channelsPage.toBeVisible(); - const lastPostByAdmin = await channelsPage.centerView.getLastPost(); + const lastPostByAdmin = await channelsPage.getLastPost(); await lastPostByAdmin.toBeVisible(); // # 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'); // # 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.postMenu.toBeVisible(); await post.postMenu.reply(); diff --git a/e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts b/e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts index 24965f13e4..378441581b 100644 --- a/e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/edit_file_attachments/edit_file_attachment.spec.ts @@ -15,7 +15,7 @@ test('MM-T5654_1 should be able to add attachments while editing a post', async await channelsPage.toBeVisible(); await channelsPage.centerView.postCreate.postMessage(originalMessage); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.hover(); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); 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.centerView.postCreate.postMessage(originalMessage); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.hover(); 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.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.hover(); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); 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.centerView.postCreate.postMessage(originalMessage); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.hover(); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); await updatedPost.toContainText('Edited message'); 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.sendMessage(); - const secondUpdatedPost = await channelsPage.centerView.getLastPost(); + const secondUpdatedPost = await channelsPage.getLastPost(); await secondUpdatedPost.toBeVisible(); await secondUpdatedPost.toContainText('Edited message'); 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', ]); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.toContainText(originalMessage); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); await updatedPost.toContainText(originalMessage); 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.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.toContainText(originalMessage); 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', ]); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.toContainText(originalMessage); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); await updatedPost.toContainText(originalMessage); 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.centerView.postCreate.postMessage(originalMessage, ['sample_text_file.txt']); - const post = await channelsPage.centerView.getLastPost(); + const post = await channelsPage.getLastPost(); await post.toBeVisible(); await post.toContainText(originalMessage); 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.sendMessage(); - const updatedPost = await channelsPage.centerView.getLastPost(); + const updatedPost = await channelsPage.getLastPost(); await updatedPost.toBeVisible(); await updatedPost.toContainText(newMessage); 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.notToBeVisible(); - const restoredPost = await channelsPage.centerView.getLastPost(); + const restoredPost = await channelsPage.getLastPost(); await restoredPost.toBeVisible(); expect(restoredPost.toContainText('sample_text_file.txt')); }); diff --git a/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts b/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts index 77a60bdd82..f58809dd53 100644 --- a/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts @@ -34,7 +34,7 @@ test.fixme( await channelsPage.centerView.postCreate.sendMessage(); // * Verify that last message has the gif - const lastPost = await channelsPage.centerView.getLastPost(); + const lastPost = await channelsPage.getLastPost(); await lastPost.toBeVisible(); 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'); // # Open the last post sent in RHS - const lastPost = await channelsPage.centerView.getLastPost(); + const lastPost = await channelsPage.getLastPost(); await lastPost.hover(); await lastPost.postMenu.toBeVisible(); await lastPost.postMenu.reply(); diff --git a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts index 17f2a8d9e7..a2c6e76817 100644 --- a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts @@ -28,7 +28,7 @@ test('MM-T5139: Message Priority - Standard message priority and system setting' await channelsPage.postMessage(testMessage); // # Verify message posts without priority label - const lastPost = await channelsPage.centerView.getLastPost(); + const lastPost = await channelsPage.getLastPost(); await lastPost.toBeVisible(); await lastPost.toContainText(testMessage); await expect(lastPost.container.locator('.post-priority')).not.toBeVisible(); diff --git a/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts index 0a612f0231..e7e7492c0c 100644 --- a/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts @@ -42,14 +42,14 @@ test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, headles 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 - const otherLastPost = await otherChannelsPage.centerView.getLastPost(); + const otherLastPost = await otherChannelsPage.getLastPost(); await otherLastPost.toContainText(message); await expect(otherLastPost.container.locator('.mention--highlight')).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 await adminChannelsPage.goto(team.name, 'off-topic'); - const adminLastPost = await adminChannelsPage.centerView.getLastPost(); + const adminLastPost = await adminChannelsPage.getLastPost(); await adminLastPost.toContainText(message); await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible(); await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible(); diff --git a/e2e-tests/playwright/specs/functional/channels/scheduled_drafts/create_scheduled_draft.spec.ts b/e2e-tests/playwright/specs/functional/channels/scheduled_drafts/create_scheduled_draft.spec.ts index 2b06c60258..f904508a4b 100644 --- a/e2e-tests/playwright/specs/functional/channels/scheduled_drafts/create_scheduled_draft.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/scheduled_drafts/create_scheduled_draft.spec.ts @@ -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 await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).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(); }); @@ -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'); // # 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); const sidebarRight = channelsPage.sidebarRight; @@ -176,7 +177,8 @@ test('MM-T5643_9 should send a scheduled message immediately', async ({pw}) => { // Verify message has arrived await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).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}) => { @@ -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 await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).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(); }); @@ -267,7 +270,8 @@ test('MM-T5644 should edit scheduled message', async ({pw}) => { await page.waitForSelector(channelsPage.centerView.scheduledDraftChannelInfoMessageLocator, {state: 'hidden'}); await expect(channelsPage.centerView.scheduledDraftChannelInfoMessage).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(); }); diff --git a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts index 8e1e139a51..1d772bbdf5 100644 --- a/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/settings/notifications/highlight_without_notification.spec.ts @@ -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 const messageWithoutKeyword = 'This message does not contain the keyword'; await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword); - const lastPostWithoutHighlight = await channelsPage.centerView.getLastPost(); + const lastPostWithoutHighlight = await channelsPage.getLastPost(); // * Verify that the keywords are not highlighted 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 const messageWithKeyword = `This message contains the keyword ${keywords[3]}`; await channelsPage.centerView.postCreate.postMessage(messageWithKeyword); - const lastPostWithHighlight = await channelsPage.centerView.getLastPost(); + const lastPostWithHighlight = await channelsPage.getLastPost(); // * Verify that the keywords are highlighted 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 const messageWithoutKeyword = 'This message does not contain the keyword'; await channelsPage.centerView.postCreate.postMessage(messageWithoutKeyword); - const lastPostWithoutHighlight = await channelsPage.centerView.getLastPost(); + const lastPostWithoutHighlight = await channelsPage.getLastPost(); // # Open the message in the RHS 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(); // * 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(highlightKeyword)).toHaveClass( highlightWithoutNotificationClass, diff --git a/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts b/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts index 149be31bd9..298a8c61df 100644 --- a/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts +++ b/e2e-tests/playwright/specs/functional/system_console/system_users/export_data.spec.ts @@ -53,9 +53,8 @@ test.fixme('MM-T5522 Should begin export of data when export button is pressed', await channelsPage.centerView.toBeVisible(); // * Verify that we have started the export and that the second one is running second - const lastPost = await channelsPage.centerView.getLastPost(); - const postText = await lastPost.body.innerText(); - expect(postText).toContain('export of user data for the last 30 days'); + const lastPost = await channelsPage.getLastPost(); + await lastPost.toContain('export of user data for the last 30 days'); // * Wait until the first export finishes await channelsPage.centerView.waitUntilLastPostContains('contains user data for all time', pw.duration.half_min); diff --git a/server/public/model/custom_profile_attributes.go b/server/public/model/custom_profile_attributes.go index 1cd7527dc9..92be0b46d5 100644 --- a/server/public/model/custom_profile_attributes.go +++ b/server/public/model/custom_profile_attributes.go @@ -119,7 +119,7 @@ func (c CustomProfileAttributesSelectOption) IsValid() error { type CPAField struct { PropertyField - Attrs CPAAttrs + Attrs CPAAttrs `json:"attrs"` } type CPAAttrs struct {