E2E/Playwright: Upgrade Playwright to 1.61 and its deps (#37277)

* chore: upgrade playwright to 1.61 and its deps

* fix failed tests

* address comments

* use assignTeamsToPolicy helper method
This commit is contained in:
sabril
2026-06-30 04:38:05 +00:00
committed by GitHub
parent 743b565c2d
commit d85da5ce2c
14 changed files with 2216 additions and 2946 deletions
@@ -210,7 +210,7 @@ jobs:
# tar for all future runs — no need for a retry loop.
run: |
set -euo pipefail
IMAGE="mcr.microsoft.com/playwright:v1.59.1-noble"
IMAGE="mcr.microsoft.com/playwright:v1.61.0-noble"
TAR="/tmp/playwright-docker-image.tar"
if [ -f "${TAR}" ]; then
echo "Loading Playwright image from GitHub Actions cache"
+1 -1
View File
@@ -289,7 +289,7 @@ $(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
# shellcheck disable=SC2016
echo '
playwright:
image: mcr.microsoft.com/playwright:v1.59.1-noble
image: mcr.microsoft.com/playwright:v1.61.0-noble
entrypoint: ["/bin/bash", "-c"]
command:
- |
+1 -1
View File
@@ -150,7 +150,7 @@ test(
Change to the `./` project directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
```bash
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.59.1-noble /bin/bash
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.61.0-noble /bin/bash
```
#### 2. Inside the docker container
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@mattermost/playwright-lib",
"version": "11.7.0",
"version": "11.10.0",
"description": "A comprehensive end-to-end testing library for Mattermost web, desktop and plugin applications using Playwright",
"repository": {
"type": "git",
@@ -43,26 +43,26 @@
"access": "public"
},
"dependencies": {
"@axe-core/playwright": "4.11.1",
"@axe-core/playwright": "4.12.1",
"@mattermost/client": "file:../../../webapp/platform/client",
"@mattermost/types": "file:../../../webapp/platform/types",
"@percy/cli": "1.31.11",
"@percy/cli": "1.32.2",
"@percy/playwright": "1.1.0",
"async-wait-until": "2.0.31",
"axe-core": "4.11.2",
"axe-core": "4.12.1",
"chalk": "5.6.2",
"deepmerge": "4.3.1",
"dotenv": "17.4.2",
"luxon": "3.7.2",
"mime-types": "3.0.2",
"uuid": "13.0.0"
"uuid": "14.0.1"
},
"devDependencies": {
"@rollup/plugin-typescript": "12.3.0",
"@types/luxon": "3.7.1",
"@types/luxon": "3.7.2",
"@types/mime-types": "3.0.1",
"@types/node": "25.6.0",
"rollup": "4.60.1",
"@types/node": "26.0.1",
"rollup": "4.62.2",
"rollup-plugin-copy": "3.5.0"
},
"peerDependencies": {
@@ -3,6 +3,7 @@
import merge from 'deepmerge';
import type {
AccessControlSettings,
AdminConfig,
ClusterSettings,
EmailSettings,
@@ -26,6 +27,7 @@ export function mergeWithOnPremServerConfig(overrides: Partial<AdminConfig>): Ad
}
type TestAdminConfig = {
AccessControlSettings: Partial<AccessControlSettings>;
ClusterSettings: Partial<ClusterSettings>;
EmailSettings: Partial<EmailSettings>;
ExperimentalSettings: Partial<ExperimentalSettings>;
@@ -39,11 +41,16 @@ type TestAdminConfig = {
// On-prem setting that is different from the default
const onPremServerConfig = (): Partial<TestAdminConfig> => {
return {
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
},
ClusterSettings: {
Enable: testConfig.haClusterEnabled,
ClusterName: testConfig.haClusterName,
},
EmailSettings: {
FeedbackName: 'Mattermost',
PushNotificationServer: testConfig.pushNotificationServer,
},
LogSettings: {
@@ -77,6 +84,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
EnableSecurityFixAlert: false,
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
EnableTesting: true,
AllowedUntrustedInternalConnections: 'localhost 127.0.0.1',
},
TeamSettings: {
EnableOpenServer: true,
@@ -86,7 +94,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
};
// Should be based only from the generated default config from ./server via "make config-reset"
// Based on v11.7 server
// Based on v11.9 server
const defaultServerConfig: AdminConfig = {
ServiceSettings: {
SiteURL: '',
@@ -129,7 +137,7 @@ const defaultServerConfig: AdminConfig = {
EnableClientPerformanceDebugging: false,
EnableSecurityFixAlert: true,
EnableInsecureOutgoingConnections: false,
AllowedUntrustedInternalConnections: 'localhost,127.0.0.1',
AllowedUntrustedInternalConnections: '',
EnableMultifactorAuthentication: false,
EnforceMultifactorAuthentication: false,
EnableUserAccessTokens: false,
@@ -233,6 +241,7 @@ const defaultServerConfig: AdminConfig = {
EnableLastActiveTime: true,
UserStatusAwayTimeout: 300,
MaxChannelsPerTeam: 2000,
EnableChannelCategorySorting: true,
MaxNotificationsPerChannel: 1000,
EnableConfirmNotificationsToChannel: true,
TeammateNameDisplay: 'username',
@@ -240,7 +249,6 @@ const defaultServerConfig: AdminConfig = {
LockTeammateNameDisplay: false,
ExperimentalPrimaryTeam: '',
ExperimentalDefaultChannels: [],
EnableChannelCategorySorting: true,
},
ClientRequirements: {
AndroidLatestVersion: '',
@@ -289,7 +297,7 @@ const defaultServerConfig: AdminConfig = {
Certificate: '',
},
PasswordSettings: {
MinimumLength: 14,
MinimumLength: 8,
Lowercase: false,
Number: false,
Uppercase: false,
@@ -324,6 +332,15 @@ const defaultServerConfig: AdminConfig = {
AmazonS3RequestTimeoutMilliseconds: 30000,
AmazonS3UploadPartSizeBytes: 5242880,
AmazonS3StorageClass: '',
AzureStorageAccount: '',
AzureAuthMode: 'shared_key',
AzureAccessKey: '',
AzureContainer: '',
AzurePathPrefix: '',
AzureCloud: 'commercial',
AzureEndpoint: '',
AzureSSL: true,
AzureRequestTimeoutMilliseconds: 30000,
DedicatedExportStore: false,
ExportDriverName: 'local',
ExportDirectory: './data/',
@@ -341,6 +358,16 @@ const defaultServerConfig: AdminConfig = {
ExportAmazonS3PresignExpiresSeconds: 21600,
ExportAmazonS3UploadPartSizeBytes: 104857600,
ExportAmazonS3StorageClass: '',
ExportAzureStorageAccount: '',
ExportAzureAuthMode: 'shared_key',
ExportAzureAccessKey: '',
ExportAzureContainer: '',
ExportAzurePathPrefix: '',
ExportAzureCloud: 'commercial',
ExportAzureEndpoint: '',
ExportAzureSSL: true,
ExportAzureRequestTimeoutMilliseconds: 30000,
ExportAzurePresignExpiresSeconds: 21600,
},
EmailSettings: {
EnableSignUpWithEmail: true,
@@ -349,7 +376,7 @@ const defaultServerConfig: AdminConfig = {
SendEmailNotifications: true,
UseChannelInEmailNotifications: false,
RequireEmailVerification: false,
FeedbackName: 'Mattermost',
FeedbackName: '',
FeedbackEmail: 'test@example.com',
ReplyToAddress: 'test@example.com',
FeedbackOrganization: '',
@@ -655,7 +682,7 @@ const defaultServerConfig: AdminConfig = {
ClientKey: '',
Trace: '',
IgnoredPurgeIndexes: '',
EnableSearchPublicChannelsWithoutMembership: false,
EnableSearchPublicChannelsWithoutMembership: true,
},
DataRetentionSettings: {
EnableMessageDeletion: false,
@@ -672,6 +699,12 @@ const defaultServerConfig: AdminConfig = {
RetentionIdsBatchSize: 100,
PreservePinnedPosts: false,
},
MobileEphemeralModeSettings: {
Enable: false,
DisconnectionTimeoutSeconds: 60,
OfflinePersistenceTimerHours: 24,
AutoCacheCleanupDays: 7,
},
MessageExportSettings: {
EnableExport: false,
ExportFormat: 'actiance',
@@ -774,20 +807,28 @@ const defaultServerConfig: AdminConfig = {
ExperimentalAuditSettingsSystemConsoleUI: true,
CustomProfileAttributes: true,
AttributeBasedAccessControl: true,
PermissionPolicies: true,
AttributeValueMasking: false,
PermissionPolicies: false,
ChannelPermissionPolicies: false,
PolicySimulation: false,
ContentFlagging: true,
EnableMattermostEntry: true,
MobileSSOCodeExchange: false,
EnableShiftEscapeToMarkAllRead: false,
AutoTranslation: true,
ClassificationMarkings: true,
BurnOnRead: true,
EnableAIPluginBridge: false,
EnableAIRecaps: false,
ClassificationMarkings: true,
PropertyFieldRank: true,
IntegratedBoards: false,
CJKSearch: true,
AggregatePluginMetrics: false,
ManagedChannelCategories: false,
MobileEphemeralMode: true,
SessionAttributes: false,
DiscoverableChannels: false,
MobileEphemeralMode: false,
PropertyFieldRank: false,
TeamMembershipAccessControl: false,
MmBlocksEnabled: true,
},
ImportSettings: {
@@ -817,13 +858,13 @@ const defaultServerConfig: AdminConfig = {
MemberSyncBatchSize: 20,
},
AccessControlSettings: {
EnableAttributeBasedAccessControl: true,
EnableUserManagedAttributes: true,
EnableAttributeBasedAccessControl: false,
EnableUserManagedAttributes: false,
TrustProxyDeviceIdentityHeader: false,
EnforceDeviceIDConsistency: false,
},
ContentFlaggingSettings: {
EnableContentFlagging: true,
EnableContentFlagging: false,
NotificationSettings: {
EventTargetMapping: {
assigned: ['reviewers'],
@@ -850,7 +891,7 @@ const defaultServerConfig: AdminConfig = {
CommonReviewers: true,
CommonReviewerIds: [],
TeamReviewersSetting: {},
SystemAdminsAsReviewers: true,
SystemAdminsAsReviewers: false,
TeamAdminsAsReviewers: true,
},
},
@@ -869,10 +910,4 @@ const defaultServerConfig: AdminConfig = {
LLMServiceID: '',
},
},
MobileEphemeralModeSettings: {
Enable: false,
DisconnectionTimeoutSeconds: 60,
OfflinePersistenceTimerHours: 24,
AutoCacheCleanupDays: 7,
},
};
@@ -52,12 +52,7 @@ export class ColumnToggleMenu {
}
type RoleFilter =
| 'Any'
| 'System Admin'
| 'Member'
| 'Guests (all)'
| 'Guests in a single channel'
| 'Guests in multiple channels';
'Any' | 'System Admin' | 'Member' | 'Guests (all)' | 'Guests in a single channel' | 'Guests in multiple channels';
type StatusFilter = 'Any' | 'Activated users' | 'Deactivated users';
/**
+2046 -2867
View File
File diff suppressed because it is too large Load Diff
+12 -11
View File
@@ -35,19 +35,20 @@
},
"devDependencies": {
"@mattermost/eslint-plugin": "file:../../webapp/platform/eslint-plugin",
"@playwright/test": "1.59.1",
"@types/luxon": "3.7.1",
"@types/pg": "8.15.4",
"@typescript-eslint/eslint-plugin": "8.58.1",
"@playwright/test": "1.61.1",
"@types/luxon": "3.7.2",
"@types/pg": "8.20.0",
"@typescript-eslint/eslint-plugin": "8.62.0",
"cross-env": "10.1.0",
"dayjs": "1.11.20",
"eslint": "9.39.2",
"eslint-import-resolver-typescript": "^4.4.5",
"dayjs": "1.11.21",
"eslint": "9.39.4",
"eslint-import-resolver-typescript": "4.4.5",
"globals": "17.7.0",
"glob": "13.0.6",
"luxon": "3.7.2",
"pg": "8.13.1",
"prettier": "3.8.2",
"typescript": "6.0.2",
"zod": "4.3.6"
"pg": "8.22.0",
"prettier": "3.9.1",
"typescript": "6.0.3",
"zod": "4.4.3"
}
}
@@ -24,7 +24,7 @@ import {
* MM-T5800: Policy enforcement after attribute change (bidirectional)
*/
test('MM-T5800 Policy enforcement after attribute change (bidirectional)', async ({pw}) => {
test.setTimeout(120000);
test.setTimeout(180000);
await pw.skipIfNoLicense();
@@ -9,6 +9,7 @@ import {
createTeamAdmin,
waitForAttributeViewToInclude,
} from '../../../channels/team_settings/helpers';
import {assignTeamsToPolicy} from '../teams/helpers';
import {enableUserManagedAttributes} from '../support';
import {
@@ -66,21 +67,18 @@ test.describe('Attribute-Value Masking - Admin Roles', {tag: ['@abac', '@abac_ma
`user.attributes.${fieldName} in ["Alpha", "Bravo"]`,
);
policyIds.push(policyId);
await setFieldAsSharedOnly(fieldId);
// Assign team and a channel to the policy so team settings shows the policy
// and the Remove link is present to exercise the "even after removing all channels" path.
// Assign channel first so the attribute view can be primed before
// we flip the field to shared_only (self-inclusion filter in
// SearchTeamAccessPolicies requires the view to be current).
await adminClient.addToTeam(team.id, adminUser.id);
const channel = await createPrivateChannel(adminClient, team.id);
await assignChannelsToPolicy(adminClient, policyId, [channel.id]);
try {
await (adminClient as any).doFetch(
`${(adminClient as any).getBaseRoute()}/access_control_policies/${policyId}/teams`,
{method: 'POST', body: JSON.stringify({team_id: team.id})},
);
} catch {
// best-effort assignment — test still validates button state
}
await waitForAttributeViewToInclude(adminClient, `user.attributes.${fieldName} in ["Alpha", "Bravo"]`, [
adminUser.id,
]);
await setFieldAsSharedOnly(fieldId);
await assignTeamsToPolicy(adminClient, policyId, [team.id]);
const {page} = await pw.testBrowser.login(adminUser);
const channelsPage = new ChannelsPage(page);
@@ -5,6 +5,7 @@ import {expect, test, navigateToABACPage, runSyncJob, verifyUserInChannel} from
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {waitForAttributeViewToInclude} from '../../../channels/team_settings/helpers';
import {
ensureUserAttributes,
createUserForABAC,
@@ -67,14 +68,19 @@ test('MM-T5786 Test "is not" (!=) operator in Simple mode', async ({pw}) => {
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department != "Sales"', [engineerUser.id]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
const engInChannel = await verifyUserInChannel(adminClient, engineerUser.id, channel.id);
const salesInChannel = await verifyUserInChannel(adminClient, salesUser.id, channel.id);
expect(engInChannel).toBe(true);
expect(salesInChannel).toBe(false);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});
/**
@@ -128,14 +134,21 @@ test('MM-T5786 Test "in" operator in Simple mode', async ({pw}) => {
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department in ["Engineering", "DevOps"]', [
engineerUser.id,
]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
const engInChannel = await verifyUserInChannel(adminClient, engineerUser.id, channel.id);
const salesInChannel = await verifyUserInChannel(adminClient, salesUser.id, channel.id);
expect(engInChannel).toBe(true);
expect(salesInChannel).toBe(false);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});
/**
@@ -189,12 +202,17 @@ test('MM-T5786 Test "starts with" operator in Simple mode', async ({pw}) => {
await navigateToABACPage(systemConsolePage.page);
}
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department.startsWith("Eng")', [engineerUser.id]);
await activatePolicy(adminClient, policyId);
await runSyncJob(systemConsolePage.page);
await waitForPolicySyncJob(adminClient, policyId);
const engInChannel = await verifyUserInChannel(adminClient, engineerUser.id, channel.id);
const salesInChannel = await verifyUserInChannel(adminClient, salesUser.id, channel.id);
expect(engInChannel).toBe(true);
expect(salesInChannel).toBe(false);
await expect
.poll(() => verifyUserInChannel(adminClient, engineerUser.id, channel.id), {
timeout: 15_000,
intervals: [500, 1000, 2000],
message: 'engineerUser should be in channel after sync',
})
.toBe(true);
expect(await verifyUserInChannel(adminClient, salesUser.id, channel.id)).toBe(false);
});
@@ -20,6 +20,7 @@ import {
getPolicyIdByName,
enableUserManagedAttributes,
} from '../support';
import {deleteFieldFromDB} from '../masking/masking_db_setup';
// Restore AccessControlSettings to the shared baseline expected by
// `specs/test_setup.ts` (ABAC enabled) after this file's tests complete, so
@@ -62,13 +63,25 @@ test('MM-T5791 Editing policy to add attribute with auto-add enabled', async ({p
const {adminUser, adminClient, team} = await pw.initSetup();
// Use ensure-exists pattern - non-destructive, safe for parallel test runs
// Ensure Office field exists with correct managed:admin permissions.
// Fields created by older server versions may lack permission_values, causing 403
// when setting attribute values — delete and recreate if permission_values is missing.
// Legacy fields with permission_field=null reject API deletion (403), so fall back to DB.
const existingFields = await adminClient.getCustomProfileAttributeFields();
const attributeFieldsMap: Record<string, any> = {};
for (const field of existingFields) {
attributeFieldsMap[field.id] = field;
}
if (!existingFields.some((f: any) => f.name === 'Office')) {
const existingOffice = existingFields.find((f: any) => f.name === 'Office') as any;
if (!existingOffice || !(existingOffice as any).permission_values) {
if (existingOffice) {
try {
await adminClient.deleteCustomProfileAttributeField(existingOffice.id);
} catch {
await deleteFieldFromDB(existingOffice.id);
}
delete attributeFieldsMap[existingOffice.id];
}
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
@@ -318,13 +331,25 @@ test('MM-T5792 Editing policy to remove attribute rule with auto-add enabled', a
const {adminUser, adminClient, team} = await pw.initSetup();
// Use ensure-exists pattern - non-destructive, safe for parallel test runs
// Ensure Office field exists with correct managed:admin permissions.
// Fields created by older server versions may lack permission_values, causing 403
// when setting attribute values — delete and recreate if permission_values is missing.
// Legacy fields with permission_field=null reject API deletion (403), so fall back to DB.
const existingFields = await adminClient.getCustomProfileAttributeFields();
const attributeFieldsMap: Record<string, any> = {};
for (const field of existingFields) {
attributeFieldsMap[field.id] = field;
}
if (!existingFields.some((f: any) => f.name === 'Office')) {
const existingOffice = existingFields.find((f: any) => f.name === 'Office') as any;
if (!existingOffice || !(existingOffice as any).permission_values) {
if (existingOffice) {
try {
await adminClient.deleteCustomProfileAttributeField(existingOffice.id);
} catch {
await deleteFieldFromDB(existingOffice.id);
}
delete attributeFieldsMap[existingOffice.id];
}
const officeField = await adminClient.createCustomProfileAttributeField({
name: 'Office',
type: 'text',
@@ -1000,7 +1000,7 @@ export async function waitForLatestSyncJob(
* Uses `expect.poll` with 500 ms intervals and a 30 s timeout so jobs that are
* briefly delayed in the queue do not cause spurious failures.
*/
export async function waitForPolicySyncJob(client: Client4, policyId: string): Promise<void> {
export async function waitForPolicySyncJob(client: Client4, policyId: string, timeoutMs = 60_000): Promise<void> {
await expect
.poll(
async () => {
@@ -1030,9 +1030,9 @@ export async function waitForPolicySyncJob(client: Client4, policyId: string): P
}
},
{
timeout: 30_000,
timeout: timeoutMs,
intervals: [500, 500, 500, 1000, 1000, 2000],
message: `Policy sync job for ${policyId} did not reach success within 30 s`,
message: `Policy sync job for ${policyId} did not reach success within ${timeoutMs / 1000} s`,
},
)
.toBe('success');
+19
View File
@@ -585,6 +585,15 @@ export type FileSettings = {
AmazonS3RequestTimeoutMilliseconds: number;
AmazonS3UploadPartSizeBytes: number;
AmazonS3StorageClass: string;
AzureStorageAccount: string;
AzureAuthMode: string;
AzureAccessKey: string;
AzureContainer: string;
AzurePathPrefix: string;
AzureCloud: string;
AzureEndpoint: string;
AzureSSL: boolean;
AzureRequestTimeoutMilliseconds: number;
DedicatedExportStore: boolean;
ExportDriverName: string;
ExportDirectory: string;
@@ -602,6 +611,16 @@ export type FileSettings = {
ExportAmazonS3PresignExpiresSeconds: number;
ExportAmazonS3UploadPartSizeBytes: number;
ExportAmazonS3StorageClass: string;
ExportAzureStorageAccount: string;
ExportAzureAuthMode: string;
ExportAzureAccessKey: string;
ExportAzureContainer: string;
ExportAzurePathPrefix: string;
ExportAzureCloud: string;
ExportAzureEndpoint: string;
ExportAzureSSL: boolean;
ExportAzureRequestTimeoutMilliseconds: number;
ExportAzurePresignExpiresSeconds: number;
};
export type EmailSettings = {