snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {type PlaywrightExtended, expect, test} from '@mattermost/playwright-lib';
test('MM-T5520-1 should activate and deactivate users', async ({pw}) => {
const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and deactivate the user
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickDeactivate();
// # Press confirm on the modal
await systemConsolePage.users.confirmModal.confirm();
// * Verify user is deactivated
await expect(userRow.container.getByText('Deactivated')).toBeVisible();
expect((await getUser()).delete_at).toBeGreaterThan(0);
// # Open menu and reactivate the user
const actionMenu2 = await userRow.openActionMenu();
await actionMenu2.clickActivate();
// * Verify user is activated
await expect(userRow.container.getByText('Member')).toBeVisible();
});
test('MM-T5520-2 should change user roles', async ({pw}) => {
const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and click Manage roles
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickManageRoles();
// # Change to System Admin and click Save
const systemAdmin = systemConsolePage.page.locator('input[name="systemadmin"]');
await systemAdmin.waitFor();
await systemAdmin.click();
await systemConsolePage.users.manageRolesModal.save();
// * Verify that the role was updated
await expect(userRow.container.getByText('System Admin')).toBeVisible();
expect((await getUser()).roles).toContain('system_admin');
// # Open menu and click Manage roles
const actionMenu2 = await userRow.openActionMenu();
await actionMenu2.clickManageRoles();
// # Change to Member and click Save
const systemMember = systemConsolePage.page.locator('input[name="systemmember"]');
await systemMember.waitFor();
await systemMember.click();
await systemConsolePage.users.manageRolesModal.save();
// * Verify that the role was updated
await expect(userRow.container.getByText('Member')).toBeVisible();
expect((await getUser()).roles).toContain('system_user');
});
test('MM-T5520-3 should be able to manage teams', async ({pw}) => {
const {systemConsolePage} = await setupAndGetRandomUser(pw);
const {page} = systemConsolePage;
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and click Manage teams
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickManageTeams();
// The role dropdown renders its menu in a portal (MUI Popover), so the menu
// items live at the page level rather than inside the team row.
const team = page.locator('div.manage-teams__team');
// # Open the role dropdown and click Make Team Admin
await team.getByRole('button', {name: 'Team Member'}).click();
await page.getByRole('menuitem', {name: 'Make Team Admin'}).click();
// * Verify role is updated
await expect(team.getByText('Team Admin')).toBeVisible();
// # Change back to Team Member
await team.getByRole('button', {name: 'Team Admin'}).click();
await page.getByRole('menuitem', {name: 'Make Team Member'}).click();
// * Verify role is updated
await expect(team.getByText('Team Member')).toBeVisible();
// # Click Remove from Team
await team.getByRole('button', {name: 'Team Member'}).click();
await page.getByRole('menuitem', {name: 'Remove from Team'}).click();
// * The team should be detached
await team.waitFor({state: 'detached'});
await expect(team).not.toBeVisible();
});
test('MM-T5520-4 should reset the users password', async ({pw}) => {
const {systemConsolePage} = await setupAndGetRandomUser(pw);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and click Reset Password
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickResetPassword();
// # Enter a random password and click Reset
await systemConsolePage.users.resetPasswordModal.fillPassword(pw.newTestPassword());
await systemConsolePage.users.resetPasswordModal.reset();
});
test('MM-T5520-5 should change the users email', async ({pw}) => {
const {getUser, systemConsolePage} = await setupAndGetRandomUser(pw);
const newEmail = `${pw.random.id()}@example.com`;
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and click Update Email
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickUpdateEmail();
// # Enter new email and click Update
await systemConsolePage.users.updateEmailModal.fillEmail(newEmail);
await systemConsolePage.users.updateEmailModal.update();
// * Verify that the email updated
await expect(userRow.container.getByText(newEmail)).toBeVisible();
expect((await getUser()).email).toEqual(newEmail);
});
test('MM-T5520-6 should revoke sessions', async ({pw}) => {
const {systemConsolePage} = await setupAndGetRandomUser(pw);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
// # Open menu and revoke sessions
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickRevokeSessions();
// # Press confirm on the modal
await systemConsolePage.users.confirmModal.confirm();
// * Verify no error is displayed
await expect(userRow.container.locator('.error')).not.toBeVisible();
});
/**
* Setup a new random user, and search for it such that it's the first row in the list
* @param pw
* @returns A function to get the refreshed user, and the System Console page for navigation
*/
async function setupAndGetRandomUser(pw: PlaywrightExtended) {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create a random user to edit
const user = await adminClient.createUser(await pw.random.user(), '', '');
const team = await adminClient.createTeam(await pw.random.team());
await adminClient.addToTeam(team.id, user.id);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Search for the user
await systemConsolePage.users.searchUsers(user.email);
// Wait for search results
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
await expect(userRow.container.getByText(user.email)).toBeVisible();
return {getUser: () => adminClient.getUser(user.id), systemConsolePage};
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test.describe('System Console - Users table sorting', () => {
test.describe.configure({mode: 'serial'});
test('MM-T5523-1 Sortable columns should sort the list when clicked', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 10 random users
for (let i = 0; i < 10; i++) {
await adminClient.createUser(await pw.random.user(), '', '');
}
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// * Verify that 'Email' column has aria-sort attribute
const emailColumnHeader = systemConsolePage.users.usersTable.getColumnHeader('Email');
await expect(emailColumnHeader).toBeVisible();
await expect(emailColumnHeader).toHaveAttribute('aria-sort');
// # Click on the 'Email' column header to sort and wait for sort to complete
const sortDirection = await systemConsolePage.users.usersTable.sortByColumn('Email');
// * Verify that emails are sorted in the expected direction (table can still be
// re-fetching rows from other workers creating users — poll longer than default).
await expect(async () => {
await systemConsolePage.page.waitForLoadState('networkidle').catch(() => {});
const rowCount = await systemConsolePage.users.usersTable.bodyRows.count();
const maxRows = Math.min(rowCount, 40);
const emails: string[] = [];
for (let i = 0; i < maxRows; i++) {
const row = systemConsolePage.users.usersTable.getRowByIndex(i);
const email = (await row.getEmail()).trim();
if (email) {
emails.push(email);
}
}
expect(emails.length).toBeGreaterThan(3);
const sorted = [...emails].sort((a, b) => a.localeCompare(b, undefined, {ignorePunctuation: true}));
if (sortDirection === 'descending') {
sorted.reverse();
}
expect(emails).toEqual(sorted);
}).toPass({timeout: 120_000});
// # Click on the 'Email' column header again to toggle sort direction
const reversedDirection = await systemConsolePage.users.usersTable.sortByColumn('Email');
// * Verify that the sort direction has toggled
expect(reversedDirection).not.toEqual(sortDirection);
// * Verify that emails are sorted in the toggled direction
await expect(async () => {
await systemConsolePage.page.waitForLoadState('networkidle').catch(() => {});
const rowCount = await systemConsolePage.users.usersTable.bodyRows.count();
const maxRows = Math.min(rowCount, 40);
const emails: string[] = [];
for (let i = 0; i < maxRows; i++) {
const row = systemConsolePage.users.usersTable.getRowByIndex(i);
const email = (await row.getEmail()).trim();
if (email) {
emails.push(email);
}
}
expect(emails.length).toBeGreaterThan(3);
const sorted = [...emails].sort((a, b) => a.localeCompare(b, undefined, {ignorePunctuation: true}));
if (reversedDirection === 'descending') {
sorted.reverse();
}
expect(emails).toEqual(sorted);
}).toPass({timeout: 120_000});
});
test('MM-T5523-2 Non sortable columns should not sort the list when clicked', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 10 random users
for (let i = 0; i < 10; i++) {
await adminClient.createUser(await pw.random.user(), '', '');
}
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// * Verify that 'Last login' column does not have aria-sort attribute
const lastLoginColumnHeader = systemConsolePage.users.usersTable.getColumnHeader('Last login');
await expect(lastLoginColumnHeader).toBeVisible();
await expect(lastLoginColumnHeader).not.toHaveAttribute('aria-sort');
// # Store the first row's email without sorting
const firstRowWithoutSort = systemConsolePage.users.usersTable.getRowByIndex(0);
const firstRowEmailWithoutSort = await firstRowWithoutSort.container
.getByText(pw.simpleEmailRe)
.allInnerTexts();
// # Try to click on the 'Last login' column header to sort
await systemConsolePage.users.usersTable.clickSortOnColumn('Last login');
// # Store the first row's email after sorting
const firstRowWithSort = systemConsolePage.users.usersTable.getRowByIndex(0);
const firstRowEmailWithSort = await firstRowWithSort.container.getByText(pw.simpleEmailRe).allInnerTexts();
// * Verify that the first row's email is still the same
expect(firstRowEmailWithoutSort).toEqual(firstRowEmailWithSort);
});
});

View File

@@ -0,0 +1,239 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('MM-T5523-3 Should list the column names with checkboxes in the correct order', async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the column toggle menu
const columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
// # Get all the menu items
const menuItems = columnToggleMenu.getAllMenuItems();
const menuItemsTexts = await menuItems.allInnerTexts();
// * Verify menu items exists in the correct order
expect(menuItemsTexts).toHaveLength(10);
expect(menuItemsTexts).toEqual([
'User details',
'Email',
'Member since',
'Last login',
'Last activity',
'Last post',
'Days active',
'Messages posted',
'Channel count',
'Actions',
]);
});
test('MM-T5523-4 Should allow certain columns to be checked and others to be disabled', async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the column toggle menu
const columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
// * Verify that 'Display Name' is disabled
const displayNameMenuItem = await columnToggleMenu.getMenuItem('User details');
expect(displayNameMenuItem).toBeDisabled();
// * Verify that 'Actions' is disabled
const actionsMenuItem = await columnToggleMenu.getMenuItem('Actions');
expect(actionsMenuItem).toBeDisabled();
// * Verify that 'Email' however is enabled
const emailMenuItem = await columnToggleMenu.getMenuItem('Email');
expect(emailMenuItem).not.toBeDisabled();
});
test('MM-T5523-5 Should show/hide the columns which are toggled on/off', async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the column toggle menu
let columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
// # Uncheck the Email and Last login columns to hide them
await columnToggleMenu.clickMenuItem('Email');
await columnToggleMenu.clickMenuItem('Last login');
// * Close the column toggle menu
await columnToggleMenu.close();
// * Verify that Email column and Last login column are hidden
await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Email'})).not.toBeVisible();
await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Last login'})).not.toBeVisible();
// # Now open the column toggle menu again
columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
// # Check the Email column to show it
await columnToggleMenu.clickMenuItem('Email');
// * Close the column toggle menu
await columnToggleMenu.close();
// * Verify that Email column is now shown
await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Email'})).toBeVisible();
// * Verify that however Last login column is still hidden as we did not check it on
await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Last login'})).not.toBeVisible();
});
/**
* @objective Verify that the Channel count column displays a numeric value for a user with known channel memberships
*
* @precondition
* A guest user exists with exactly two channel memberships
*/
test(
'displays numeric channel count value when Channel count column is enabled',
{tag: '@system_users'},
async ({pw}) => {
const {adminUser, adminClient, team} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Create two channels
const ch1Name = `count-ch1-${pw.random.id()}`;
const channel1 = await adminClient.createChannel({
team_id: team.id,
name: ch1Name.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: ch1Name,
type: 'O',
});
const ch2Name = `count-ch2-${pw.random.id()}`;
const channel2 = await adminClient.createChannel({
team_id: team.id,
name: ch2Name.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: ch2Name,
type: 'O',
});
// # Create a guest user and add to exactly two channels
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
await adminClient.addToTeam(team.id, guestUser.id);
await adminClient.addToChannel(guestUser.id, channel1.id);
await adminClient.addToChannel(guestUser.id, channel2.id);
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Enable Channel count column
const columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
await columnToggleMenu.clickMenuItem('Channel count');
await columnToggleMenu.close();
// * Verify Channel count column header is visible
await expect(
systemConsolePage.users.container.getByRole('columnheader', {name: 'Channel count'}),
).toBeVisible();
// # Search for the guest user
await systemConsolePage.users.searchUsers(guestUser.email);
await systemConsolePage.users.isLoadingComplete();
// * Verify the Channel count cell displays the expected numeric value
const firstRow = systemConsolePage.users.container.locator('tbody tr').first();
const channelCountCell = firstRow.locator('.channelCountColumn');
await expect(channelCountCell).toHaveText('2');
},
);
/**
* @objective Verify that the Channel count column can be toggled on and off
*/
test('toggles Channel count column visibility on and off', {tag: '@system_users'}, async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the column toggle menu and enable Channel count
let columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
await columnToggleMenu.clickMenuItem('Channel count');
await columnToggleMenu.close();
// * Verify Channel count column header is visible
await expect(systemConsolePage.users.container.getByRole('columnheader', {name: 'Channel count'})).toBeVisible();
// # Open column toggle menu again and disable Channel count
columnToggleMenu = await systemConsolePage.users.openColumnToggleMenu();
await columnToggleMenu.clickMenuItem('Channel count');
await columnToggleMenu.close();
// * Verify Channel count column header is hidden
await expect(
systemConsolePage.users.container.getByRole('columnheader', {name: 'Channel count'}),
).not.toBeVisible();
});

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test.fixme('MM-T5522 Should begin export of data when export button is pressed', async ({pw}) => {
test.slow();
// # Skip test if no license
await pw.skipIfNoLicense();
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {page, channelsPage, systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Change the export duration to All time
const dateRangeMenu = await systemConsolePage.users.openDateRangeSelectorMenu();
await dateRangeMenu.clickMenuItem('All time');
// # Click Export button and confirm the modal
await systemConsolePage.users.clickExport();
await systemConsolePage.users.confirmModal.confirm();
// # Change the export duration to Last 30 days
const dateRangeMenu2 = await systemConsolePage.users.openDateRangeSelectorMenu();
await dateRangeMenu2.clickMenuItem('Last 30 days');
// # Click Export button and confirm the modal
await systemConsolePage.users.clickExport();
await systemConsolePage.users.confirmModal.confirm();
// # Click Export again button and confirm the modal
await systemConsolePage.users.clickExport();
await systemConsolePage.users.confirmModal.confirm();
// * Verify that we are told that one is already running
expect(page.getByText('Export is in progress')).toBeVisible();
// # Go back to Channels and open the system bot DM
channelsPage.goto('ad-1/messages', '@system-bot');
await channelsPage.centerView.toBeVisible();
// * Verify that we have started the export and that the second one is running second
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);
// * Wait until the second export finishes
await channelsPage.centerView.waitUntilLastPostContains(
'contains user data for the last 30 days',
pw.duration.half_min,
);
});

View File

@@ -0,0 +1,321 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('MM-T5521-7 Should be able to filter users with team filter', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create a team with a user
const team1 = await adminClient.createTeam(await pw.random.team());
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.addToTeam(team1.id, user1.id);
// # Create another team with a user
const team2 = await adminClient.createTeam(await pw.random.team());
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.addToTeam(team2.id, user2.id);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter's popover
const filterPopover = await systemConsolePage.users.openFilterPopover();
// # Enter the team name of the first user and select it
await filterPopover.filterByTeam(team1.display_name);
// # Save the filter and close the popover
await filterPopover.save();
await filterPopover.close();
await systemConsolePage.users.isLoadingComplete();
// * Verify that the user corresponding to the first team is visible as team-1 filter was applied
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the user corresponding to the second team is not visible as team-2 filter was not applied
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-8 Should be able to filter users with role filter', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create a guest user
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
// # Create a regular user
const regularUser = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter popover
const filterPopover = await systemConsolePage.users.openFilterPopover();
// # Open the role filter in the popover and select Guest
await filterPopover.filterByRole('Guests (all)');
// # Save the filter and close the popover
await filterPopover.save();
await filterPopover.close();
await systemConsolePage.users.isLoadingComplete();
// # Search for the guest user with the filter already applied
await systemConsolePage.users.searchUsers(guestUser.email);
// * Verify that guest user is visible as a 'Guest' role filter was applied
await expect(systemConsolePage.users.container.getByText(guestUser.email)).toBeVisible();
// # Search for the regular user with the filter already applied
await systemConsolePage.users.searchUsers(regularUser.email);
// * Verify that regular user is not visible as 'Guest' role filter was applied
await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible();
});
test('MM-T5521-9 Should be able to filter users with status filter', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create a user and then deactivate it
const deactivatedUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserActive(deactivatedUser.id, false);
// # Create a regular user
const regularUser = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter popover
const filterPopover = await systemConsolePage.users.openFilterPopover();
// # Open the status filter in the popover and select Deactivated users
await filterPopover.filterByStatus('Deactivated users');
// # Save the filter and close the popover
await filterPopover.save();
await filterPopover.close();
// # Search for the deactivated user with the filter already applied
await systemConsolePage.users.searchUsers(deactivatedUser.email);
// * Verify that deactivated user is visible as a 'Deactivated' status filter was applied
await expect(systemConsolePage.users.container.getByText(deactivatedUser.email)).toBeVisible();
// # Search for the regular user with the filter already applied
await systemConsolePage.users.searchUsers(regularUser.email);
// * Verify that regular user is not visible as 'Deactivated' status filter was applied
await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible();
});
/**
* @objective Verify that the role filter dropdown shows all guest filter variants
*/
test('displays all guest filter variants in the role filter dropdown', {tag: '@system_users'}, async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter popover
const filterPopover = await systemConsolePage.users.openFilterPopover();
// # Open the role filter menu
await filterPopover.openRoleMenu();
// * Verify all 6 role filter options are present
const roleOptions = filterPopover.container.getByRole('option');
const roleTexts = await roleOptions.allInnerTexts();
expect(roleTexts).toEqual([
'Any',
'System Admin',
'Member',
'Guests (all)',
'Guests in a single channel',
'Guests in multiple channels',
]);
});
/**
* @objective Verify that filtering by single-channel guest filter returns only guests with exactly one channel membership
*
* @precondition
* A guest user exists with exactly one channel membership
*/
test(
'filters users by single-channel guest filter and shows only single-channel guests',
{tag: '@system_users'},
async ({pw}) => {
const {adminUser, adminClient, team} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Create a channel
const channelName = `guest-ch-${pw.random.id()}`;
const channel = await adminClient.createChannel({
team_id: team.id,
name: channelName.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: channelName,
type: 'O',
});
// # Create a guest user and add to exactly one channel
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
await adminClient.addToTeam(team.id, guestUser.id);
await adminClient.addToChannel(guestUser.id, channel.id);
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter popover and filter by single-channel guests
const filterPopover = await systemConsolePage.users.openFilterPopover();
await filterPopover.filterByRole('Guests in a single channel');
await filterPopover.save();
await systemConsolePage.users.isLoadingComplete();
// # Search for the guest user
await systemConsolePage.users.searchUsers(guestUser.email);
// * Verify the single-channel guest is visible
await expect(systemConsolePage.users.container.getByText(guestUser.email)).toBeVisible();
},
);
/**
* @objective Verify that filtering by multi-channel guest filter returns only guests with more than one channel membership
*
* @precondition
* A guest user exists with two channel memberships and another with one
*/
test(
'filters users by multi-channel guest filter and excludes single-channel guests',
{tag: '@system_users'},
async ({pw}) => {
const {adminUser, adminClient, team} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Create two channels
const ch1Name = `guest-multi-1-${pw.random.id()}`;
const channel1 = await adminClient.createChannel({
team_id: team.id,
name: ch1Name.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: ch1Name,
type: 'O',
});
const ch2Name = `guest-multi-2-${pw.random.id()}`;
const channel2 = await adminClient.createChannel({
team_id: team.id,
name: ch2Name.toLowerCase().replace(/[^a-z0-9-]/g, ''),
display_name: ch2Name,
type: 'O',
});
// # Create a guest user with 2 channel memberships
const multiChannelGuest = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(multiChannelGuest.id, 'system_guest');
await adminClient.addToTeam(team.id, multiChannelGuest.id);
await adminClient.addToChannel(multiChannelGuest.id, channel1.id);
await adminClient.addToChannel(multiChannelGuest.id, channel2.id);
// # Create a guest user with only 1 channel membership
const singleChannelGuest = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(singleChannelGuest.id, 'system_guest');
await adminClient.addToTeam(team.id, singleChannelGuest.id);
await adminClient.addToChannel(singleChannelGuest.id, channel1.id);
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Open the filter popover and filter by multi-channel guests
const filterPopover = await systemConsolePage.users.openFilterPopover();
await filterPopover.filterByRole('Guests in multiple channels');
await filterPopover.save();
await systemConsolePage.users.isLoadingComplete();
// # Search for the multi-channel guest
await systemConsolePage.users.searchUsers(multiChannelGuest.email);
// * Verify the multi-channel guest is visible
await expect(systemConsolePage.users.container.getByText(multiChannelGuest.email)).toBeVisible();
// # Search for the single-channel guest
await systemConsolePage.users.searchUsers(singleChannelGuest.email);
// * Verify the single-channel guest is NOT visible
await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible();
},
);

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Client4} from '@mattermost/client';
import type {UserProfile} from '@mattermost/types/users';
import type {SystemConsolePage} from '@mattermost/playwright-lib';
import {expect, test} from '@mattermost/playwright-lib';
/**
* Delegated (granular) administration roles surfaced in the Manage Roles modal.
* Mirrors DELEGATED_ROLE_NAMES in the manage_roles_modal component.
*/
const USER_MANAGER_ROLE = 'system_user_manager';
const USER_MANAGER_LABEL = 'User Manager';
const SYSTEM_MANAGER_ROLE = 'system_manager';
const SYSTEM_MANAGER_LABEL = 'System Manager';
/**
* Open the Manage Roles modal for the given user from System Console > User Management > Users.
*/
async function openManageRolesModal(systemConsolePage: SystemConsolePage, user: UserProfile) {
await systemConsolePage.goto();
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
await systemConsolePage.users.searchUsers(user.email);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
await expect(userRow.container.getByText(user.email)).toBeVisible();
const actionMenu = await userRow.openActionMenu();
await actionMenu.clickManageRoles();
const {manageRolesModal} = systemConsolePage.users;
await manageRolesModal.toBeVisible();
return manageRolesModal;
}
/**
* Skip when the server license does not enable delegated granular administration.
* The feature requires an Enterprise/Enterprise Advanced license with the LDAP Groups
* feature and is not available on the Entry SKU (see utils/license_utils.ts).
*/
async function skipIfNoDelegatedAdminLicense(adminClient: Client4) {
const license = await adminClient.getClientLicenseOld();
test.skip(
license.IsLicensed !== 'true' || license.LDAPGroups !== 'true' || license.SkuShortName === 'entry',
'Skipping test - server not licensed for delegated granular administration',
);
}
/**
* @objective Verify a delegated administration role can be granted from the Manage Roles modal and persists.
*/
test(
'grants a delegated administration role from the Manage Roles modal',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);
// # Login as admin and open the Manage Roles modal for a regular user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);
// * Verify the Delegated Administration Roles section is shown for a regular member
await expect(manageRolesModal.delegatedRolesTitle).toBeVisible();
// # Grant the User Manager role and save
const userManagerCheckbox = manageRolesModal.getDelegatedRoleCheckbox(USER_MANAGER_LABEL);
await expect(userManagerCheckbox).not.toBeChecked();
await userManagerCheckbox.check();
await manageRolesModal.save();
// * Verify the role was persisted via the API
const updatedUser = await adminClient.getUser(user.id);
expect(updatedUser.roles).toContain(USER_MANAGER_ROLE);
},
);
/**
* @objective Verify the modal pre-selects delegated administration roles the user already has.
*/
test(
'pre-selects delegated administration roles the user already has',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);
// # Grant the System Manager role to the user up front
await adminClient.updateUserRoles(user.id, `system_user ${SYSTEM_MANAGER_ROLE}`);
// # Login as admin and open the Manage Roles modal for that user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);
// * Verify the already-granted role is pre-checked and others are not
await expect(manageRolesModal.getDelegatedRoleCheckbox(SYSTEM_MANAGER_LABEL)).toBeChecked();
await expect(manageRolesModal.getDelegatedRoleCheckbox(USER_MANAGER_LABEL)).not.toBeChecked();
},
);
/**
* @objective Verify the Delegated Administration Roles section is hidden when System Admin is selected.
*/
test(
'hides the delegated administration roles section when System Admin is selected',
{tag: ['@system_console', '@user_management']},
async ({pw}) => {
const {adminUser, adminClient, user} = await pw.initSetup();
await skipIfNoDelegatedAdminLicense(adminClient);
// # Login as admin and open the Manage Roles modal for a regular user
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const manageRolesModal = await openManageRolesModal(systemConsolePage, user);
// * Verify the section is initially visible for a member
await expect(manageRolesModal.delegatedRolesTitle).toBeVisible();
// # Promote the account to System Admin within the modal
await manageRolesModal.systemAdminRadio.check();
// * Verify the delegated roles section is hidden since System Admins already have full access
await expect(manageRolesModal.delegatedRolesTitle).not.toBeVisible();
},
);

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('MM-T5521-1 Should be able to search users with their first names', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 2 users
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Enter the 'First Name' of the first user in the search box
await systemConsolePage.users.searchUsers(user1.first_name);
// * Verify that the searched user i.e first user is found in the list
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the second user doesnt appear in the list
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-2 Should be able to search users with their last names', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 2 users
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Enter the 'Last Name' of the user in the search box
await systemConsolePage.users.searchUsers(user1.last_name);
// * Verify that the searched user i.e first user is found in the list
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the second user doesnt appear in the list
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-3 Should be able to search users with their emails', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 2 users
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// * Enter the 'Email' of the first user in the search box
await systemConsolePage.users.searchUsers(user1.email);
// * Verify that the searched user i.e first user is found in the list
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the second user doesnt appear in the list
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-4 Should be able to search users with their usernames', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 2 users
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
// # Enter the 'Username' of the first user in the search box
await systemConsolePage.users.searchUsers(user1.username);
// * Verify that the searched user i.e first user is found in the list
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the another user is not visible
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-5 Should be able to search users with their nick names', async ({pw}) => {
const {adminUser, adminClient} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Create 2 users
const user1 = await adminClient.createUser(await pw.random.user(), '', '');
const user2 = await adminClient.createUser(await pw.random.user(), '', '');
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
// # Enter the 'Nickname' of the first user in the search box
await systemConsolePage.users.searchUsers(user1.nickname);
// * Verify that the searched user i.e first user is found in the list
await expect(systemConsolePage.users.container.getByText(user1.email)).toBeVisible();
// * Verify that the second user doesnt appear in the list
await expect(systemConsolePage.users.container.getByText(user2.email)).not.toBeVisible();
});
test('MM-T5521-6 Should show no user is found when user doesnt exists', async ({pw}) => {
const {adminUser} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Log in as admin
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Visit system console
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// # Go to Users section
await systemConsolePage.sidebar.users.click();
// # Enter random text in the search box
await systemConsolePage.users.searchUsers(`!${pw.random.id(15)}_^^^_${pw.random.id(15)}!`);
await expect(systemConsolePage.users.container.getByText('No data')).toBeVisible();
});

View File

@@ -0,0 +1,602 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* LOCALIZATION NOTE:
* This test suite uses test attributes (data-testid, id) for buttons to avoid
* language dependencies, but still has some localization dependencies:
* - Error message text assertions (e.g., 'Invalid email address')
* - Field labels (e.g., 'Username', 'Email')
* - Test data values (e.g., 'JavaScript', 'Python' option names)
*
* Currently runs in English-only (locale: 'en-US' in playwright.config.ts).
* For multi-language support, these assertions would need refactoring.
*/
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import type {Client4} from '@mattermost/client';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import type {SystemConsolePage} from '@mattermost/playwright-lib';
import {expect, getRandomId, test} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../channels/custom_profile_attributes/helpers';
import {
setupCustomProfileAttributeFields,
setupCustomProfileAttributeValuesForUser,
deleteCustomProfileAttributes,
} from '../../channels/custom_profile_attributes/helpers';
/** Per-run CPA names — avoids reusing global fields (e.g. "Old Name") from other suites. */
let cpaFieldNames: {
department: string;
workEmail: string;
personalWebsite: string;
location: string;
skills: string;
};
/** Rendered label for each CPA field: attrs.display_name when set, else field.name. */
let cpaDisplayLabels: {
department: string;
workEmail: string;
personalWebsite: string;
location: string;
skills: string;
};
let testUserAttributes: CustomProfileAttribute[];
let team: Team;
let adminUser: UserProfile;
let testUser: UserProfile;
let attributeFieldsMap: Record<string, UserPropertyField> = {};
let adminClient: Client4;
let systemConsolePage: SystemConsolePage | undefined;
test.describe('System Console - Admin User Profile Editing', () => {
test.beforeEach(async ({pw}) => {
// Ensure license for Custom Profile Attributes functionality.
// isEnterpriseLicense() in the webapp only returns true for SKUs: enterprise, E20,
// advanced, entry. If the CI license is a lower tier, CPA rendering is gated off.
await pw.ensureLicense();
await pw.skipIfNoLicense();
// Self-isolating setup — avoid pw.initSetup()'s destructive
// adminClient.updateConfig() full-config reset which wipes CPA fields mid-run
// for other concurrent tests in the same worker pool. Create a uniquely-named
// team and user per test instead.
const clientInfo = await pw.getAdminClient();
if (!clientInfo.adminUser) {
throw new Error('Admin user not found');
}
adminClient = clientInfo.adminClient;
adminUser = clientInfo.adminUser;
const suffix = getRandomId();
cpaFieldNames = {
department: `UAAE_Department_${suffix}`,
workEmail: `UAAE_Work_Email_${suffix}`,
personalWebsite: `UAAE_Personal_Website_${suffix}`,
location: `UAAE_Location_${suffix}`,
skills: `UAAE_Skills_${suffix}`,
};
// Mirror display_name values from testUserAttributes; absent display_name falls back to name.
cpaDisplayLabels = {
department: cpaFieldNames.department,
workEmail: 'Work Email',
personalWebsite: 'Personal Website',
location: cpaFieldNames.location,
skills: cpaFieldNames.skills,
};
testUserAttributes = [
{
name: cpaFieldNames.department,
value: 'Engineering',
type: 'text',
attrs: {
visibility: 'when_set',
},
},
{
name: cpaFieldNames.workEmail,
value: 'work@company.com',
type: 'text',
attrs: {
value_type: 'email',
visibility: 'when_set',
display_name: 'Work Email',
},
},
{
name: cpaFieldNames.personalWebsite,
value: 'https://johndoe.com',
type: 'text',
attrs: {
value_type: 'url',
visibility: 'when_set',
display_name: 'Personal Website',
},
},
{
name: cpaFieldNames.location,
type: 'select',
attrs: {
visibility: 'when_set',
},
options: [
{name: 'Remote', color: '#00FFFF'},
{name: 'Office', color: '#FF00FF'},
{name: 'Hybrid', color: '#FFFF00'},
],
},
{
name: cpaFieldNames.skills,
type: 'multiselect',
attrs: {
visibility: 'when_set',
},
options: [
{name: 'JavaScript', color: '#F0DB4F'},
{name: 'React', color: '#61DAFB'},
{name: 'Python', color: '#3776AB'},
{name: 'Go', color: '#00ADD8'},
],
},
];
team = await adminClient.createTeam({
name: `uaae-${suffix}`,
display_name: `UAAE ${suffix}`,
type: 'O',
} as any);
await adminClient.addToTeam(team.id, adminUser.id);
// Create test user to edit
testUser = await pw.createNewUserProfile(adminClient, {prefix: 'admin-edit-target-'});
await adminClient.addToTeam(team.id, testUser.id);
// Pre-cleanup: delete stale UAAE-prefixed fields from previous runs of
// this suite. The prefix is unique to these tests; do not delete other
// suites' fields. attribute_order_and_popover now cleans up its own.
try {
const existingFields = await adminClient.getCustomProfileAttributeFields();
const staleUaaeFields = existingFields.filter((f) => f.name.startsWith('UAAE_'));
if (staleUaaeFields.length > 0) {
const staleMap: Record<string, UserPropertyField> = {};
for (const f of staleUaaeFields) {
staleMap[f.id] = f;
}
await deleteCustomProfileAttributes(adminClient, staleMap);
}
} catch {
// Best-effort — if cleanup fails, proceed and let the real error surface below.
}
// Set up custom user attribute fields
attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, testUserAttributes);
// Fail fast if any expected field was not created — setupCustomProfileAttributeFields
// silently swallows 422 errors (e.g. 20-field server limit), leaving the map incomplete.
// Without this check the test would only time out 30 s later at the UI assertion with a
// misleading "element not found" error.
const missingFields = testUserAttributes
.map((a) => a.name)
.filter((name) => !Object.values(attributeFieldsMap).some((f) => f.name === name));
if (missingFields.length > 0) {
const all = await adminClient.getCustomProfileAttributeFields().catch(() => []);
throw new Error(
`CPA field creation failed for: [${missingFields.join(', ')}]. ` +
`Server currently has ${all.length} fields: [${all.map((f) => f.name).join(', ')}]. ` +
'Possible 20-field limit breach — check for leaked fields from other test suites.',
);
}
// Fields reused by name can still carry access_mode=source_only from another suite; the admin
// user detail page hides those (system_user_detail.tsx) so no CPA labels ever appear.
const refreshedFields = await adminClient.getCustomProfileAttributeFields();
for (const attr of testUserAttributes) {
const field = refreshedFields.find((f) => f.name === attr.name);
if (field?.attrs?.access_mode === 'source_only') {
await adminClient.patchCustomProfileAttributeField(field.id, {
attrs: {...field.attrs, access_mode: ''},
} as any);
}
}
// Set initial custom attribute values for the test user
await setupCustomProfileAttributeValuesForUser(
adminClient,
testUserAttributes,
attributeFieldsMap,
testUser.id,
);
// Login as admin
({systemConsolePage} = await pw.testBrowser.login(adminUser));
// Navigate to system console users
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
await systemConsolePage.sidebar.users.click();
await systemConsolePage!.users.toBeVisible();
// Search for target user and navigate to user detail page
await systemConsolePage!.users.searchUsers(testUser.email);
const userRow = systemConsolePage!.users.usersTable.getRowByIndex(0);
await userRow.container.getByText(testUser.email).click();
// Wait for the initial navigation to the user detail page.
await systemConsolePage.page.waitForURL(`**/admin_console/user_management/user/${testUser.id}`);
// Freeze the fields API so concurrent shard activity (field creates/deletes) cannot
// trigger a WebSocket-driven re-fetch that wipes the CPA section from Redux mid-test.
const frozenFields = Object.values(attributeFieldsMap);
await systemConsolePage.page.route('**/api/v4/custom_profile_attributes/fields', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(frozenFields),
});
});
// Reload the page to clear the Redux CPA field cache.
//
// system_user_detail.tsx componentDidMount only calls getCustomProfileAttributeFields()
// when customProfileAttributeFields.length === 0 (line 226). Playwright reuses the same
// browser context across beforeEach runs, so the in-memory Redux store from the previous
// test still holds the OLD UAAE field definitions (e.g. UAAE_Department_fe45b8d). Even
// though afterEach deleted those fields from the server, Redux never clears them. On the
// next beforeEach the condition is false, the fetch is skipped, the stale labels render,
// and span:text-is("UAAE_Department_<new-suffix>") never matches — causing a 30 s timeout.
//
// A full page reload tears down the React/Redux state so componentDidMount starts with an
// empty store and unconditionally fetches the current (freshly created) fields.
await systemConsolePage.page.reload();
await systemConsolePage.page.waitForURL(`**/admin_console/user_management/user/${testUser.id}`);
await systemConsolePage!.users.userDetail.userCard.container.waitFor({state: 'visible'});
const {userCard} = systemConsolePage!.users.userDetail;
await expect(userCard.getFieldInputByExactLabel(cpaDisplayLabels.department)).toBeVisible({timeout: 30_000});
await expect(userCard.getFieldInputByExactLabel(cpaDisplayLabels.workEmail)).toBeVisible({timeout: 30_000});
// Remove the intercept now that field visibility is confirmed.
// Keeping it active through the test body would intercept the save API call
// (which also hits the /fields endpoint during submit), causing "Failed to update user".
// Validation tests restore their own intercept via try/finally.
await systemConsolePage.page.unroute('**/api/v4/custom_profile_attributes/fields').catch(() => {});
});
test.afterEach(async ({pw}) => {
// When beforeEach was skipped (e.g. test.skip()), attributeFieldsMap stays
// empty and there is nothing server-side to clean up.
if (Object.keys(attributeFieldsMap).length === 0) {
return;
}
// Safety-net unroute in case a validation test's try/finally was skipped by an
// earlier error, or the beforeEach unroute was never reached.
await systemConsolePage?.page.unroute('**/api/v4/custom_profile_attributes/fields').catch(() => {});
// Clean up custom user attribute fields
const {adminClient: cleanupClient} = await pw.getAdminClient();
await deleteCustomProfileAttributes(cleanupClient, attributeFieldsMap);
});
test('MM-65126 Should edit custom user attributes from system console', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// # Find and edit Department field (custom text attribute)
const departmentInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.department);
await departmentInput.clear();
await departmentInput.fill('Marketing');
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify success (no error message and field retains new value)
await expect(userDetail.errorMessage).not.toBeVisible();
await expect(departmentInput).toHaveValue('Marketing');
// * Verify Save button becomes disabled after successful save
await userDetail.waitForSaveComplete();
});
test('Should display user attributes in two-column layout', async () => {
const {userCard} = systemConsolePage!.users.userDetail;
// * Verify two-column layout exists
await expect(userCard.twoColumnLayout).toBeVisible();
// * Verify system fields are present
await expect(userCard.usernameInput).toBeVisible();
await expect(userCard.emailInput).toBeVisible();
await expect(userCard.authenticationMethod).toBeVisible();
// * Verify custom user attributes are present
for (const field of testUserAttributes) {
const label = field.attrs?.display_name || field.name;
await expect(systemConsolePage!.page.locator('label').filter({hasText: label})).toBeVisible();
}
// * Verify we have input fields (at least 4-5 total)
const inputElements = systemConsolePage!.page.locator('input, select');
const inputCount = await inputElements.count();
expect(inputCount).toBeGreaterThan(4);
// * Verify fields are arranged in rows with two columns
const rowCount = await userCard.fieldRows.count();
expect(rowCount).toBeGreaterThan(0);
});
test('Should edit system email attribute and save', async () => {
const {userDetail} = systemConsolePage!.users;
const {emailInput} = userDetail.userCard;
// # Enter new valid email
const newEmail = `updated-${testUser.email}`;
await emailInput.clear();
await emailInput.fill(newEmail);
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify success
await expect(userDetail.errorMessage).not.toBeVisible();
await expect(emailInput).toHaveValue(newEmail);
await userDetail.waitForSaveComplete();
});
test('Should edit custom select attribute and save', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// # Find Location select field
const locationSelect = userCard.getSelectByExactLabel(cpaDisplayLabels.location);
// # Get the first available option (since we can't predict the option value/ID)
const firstOption = await locationSelect.locator('option').nth(1); // Skip the default "Select an option"
const firstOptionValue = await firstOption.getAttribute('value');
await locationSelect.selectOption(firstOptionValue || '');
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify success and persistence
await expect(userDetail.errorMessage).not.toBeVisible();
// Don't check exact value since it's a generated ID, just verify it's not empty
const selectedValue = await locationSelect.inputValue();
expect(selectedValue).toBeTruthy();
await userDetail.waitForSaveComplete();
});
test('Should display custom multiselect attribute and save form', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// * Verify Skills multiselect component is displayed
const skillsColumn = userCard.getCpaMultiselectContainer(cpaDisplayLabels.skills);
await expect(skillsColumn).toBeVisible();
// # Make a change to a different field to trigger save state
const departmentInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.department);
await departmentInput.fill('Engineering Updated');
// # Verify save button becomes enabled
await expect(userDetail.saveButton).toBeEnabled();
// # Save the form and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify success (no error message)
await expect(userDetail.errorMessage).not.toBeVisible();
// * Verify save completed
await userDetail.waitForSaveComplete();
// * Verify the change persisted
await expect(departmentInput).toHaveValue('Engineering Updated');
});
test('Should validate invalid email and show error with cancel option', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// Re-apply the fields intercept for this validation test.
// Without it, a concurrent CPA test's afterEach can delete our fields via the
// setupCustomProfileAttributeFields early-return bug. The server then emits
// WebsocketEventCPAFieldDeleted; the browser re-fetches /fields; Redux clears;
// handleCpaValueChange finds field===undefined and skips setting the error state,
// so the field-error element never renders and the assertion times out.
const frozenFields = Object.values(attributeFieldsMap);
await systemConsolePage!.page.route('**/api/v4/custom_profile_attributes/fields', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(frozenFields),
});
});
try {
// # Find CPA email field (Work Email)
const workEmailInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.workEmail);
await workEmailInput.scrollIntoViewIfNeeded();
const originalEmail = await workEmailInput.inputValue();
// # Enter invalid email
await workEmailInput.clear();
await workEmailInput.fill('not-an-email');
// * Verify inline validation error appears
const fieldError = userCard.getFieldError(cpaDisplayLabels.workEmail);
await expect(fieldError).toBeVisible({timeout: 30000});
await expect(fieldError).toContainText('Invalid email address');
// * Verify Save button is disabled due to validation error
await expect(userDetail.saveButton).toBeDisabled();
// * Verify Cancel button is visible and enabled
await expect(userDetail.cancelButton).toBeVisible();
await expect(userDetail.cancelButton).toBeEnabled();
// # Test the cancel functionality
await userDetail.cancel();
// * Verify email reverts to original value
await expect(workEmailInput).toHaveValue(originalEmail);
// * Verify validation error disappears
await expect(fieldError).not.toBeVisible();
// * Verify Cancel button disappears
await expect(userDetail.cancelButton).not.toBeVisible();
// * Verify Save button remains disabled (no unsaved changes)
await expect(userDetail.saveButton).toBeDisabled();
} finally {
await systemConsolePage!.page.unroute('**/api/v4/custom_profile_attributes/fields').catch(() => {});
}
});
test('Should validate invalid URL and show error with cancel option', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// Re-apply the fields intercept — same race-condition guard as the email validation test.
const frozenFields = Object.values(attributeFieldsMap);
await systemConsolePage!.page.route('**/api/v4/custom_profile_attributes/fields', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(frozenFields),
});
});
try {
// # Find custom URL field (Personal Website)
const urlInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.personalWebsite);
const originalUrl = await urlInput.inputValue();
// # Enter invalid URL (specifically the one mentioned: "<%>")
await urlInput.clear();
await urlInput.fill('<%>');
// * Verify inline validation error appears
const fieldError = userCard.getFieldError(cpaDisplayLabels.personalWebsite);
await expect(fieldError).toBeVisible();
await expect(fieldError).toContainText('Invalid URL');
// * Verify Save button is disabled due to validation error
await expect(userDetail.saveButton).toBeDisabled();
// * Verify Cancel button is visible
await expect(userDetail.cancelButton).toBeVisible();
await expect(userDetail.cancelButton).toBeEnabled();
// # Test cancel functionality
await userDetail.cancel();
// * Verify URL reverts to original value
await expect(urlInput).toHaveValue(originalUrl);
// * Verify validation error disappears
await expect(fieldError).not.toBeVisible();
// * Verify Cancel button disappears
await expect(userDetail.cancelButton).not.toBeVisible();
} finally {
await systemConsolePage!.page.unroute('**/api/v4/custom_profile_attributes/fields').catch(() => {});
}
});
test('Should validate invalid email in custom email attribute', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// Re-apply the fields intercept — same race-condition guard as the other validation tests.
const frozenFields = Object.values(attributeFieldsMap);
await systemConsolePage!.page.route('**/api/v4/custom_profile_attributes/fields', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(frozenFields),
});
});
try {
// # Find custom email field (Work Email)
const workEmailInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.workEmail);
// # Enter invalid email
await workEmailInput.clear();
await workEmailInput.fill('not-an-email-either');
// * Verify inline validation error appears
const fieldError = userCard.getFieldError(cpaDisplayLabels.workEmail);
await expect(fieldError).toBeVisible();
await expect(fieldError).toContainText('Invalid email address');
// * Verify Save button is disabled due to validation error
await expect(userDetail.saveButton).toBeDisabled();
// * Verify Cancel button is available
await expect(userDetail.cancelButton).toBeVisible();
} finally {
await systemConsolePage!.page.unroute('**/api/v4/custom_profile_attributes/fields').catch(() => {});
}
});
test('Should show save/cancel buttons when changes are made', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// * Initially, Save should be disabled and Cancel should not be visible
await expect(userDetail.saveButton).toBeDisabled();
await expect(userDetail.cancelButton).not.toBeVisible();
// # Make a change to trigger save needed state
const departmentInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.department);
const originalValue = await departmentInput.inputValue();
await departmentInput.clear();
await departmentInput.fill('Changed Value');
// * Verify Save button becomes enabled and Cancel button appears
await expect(userDetail.saveButton).toBeEnabled();
await expect(userDetail.cancelButton).toBeVisible();
await expect(userDetail.cancelButton).toBeEnabled();
// # Click Cancel
await userDetail.cancel();
// * Verify changes are reverted
await expect(departmentInput).toHaveValue(originalValue);
// * Verify Cancel button disappears
await expect(userDetail.cancelButton).not.toBeVisible();
// * Verify Save button is disabled
await expect(userDetail.saveButton).toBeDisabled();
});
test('Should save all user attribute changes atomically', async () => {
const {userDetail} = systemConsolePage!.users;
const {userCard} = userDetail;
// # Make changes to both system and custom attributes
const newEmail = `atomic-test-${testUser.email}`;
await userCard.emailInput.clear();
await userCard.emailInput.fill(newEmail);
const departmentInput = userCard.getFieldInputByExactLabel(cpaDisplayLabels.department);
await departmentInput.clear();
await departmentInput.fill('Sales');
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify both changes were saved successfully
await expect(userDetail.errorMessage).not.toBeVisible();
await expect(userCard.emailInput).toHaveValue(newEmail);
await expect(departmentInput).toHaveValue('Sales');
await userDetail.waitForSaveComplete();
});
});

View File

@@ -0,0 +1,209 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {UserProfile} from '@mattermost/types/users';
import type {SystemConsolePage} from '@mattermost/playwright-lib';
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verifies that authentication data field is displayed and editable for users with auth_service
*/
test('displays and allows editing of authentication data field', {tag: '@user_management'}, async ({pw}) => {
const {user, adminUser, adminClient} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Generate unique auth data to avoid unique constraint errors
const originalAuthData = `auth-data-${pw.random.id()}`;
const newAuthData = `auth-data-${pw.random.id()}`;
// # Update user to have an auth service (simulate SAML/LDAP user)
await adminClient.updateUserAuth(user.id, {
auth_service: 'saml',
auth_data: originalAuthData,
});
// # Navigate to user detail page
await navigateToUserDetail(systemConsolePage, user);
const {userDetail} = systemConsolePage.users;
// * Verify auth data field is visible with correct value
const {authDataInput} = userDetail.userCard;
await expect(authDataInput).toBeVisible();
await expect(authDataInput).toHaveValue(originalAuthData);
// # Update the auth data value
await authDataInput.clear();
await authDataInput.fill(newAuthData);
// * Verify Save button is enabled after change
await expect(userDetail.saveButton).toBeEnabled();
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify the auth data field retains new value
await expect(authDataInput).toHaveValue(newAuthData);
// * Verify the change was saved by checking API
const updatedUser = await adminClient.getUser(user.id);
expect(updatedUser.auth_data).toBe(newAuthData);
});
/**
* @objective Verifies that email and username fields are disabled with tooltips when user has auth_service
*/
test('disables email and username fields for users with auth service', {tag: '@user_management'}, async ({pw}) => {
const {user, adminUser, adminClient} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Update user to have an auth service
await adminClient.updateUserAuth(user.id, {
auth_service: 'ldap',
auth_data: `ldap-user-data-${pw.random.id()}`,
});
// # Navigate to user detail page
await navigateToUserDetail(systemConsolePage, user);
const {userDetail} = systemConsolePage.users;
// * Verify email and username fields are disabled and read-only
const {emailInput, usernameInput} = userDetail.userCard;
await expect(emailInput).toBeDisabled();
await expect(emailInput).toHaveAttribute('readonly', '');
await expect(usernameInput).toBeDisabled();
await expect(usernameInput).toHaveAttribute('readonly', '');
// # Hover over email field to verify tooltip
await emailInput.hover();
const emailTooltip = systemConsolePage.page.getByText('This email is managed by the LDAP login provider');
await expect(emailTooltip).toBeVisible();
// # Hover over username field to verify tooltip
await usernameInput.hover();
const usernameTooltip = systemConsolePage.page.getByText('This username is managed by the LDAP login provider');
await expect(usernameTooltip).toBeVisible();
});
/**
* @objective Verifies that email and username fields are editable when user has no auth_service
*/
test('allows editing email and username fields for regular users', {tag: '@user_management'}, async ({pw}) => {
const {user, adminUser, adminClient} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Navigate to user detail page
await navigateToUserDetail(systemConsolePage, user);
const {userDetail} = systemConsolePage.users;
const {userCard} = userDetail;
// * Verify email and username fields are editable
await expect(userCard.emailInput).toBeEnabled();
await expect(userCard.usernameInput).toBeEnabled();
// # Update both email and username
const newEmail = `updated-${pw.random.id()}@example.com`;
const newUsername = `updated-${pw.random.id()}`;
await userCard.emailInput.clear();
await userCard.emailInput.fill(newEmail);
await userCard.usernameInput.clear();
await userCard.usernameInput.fill(newUsername);
// * Verify Save button is enabled after changes
await expect(userDetail.saveButton).toBeEnabled();
// # Click Save button and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
// * Verify fields retain new values
await expect(userCard.emailInput).toHaveValue(newEmail);
await expect(userCard.usernameInput).toHaveValue(newUsername);
// * Verify the changes were saved by checking API
const updatedUser = await adminClient.getUser(user.id);
expect(updatedUser.email).toBe(newEmail);
expect(updatedUser.username).toBe(newUsername);
});
/**
* @objective Verifies inline validation for email field
*/
test('displays inline validation errors for invalid email', {tag: '@user_management'}, async ({pw}) => {
const {user, adminUser} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Navigate to user detail page
await navigateToUserDetail(systemConsolePage, user);
const {userDetail} = systemConsolePage.users;
const {userCard} = userDetail;
// # Enter invalid email
await userCard.emailInput.clear();
await userCard.emailInput.fill('invalid-email');
// * Verify email validation error appears
const emailError = userCard.getFieldError('Email');
await expect(emailError).toBeVisible();
// * Verify Save button is disabled due to validation error
await expect(userDetail.saveButton).toBeDisabled();
// # Fix the email
await userCard.emailInput.clear();
await userCard.emailInput.fill('valid@example.com');
// * Verify email error disappears
await expect(emailError).not.toBeVisible();
// * Verify Save button is now enabled
await expect(userDetail.saveButton).toBeEnabled();
});
/**
* @objective Verifies confirmation dialog can be cancelled
*/
test('allows cancelling save confirmation dialog', {tag: '@user_management'}, async ({pw}) => {
const {user, adminUser} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Navigate to user detail page
await navigateToUserDetail(systemConsolePage, user);
const {userDetail} = systemConsolePage.users;
// # Update email field
const newEmail = `cancelled-${pw.random.id()}@example.com`;
await userDetail.userCard.emailInput.clear();
await userDetail.userCard.emailInput.fill(newEmail);
// # Click Save button
await userDetail.save();
// * Verify confirmation modal appears
await userDetail.saveChangesModal.toBeVisible();
// # Click Cancel
await userDetail.saveChangesModal.cancel();
await expect(userDetail.userCard.emailInput).toHaveValue(newEmail);
// * Verify Save button is still enabled (unsaved changes remain)
await expect(userDetail.saveButton).toBeEnabled();
});
/**
* Navigate to the user detail page for a given user.
*/
async function navigateToUserDetail(systemConsolePage: SystemConsolePage, user: UserProfile) {
await systemConsolePage.goto();
await systemConsolePage.sidebar.users.click();
await systemConsolePage.users.toBeVisible();
await systemConsolePage.users.searchUsers(user.email);
const userRow = systemConsolePage.users.usersTable.getRowByIndex(0);
await expect(userRow.container.getByText(user.email)).toBeVisible();
await userRow.container.getByText(user.email).click();
await systemConsolePage.users.userDetail.toBeVisible();
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* E2E tests for the ranked-value picker on the System Console user detail page.
*
* A ranked Custom Profile Attribute renders a menu picker that lists values
* highest-rank-first, each with a numbered badge, and a checkmark on the
* currently selected value.
*/
import type {Client4} from '@mattermost/client';
import type {UserProfile} from '@mattermost/types/users';
import type {PropertyFieldOption} from '@mattermost/types/properties';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import {expect, getRandomId, test} from '@mattermost/playwright-lib';
import {deleteCustomProfileAttributes} from '../../channels/custom_profile_attributes/helpers';
function optionId(field: UserPropertyField, name: string): string {
const option = (field.attrs.options ?? []).find((o: PropertyFieldOption) => o.name === name);
if (!option) {
throw new Error(`Option "${name}" not found on field ${field.name}`);
}
return option.id;
}
test.describe('System Console - Ranked value picker', () => {
let adminClient: Client4;
let adminUser: UserProfile;
let testUser: UserProfile;
let field: UserPropertyField | undefined;
test.beforeEach(async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
const clientInfo = await pw.getAdminClient();
adminClient = clientInfo.adminClient;
adminUser = clientInfo.adminUser!;
// # Create a ranked attribute (highest rank = most privileged)
field = await adminClient.createCustomProfileAttributeField({
name: `clearance_${getRandomId()}`,
type: 'rank',
attrs: {
sort_order: 0,
visibility: 'always',
options: [
{name: 'Unclassified', rank: 1},
{name: 'Secret', rank: 2},
{name: 'TopSecret', rank: 3},
],
},
} as any);
// # Create a target user and pre-assign the "Secret" value
testUser = await pw.createNewUserProfile(adminClient, {prefix: 'rank-picker-target-'});
await adminClient.updateUserCustomProfileAttributesValues(testUser.id, {
[field.id]: optionId(field, 'Secret'),
});
});
test.afterEach(async () => {
if (field) {
await deleteCustomProfileAttributes(adminClient, {
[field.id]: field,
__ownedIds: new Set([field.id]),
} as any);
field = undefined;
}
});
/**
* @objective The ranked picker lists values highest-rank-first with numbered
* badges and a checkmark on the assigned value; choosing a new value and
* saving persists it.
*
* @precondition
* A ranked attribute (Unclassified/Secret/TopSecret) exists and the target
* user is assigned "Secret".
*/
test('renders ranked values highest-first and updates the assignment', async ({pw}) => {
const label = field!.name;
// # Log in as admin and open the target user's detail page
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
await systemConsolePage.page.goto(`/admin_console/user_management/user/${testUser.id}`);
await systemConsolePage.page.waitForURL(`**/admin_console/user_management/user/${testUser.id}`);
const {userDetail} = systemConsolePage.users;
const {userCard} = userDetail;
await expect(userCard.getCpaRankPicker(label)).toBeVisible({timeout: 30_000});
// * The closed picker shows the assigned value
await expect(userCard.getCpaRankPicker(label)).toContainText('Secret');
// # Open the picker
await userCard.openCpaRankPicker(label);
// * Values are listed highest-rank-first, each with its numbered badge
const items = userCard.cpaRankMenuItems();
await expect(items).toHaveCount(3);
await expect(items.nth(0)).toContainText('TopSecret');
await expect(items.nth(0)).toContainText('3');
await expect(items.nth(1)).toContainText('Secret');
await expect(items.nth(2)).toContainText('Unclassified');
await expect(items.nth(2)).toContainText('1');
// * The assigned value (Secret) carries the selected/checked state
const secretItem = items.filter({has: systemConsolePage.page.getByText('Secret', {exact: true})});
await expect(secretItem).toHaveAttribute('aria-checked', 'true');
// # Choose TopSecret
await userCard.cpaRankMenu().getByText('TopSecret', {exact: true}).click();
// * The closed picker reflects the new value
await expect(userCard.getCpaRankPicker(label)).toContainText('TopSecret');
// # Save and confirm
await userDetail.save();
await userDetail.saveChangesModal.confirm();
await expect(userDetail.errorMessage).not.toBeVisible();
await userDetail.waitForSaveComplete();
// * The new assignment persisted (the TopSecret option id)
await expect
.poll(async () => {
const values = await adminClient.getUserCustomProfileAttributesValues(testUser.id);
return values[field!.id];
})
.toBe(optionId(field!, 'TopSecret'));
});
});