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
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:
@@ -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};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user