Этот коммит содержится в:
Mario Vitale
2023-03-27 16:28:42 +02:00
родитель da7a6825ce
Коммит ba6b97fb62
1142 изменённых файлов: 44 добавлений и 44 удалений

Двоичные данные
e2e-tests/playwright/support/asset/mattermost-icon_128x128.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 13 KiB

Просмотреть файл

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {writeFile} from 'node:fs/promises';
import {request, Browser} from '@playwright/test';
import {UserProfile} from '@mattermost/types/users';
import testConfig from '@e2e-test.config';
export class TestBrowser {
readonly browser: Browser;
constructor(browser: Browser) {
this.browser = browser;
}
async login(user: UserProfile | null) {
const options = {storageState: ''};
if (user) {
// Log in via API request and save user storage
const storagePath = await loginByAPI(user.username, user.password);
options.storageState = storagePath;
}
// Sign in a user in new browser context
const context = await this.browser.newContext(options);
const page = await context.newPage();
return {context, page};
}
}
export async function loginByAPI(loginId: string, password: string, token = '', ldapOnly = false) {
const requestContext = await request.newContext();
const data: any = {
login_id: loginId,
password,
token,
deviceId: '',
};
if (ldapOnly) {
data.ldap_only = 'true';
}
// Log in via API
await requestContext.post(`${testConfig.baseURL}/api/v4/users/login`, {
data,
headers: {'X-Requested-With': 'XMLHttpRequest'},
});
// Save signed-in state to a folder
const storagePath = `storage_state/${Date.now()}_${loginId}_${password}${token ? '_' + token : ''}${
ldapOnly ? '_ldap' : ''
}.json`;
const storageState = await requestContext.storageState({path: storagePath});
await requestContext.dispose();
// Append origins to bypass seeing landing page then write to file
storageState.origins.push({
origin: testConfig.baseURL,
localStorage: [{name: '__landingPageSeen__', value: 'true'}],
});
await writeFile(storagePath, JSON.stringify(storageState));
return storagePath;
}

7
e2e-tests/playwright/support/constant.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const boardsPluginId = 'focalboard';
export const boardsProductId = 'boards';
export const callsPluginId = 'com.mattermost.calls';
export const playbooksPluginId = 'playbooks';

52
e2e-tests/playwright/support/flag.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import os from 'node:os';
import {expect, test} from '@playwright/test';
import {boardsPluginId, callsPluginId} from './constant';
import {getAdminClient} from './server/init';
import {isSmallScreen} from './util';
export async function shouldHaveBoardsEnabled(enabled = true) {
const {adminClient} = await getAdminClient();
const config = await adminClient.getConfig();
const boardsEnabled =
(typeof config.FeatureFlags.BoardsProduct === 'boolean' && config.FeatureFlags.BoardsProduct) ||
config.PluginSettings.PluginStates[boardsPluginId].Enable;
const matched = boardsEnabled === enabled;
expect(matched, matched ? '' : `Boards expect "${enabled}" but actual "${boardsEnabled}"`).toBeTruthy();
}
export async function shouldHaveCallsEnabled(enabled = true) {
const {adminClient} = await getAdminClient();
const config = await adminClient.getConfig();
const callsEnabled = config.PluginSettings.PluginStates[callsPluginId].Enable;
const matched = callsEnabled === enabled;
expect(matched, matched ? '' : `Calls expect "${enabled}" but actual "${callsEnabled}"`).toBeTruthy();
}
export async function shouldHaveFeatureFlag(name: string, value: string | boolean) {
const {adminClient} = await getAdminClient();
const config = await adminClient.getConfig();
const matched = config.FeatureFlags[name] === value;
expect(
matched,
matched ? '' : `FeatureFlags["${name}'] expect "${value}" but actual "${config.FeatureFlags[name]}"`
).toBeTruthy();
}
export function shouldSkipInSmallScreen() {
test.skip(({viewport}) => isSmallScreen(viewport), 'Not applicable to mobile device');
}
export async function shouldRunInLinux() {
const platform = os.platform();
await expect(platform, 'Run in Linux or Playwright docker image only').toBe('linux');
}

Просмотреть файл

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getRandomId} from '@e2e-support/util';
import {Channel, ChannelType} from '@mattermost/types/channels';
export function createRandomChannel(
teamId: string,
name: string,
displayName: string,
type: ChannelType = 'O',
purpose = '',
header = '',
unique = true
): Channel {
const randomSuffix = getRandomId();
const channel = {
team_id: teamId,
name: unique ? `${name}-${randomSuffix}` : name,
display_name: unique ? `${displayName} ${randomSuffix}` : displayName,
type,
purpose,
header,
};
return channel as Channel;
}

213
e2e-tests/playwright/support/server/client.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,213 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This is based on "packages/client/src/client4.ts". Modified for node client.
// Update should be made in comparison with the base Client4.
import fs from 'node:fs';
import path from 'node:path';
import FormData from 'form-data';
import 'isomorphic-unfetch';
import testConfig from '@e2e-test.config';
import Client4 from '@mattermost/client/client4';
import {Options, StatusOK} from '@mattermost/types/client4';
import {License} from '@mattermost/types/config';
import {CustomEmoji} from '@mattermost/types/emojis';
import {PluginManifest} from '@mattermost/types/plugins';
import {UserProfile} from '@mattermost/types/users';
export default class Client extends Client4 {
getFormDataOptions = (formData: FormData): Options => {
return {
method: 'post',
body: formData,
headers: {
'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
},
};
};
uploadProfileImageX = (userId: string, filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('image', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getUserRoute(userId)}/image`, options);
};
setTeamIconX = (teamId: string, filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('image', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getTeamRoute(teamId)}/image`, options);
};
createCustomEmojiX = (emoji: CustomEmoji, filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('image', fileData, path.basename(filePath));
formData.append('emoji', JSON.stringify(emoji));
const options = this.getFormDataOptions(formData);
return this.doFetch<CustomEmoji>(`${this.getEmojisRoute()}`, options);
};
uploadBrandImageX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('image', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBrandRoute()}/image`, options);
};
uploadPublicSamlCertificateX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('certificate', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBaseRoute()}/saml/certificate/public`, options);
};
uploadPrivateSamlCertificateX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('certificate', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBaseRoute()}/saml/certificate/private`, options);
};
uploadPublicLdapCertificateX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('certificate', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBaseRoute()}/ldap/certificate/public`, options);
};
uploadPrivateLdapCertificateX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('certificate', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBaseRoute()}/ldap/certificate/private`, options);
};
uploadIdpSamlCertificateX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('certificate', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBaseRoute()}/saml/certificate/idp`, options);
};
uploadLicenseX = (filePath: string) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('license', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<License>(`${this.getBaseRoute()}/license`, options);
};
uploadPluginX = async (filePath: string, force = false) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
if (force) {
formData.append('force', 'true');
}
formData.append('plugin', fileData, path.basename(filePath));
const options = this.getFormDataOptions(formData);
return this.doFetch<PluginManifest>(this.getPluginsRoute(), options);
};
// *****************************************************************************
// Boards client
// based on https://github.com/mattermost/focalboard/blob/main/webapp/src/octoClient.ts
// *****************************************************************************
async patchUserConfig(userID: string, patch: UserConfigPatch): Promise<UserPreference[] | undefined> {
const path = `/users/${encodeURIComponent(userID)}/config`;
const options = {
method: 'put',
body: JSON.stringify(patch),
};
return this.doFetch<UserPreference[]>(this.getBoardsRoute() + path, options);
}
}
// Variable to hold cache
const clients: Record<string, ClientCache> = {};
async function makeClient(userRequest?: UserRequest, useCache = true): Promise<ClientCache> {
const client = new Client();
client.setUrl(testConfig.baseURL);
try {
if (!userRequest) {
return {client, user: null};
}
const cacheKey = userRequest.username + userRequest.password;
if (useCache && clients[cacheKey] != null) {
return clients[cacheKey];
}
const userProfile = await client.login(userRequest.username, userRequest.password);
const user = {...userProfile, password: userRequest.password};
const config = await client.getClientConfigOld();
client.setUseBoardsProduct(config.FeatureFlagBoardsProduct === 'true');
if (useCache) {
clients[cacheKey] = {client, user};
}
return {client, user};
} catch (err) {
// log an error for debugging
// eslint-disable-next-line no-console
console.log('makeClient', err);
return {client, user: null};
}
}
// Client types
type UserRequest = {
username: string;
email?: string;
password: string;
};
type ClientCache = {
client: Client;
user: UserProfile | null;
};
// Boards types
interface UserPreference {
user_id: string;
category: string;
name: string;
value: any;
}
interface UserConfigPatch {
updatedFields?: Record<string, string>;
deletedFields?: string[];
}
export {Client, makeClient};

Просмотреть файл

@@ -0,0 +1,709 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import merge from 'deepmerge';
import {
AdminConfig,
ExperimentalSettings,
FeatureFlags,
PasswordSettings,
ServiceSettings,
TeamSettings,
PluginSettings,
ClusterSettings,
CollapsedThreads,
} from '@mattermost/types/config';
import testConfig from '@e2e-test.config';
export function getOnPremServerConfig(): AdminConfig {
return merge<AdminConfig>(defaultServerConfig, onPremServerConfig() as AdminConfig);
}
type TestAdminConfig = {
ClusterSettings: Partial<ClusterSettings>;
ExperimentalSettings: Partial<ExperimentalSettings>;
FeatureFlags: Partial<FeatureFlags>;
PasswordSettings: Partial<PasswordSettings>;
PluginSettings: Partial<PluginSettings>;
ServiceSettings: Partial<ServiceSettings>;
TeamSettings: Partial<TeamSettings>;
};
// On-prem setting that is different from the default
const onPremServerConfig = (): Partial<TestAdminConfig> => {
return {
ClusterSettings: {
Enable: testConfig.haClusterEnabled,
ClusterName: testConfig.haClusterName,
},
ExperimentalSettings: {
EnableAppBar: true,
},
FeatureFlags: {
BoardsProduct: testConfig.boardsProductEnabled,
},
PasswordSettings: {
MinimumLength: 5,
Lowercase: false,
Number: false,
Uppercase: false,
Symbol: false,
},
PluginSettings: {
EnableUploads: true,
Plugins: {
'com.mattermost.calls': {
defaultenabled: true,
},
},
PluginStates: {
focalboard: {
Enable: !testConfig.boardsProductEnabled,
},
},
},
ServiceSettings: {
SiteURL: testConfig.baseURL,
EnableOnboardingFlow: false,
},
TeamSettings: {
EnableOpenServer: true,
},
};
};
// Should be based only from the generated default config from ./server via "make config-reset"
// Based on v7.10 server
const defaultServerConfig: AdminConfig = {
ServiceSettings: {
SiteURL: '',
WebsocketURL: '',
LicenseFileLocation: '',
ListenAddress: ':8065',
ConnectionSecurity: '',
TLSCertFile: '',
TLSKeyFile: '',
TLSMinVer: '1.2',
TLSStrictTransport: false,
TLSStrictTransportMaxAge: 63072000,
TLSOverwriteCiphers: [],
UseLetsEncrypt: false,
LetsEncryptCertificateCacheFile: './config/letsencrypt.cache',
Forward80To443: false,
TrustedProxyIPHeader: [],
ReadTimeout: 300,
WriteTimeout: 300,
IdleTimeout: 60,
MaximumLoginAttempts: 10,
GoroutineHealthThreshold: -1,
EnableOAuthServiceProvider: true,
EnableIncomingWebhooks: true,
EnableOutgoingWebhooks: true,
EnableCommands: true,
EnablePostUsernameOverride: false,
EnablePostIconOverride: false,
GoogleDeveloperKey: '',
EnableLinkPreviews: true,
EnablePermalinkPreviews: true,
RestrictLinkPreviews: '',
EnableTesting: false,
EnableDeveloper: false,
DeveloperFlags: '',
EnableClientPerformanceDebugging: false,
EnableOpenTracing: false,
EnableSecurityFixAlert: true,
EnableInsecureOutgoingConnections: false,
AllowedUntrustedInternalConnections: '',
EnableMultifactorAuthentication: false,
EnforceMultifactorAuthentication: false,
EnableUserAccessTokens: false,
AllowCorsFrom: '',
CorsExposedHeaders: '',
CorsAllowCredentials: false,
CorsDebug: false,
AllowCookiesForSubdomains: false,
ExtendSessionLengthWithActivity: true,
SessionLengthWebInDays: 30,
SessionLengthWebInHours: 720,
SessionLengthMobileInDays: 30,
SessionLengthMobileInHours: 720,
SessionLengthSSOInDays: 30,
SessionLengthSSOInHours: 720,
SessionCacheInMinutes: 10,
SessionIdleTimeoutInMinutes: 43200,
WebsocketSecurePort: 443,
WebsocketPort: 80,
WebserverMode: 'gzip',
EnableGifPicker: true,
GfycatAPIKey: '2_KtH_W5',
GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof',
EnableCustomEmoji: true,
EnableEmojiPicker: true,
PostEditTimeLimit: -1,
TimeBetweenUserTypingUpdatesMilliseconds: 5000,
EnablePostSearch: true,
EnableFileSearch: true,
MinimumHashtagLength: 3,
EnableUserTypingMessages: true,
EnableChannelViewedMessages: true,
EnableUserStatuses: true,
ExperimentalEnableAuthenticationTransfer: true,
ClusterLogTimeoutMilliseconds: 2000,
EnablePreviewFeatures: true,
EnableTutorial: true,
EnableOnboardingFlow: true,
ExperimentalEnableDefaultChannelLeaveJoinMessages: true,
ExperimentalGroupUnreadChannels: 'disabled',
EnableAPITeamDeletion: false,
EnableAPITriggerAdminNotifications: false,
EnableAPIUserDeletion: false,
ExperimentalEnableHardenedMode: false,
ExperimentalStrictCSRFEnforcement: false,
EnableEmailInvitations: false,
DisableBotsWhenOwnerIsDeactivated: true,
EnableBotAccountCreation: false,
EnableSVGs: false,
EnableLatex: false,
EnableInlineLatex: true,
PostPriority: true,
EnableAPIChannelDeletion: false,
EnableLocalMode: false,
LocalModeSocketLocation: '/var/tmp/mattermost_local.socket',
EnableAWSMetering: false,
SplitKey: '',
FeatureFlagSyncIntervalSeconds: 30,
DebugSplit: false,
ThreadAutoFollow: true,
CollapsedThreads: CollapsedThreads.ALWAYS_ON,
ManagedResourcePaths: '',
EnableCustomGroups: true,
SelfHostedPurchase: true,
AllowSyncedDrafts: true,
SelfHostedExpansion: false,
},
TeamSettings: {
SiteName: 'Mattermost',
MaxUsersPerTeam: 50,
EnableUserCreation: true,
EnableOpenServer: false,
EnableUserDeactivation: false,
RestrictCreationToDomains: '',
EnableCustomUserStatuses: true,
EnableCustomBrand: false,
CustomBrandText: '',
CustomDescriptionText: '',
RestrictDirectMessage: 'any',
EnableLastActiveTime: true,
UserStatusAwayTimeout: 300,
MaxChannelsPerTeam: 2000,
MaxNotificationsPerChannel: 1000,
EnableConfirmNotificationsToChannel: true,
TeammateNameDisplay: 'username',
ExperimentalViewArchivedChannels: true,
ExperimentalEnableAutomaticReplies: false,
LockTeammateNameDisplay: false,
ExperimentalPrimaryTeam: '',
ExperimentalDefaultChannels: [],
},
ClientRequirements: {
AndroidLatestVersion: '',
AndroidMinVersion: '',
IosLatestVersion: '',
IosMinVersion: '',
},
SqlSettings: {
DriverName: 'postgres',
DataSource:
'postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes',
DataSourceReplicas: [],
DataSourceSearchReplicas: [],
MaxIdleConns: 20,
ConnMaxLifetimeMilliseconds: 3600000,
ConnMaxIdleTimeMilliseconds: 300000,
MaxOpenConns: 300,
Trace: false,
AtRestEncryptKey: '',
QueryTimeout: 30,
DisableDatabaseSearch: false,
MigrationsStatementTimeoutSeconds: 100000,
ReplicaLagSettings: [],
},
LogSettings: {
EnableConsole: true,
ConsoleLevel: 'DEBUG',
ConsoleJson: true,
EnableColor: false,
EnableFile: true,
FileLevel: 'INFO',
FileJson: true,
FileLocation: '',
EnableWebhookDebugging: true,
EnableDiagnostics: true,
VerboseDiagnostics: false,
EnableSentry: true,
AdvancedLoggingConfig: '',
},
ExperimentalAuditSettings: {
FileEnabled: false,
FileName: '',
FileMaxSizeMB: 100,
FileMaxAgeDays: 0,
FileMaxBackups: 0,
FileCompress: false,
FileMaxQueueSize: 1000,
AdvancedLoggingConfig: '',
},
NotificationLogSettings: {
EnableConsole: true,
ConsoleLevel: 'DEBUG',
ConsoleJson: true,
EnableColor: false,
EnableFile: true,
FileLevel: 'INFO',
FileJson: true,
FileLocation: '',
AdvancedLoggingConfig: '',
},
PasswordSettings: {
MinimumLength: 8,
Lowercase: false,
Number: false,
Uppercase: false,
Symbol: false,
},
FileSettings: {
EnableFileAttachments: true,
EnableMobileUpload: true,
EnableMobileDownload: true,
MaxFileSize: 104857600,
MaxImageResolution: 33177600,
MaxImageDecoderConcurrency: -1,
DriverName: 'local',
Directory: './data/',
EnablePublicLink: false,
ExtractContent: true,
ArchiveRecursion: false,
PublicLinkSalt: '',
InitialFont: 'nunito-bold.ttf',
AmazonS3AccessKeyId: '',
AmazonS3SecretAccessKey: '',
AmazonS3Bucket: '',
AmazonS3PathPrefix: '',
AmazonS3Region: '',
AmazonS3Endpoint: 's3.amazonaws.com',
AmazonS3SSL: true,
AmazonS3SignV2: false,
AmazonS3SSE: false,
AmazonS3Trace: false,
AmazonS3RequestTimeoutMilliseconds: 30000,
},
EmailSettings: {
EnableSignUpWithEmail: true,
EnableSignInWithEmail: true,
EnableSignInWithUsername: true,
SendEmailNotifications: true,
UseChannelInEmailNotifications: false,
RequireEmailVerification: false,
FeedbackName: '',
FeedbackEmail: 'test@example.com',
ReplyToAddress: 'test@example.com',
FeedbackOrganization: '',
EnableSMTPAuth: false,
SMTPUsername: '',
SMTPPassword: '',
SMTPServer: 'localhost',
SMTPPort: '10025',
SMTPServerTimeout: 10,
ConnectionSecurity: '',
SendPushNotifications: true,
PushNotificationServer: 'https://push-test.mattermost.com',
PushNotificationContents: 'full',
PushNotificationBuffer: 1000,
EnableEmailBatching: false,
EmailBatchingBufferSize: 256,
EmailBatchingInterval: 30,
EnablePreviewModeBanner: true,
SkipServerCertificateVerification: false,
EmailNotificationContentsType: 'full',
LoginButtonColor: '#0000',
LoginButtonBorderColor: '#2389D7',
LoginButtonTextColor: '#2389D7',
EnableInactivityEmail: true,
},
RateLimitSettings: {
Enable: false,
PerSec: 10,
MaxBurst: 100,
MemoryStoreSize: 10000,
VaryByRemoteAddr: true,
VaryByUser: false,
VaryByHeader: '',
},
PrivacySettings: {
ShowEmailAddress: true,
ShowFullName: true,
},
SupportSettings: {
TermsOfServiceLink: 'https://mattermost.com/terms-of-use/',
PrivacyPolicyLink: 'https://mattermost.com/privacy-policy/',
AboutLink: 'https://docs.mattermost.com/about/product.html/',
HelpLink: 'https://mattermost.com/default-help/',
ReportAProblemLink: 'https://mattermost.com/default-report-a-problem/',
SupportEmail: '',
CustomTermsOfServiceEnabled: false,
CustomTermsOfServiceReAcceptancePeriod: 365,
EnableAskCommunityLink: true,
},
AnnouncementSettings: {
EnableBanner: false,
BannerText: '',
BannerColor: '#f2a93b',
BannerTextColor: '#333333',
AllowBannerDismissal: true,
AdminNoticesEnabled: true,
UserNoticesEnabled: true,
NoticesURL: 'https://notices.mattermost.com/',
NoticesFetchFrequency: 3600,
NoticesSkipCache: false,
},
ThemeSettings: {
EnableThemeSelection: true,
DefaultTheme: 'default',
AllowCustomThemes: true,
AllowedThemes: [],
},
GitLabSettings: {
Enable: false,
Secret: '',
Id: '',
Scope: '',
AuthEndpoint: '',
TokenEndpoint: '',
UserAPIEndpoint: '',
DiscoveryEndpoint: '',
ButtonText: '',
ButtonColor: '',
},
GoogleSettings: {
Enable: false,
Secret: '',
Id: '',
Scope: 'profile email',
AuthEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
TokenEndpoint: 'https://www.googleapis.com/oauth2/v4/token',
UserAPIEndpoint:
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata',
DiscoveryEndpoint: '',
ButtonText: '',
ButtonColor: '',
},
Office365Settings: {
Enable: false,
Secret: '',
Id: '',
Scope: 'User.Read',
AuthEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
TokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
UserAPIEndpoint: 'https://graph.microsoft.com/v1.0/me',
DiscoveryEndpoint: '',
DirectoryId: '',
},
OpenIdSettings: {
Enable: false,
Secret: '',
Id: '',
Scope: 'profile openid email',
AuthEndpoint: '',
TokenEndpoint: '',
UserAPIEndpoint: '',
DiscoveryEndpoint: '',
ButtonText: '',
ButtonColor: '#145DBF',
},
LdapSettings: {
Enable: false,
EnableSync: false,
LdapServer: '',
LdapPort: 389,
ConnectionSecurity: '',
BaseDN: '',
BindUsername: '',
BindPassword: '',
UserFilter: '',
GroupFilter: '',
GuestFilter: '',
EnableAdminFilter: false,
AdminFilter: '',
GroupDisplayNameAttribute: '',
GroupIdAttribute: '',
FirstNameAttribute: '',
LastNameAttribute: '',
EmailAttribute: '',
UsernameAttribute: '',
NicknameAttribute: '',
IdAttribute: '',
PositionAttribute: '',
LoginIdAttribute: '',
PictureAttribute: '',
SyncIntervalMinutes: 60,
SkipCertificateVerification: false,
PublicCertificateFile: '',
PrivateKeyFile: '',
QueryTimeout: 60,
MaxPageSize: 0,
LoginFieldName: '',
LoginButtonColor: '#0000',
LoginButtonBorderColor: '#2389D7',
LoginButtonTextColor: '#2389D7',
Trace: false,
},
ComplianceSettings: {
Enable: false,
Directory: './data/',
EnableDaily: false,
BatchSize: 30000,
},
LocalizationSettings: {
DefaultServerLocale: 'en',
DefaultClientLocale: 'en',
AvailableLocales: '',
},
SamlSettings: {
Enable: false,
EnableSyncWithLdap: false,
EnableSyncWithLdapIncludeAuth: false,
IgnoreGuestsLdapSync: false,
Verify: true,
Encrypt: true,
SignRequest: false,
IdpURL: '',
IdpDescriptorURL: '',
IdpMetadataURL: '',
ServiceProviderIdentifier: '',
AssertionConsumerServiceURL: '',
SignatureAlgorithm: 'RSAwithSHA1',
CanonicalAlgorithm: 'Canonical1.0',
ScopingIDPProviderId: '',
ScopingIDPName: '',
IdpCertificateFile: '',
PublicCertificateFile: '',
PrivateKeyFile: '',
IdAttribute: '',
GuestAttribute: '',
EnableAdminAttribute: false,
AdminAttribute: '',
FirstNameAttribute: '',
LastNameAttribute: '',
EmailAttribute: '',
UsernameAttribute: '',
NicknameAttribute: '',
LocaleAttribute: '',
PositionAttribute: '',
LoginButtonText: 'SAML',
LoginButtonColor: '#34a28b',
LoginButtonBorderColor: '#2389D7',
LoginButtonTextColor: '#ffffff',
},
NativeAppSettings: {
AppCustomURLSchemes: ['mmauth://', 'mmauthbeta://'],
AppDownloadLink: 'https://mattermost.com/download/#mattermostApps',
AndroidAppDownloadLink: 'https://mattermost.com/mattermost-android-app/',
IosAppDownloadLink: 'https://mattermost.com/mattermost-ios-app/',
},
ClusterSettings: {
Enable: false,
ClusterName: '',
OverrideHostname: '',
NetworkInterface: '',
BindAddress: '',
AdvertiseAddress: '',
UseIPAddress: true,
EnableGossipCompression: true,
EnableExperimentalGossipEncryption: false,
ReadOnlyConfig: true,
GossipPort: 8074,
StreamingPort: 8075,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 128,
IdleConnTimeoutMilliseconds: 90000,
},
MetricsSettings: {
Enable: false,
BlockProfileRate: 0,
ListenAddress: ':8067',
},
ExperimentalSettings: {
ClientSideCertEnable: false,
ClientSideCertCheck: 'secondary',
LinkMetadataTimeoutMilliseconds: 5000,
RestrictSystemAdmin: false,
UseNewSAMLLibrary: false,
EnableSharedChannels: false,
EnableRemoteClusterService: false,
EnableAppBar: false,
PatchPluginsReactDOM: false,
},
AnalyticsSettings: {
MaxUsersForStatistics: 2500,
},
ElasticsearchSettings: {
ConnectionURL: 'http://localhost:9200',
Username: 'elastic',
Password: 'changeme',
EnableIndexing: false,
EnableSearching: false,
EnableAutocomplete: false,
Sniff: true,
PostIndexReplicas: 1,
PostIndexShards: 1,
ChannelIndexReplicas: 1,
ChannelIndexShards: 1,
UserIndexReplicas: 1,
UserIndexShards: 1,
AggregatePostsAfterDays: 365,
PostsAggregatorJobStartTime: '03:00',
IndexPrefix: '',
LiveIndexingBatchSize: 1,
BatchSize: 10000,
RequestTimeoutSeconds: 30,
SkipTLSVerification: false,
CA: '',
ClientCert: '',
ClientKey: '',
Trace: '',
},
BleveSettings: {
IndexDir: '',
EnableIndexing: false,
EnableSearching: false,
EnableAutocomplete: false,
BatchSize: 10000,
},
DataRetentionSettings: {
EnableMessageDeletion: false,
EnableFileDeletion: false,
EnableBoardsDeletion: false,
MessageRetentionDays: 365,
FileRetentionDays: 365,
BoardsRetentionDays: 365,
DeletionJobStartTime: '02:00',
BatchSize: 3000,
},
MessageExportSettings: {
EnableExport: false,
ExportFormat: 'actiance',
DailyRunTime: '01:00',
ExportFromTimestamp: 0,
BatchSize: 10000,
DownloadExportResults: false,
GlobalRelaySettings: {
CustomerType: 'A9',
SMTPUsername: '',
SMTPPassword: '',
EmailAddress: '',
SMTPServerTimeout: 1800,
},
},
JobSettings: {
RunJobs: true,
RunScheduler: true,
CleanupJobsThresholdDays: -1,
CleanupConfigThresholdDays: -1,
},
ProductSettings: {
EnablePublicSharedBoards: false,
},
PluginSettings: {
Enable: true,
EnableUploads: false,
AllowInsecureDownloadURL: false,
EnableHealthCheck: true,
Directory: './plugins',
ClientDirectory: './client/plugins',
Plugins: {},
PluginStates: {
'com.mattermost.apps': {
Enable: true,
},
'com.mattermost.calls': {
Enable: true,
},
'com.mattermost.nps': {
Enable: true,
},
focalboard: {
Enable: true,
},
playbooks: {
Enable: true,
},
},
EnableMarketplace: true,
EnableRemoteMarketplace: true,
AutomaticPrepackagedPlugins: true,
RequirePluginSignature: false,
MarketplaceURL: 'https://api.integrations.mattermost.com',
SignaturePublicKeyFiles: [],
ChimeraOAuthProxyURL: '',
},
DisplaySettings: {
CustomURLSchemes: [],
ExperimentalTimezone: true,
},
GuestAccountsSettings: {
Enable: false,
AllowEmailAccounts: true,
EnforceMultifactorAuthentication: false,
RestrictCreationToDomains: '',
},
ImageProxySettings: {
Enable: false,
ImageProxyType: 'local',
RemoteImageProxyURL: '',
RemoteImageProxyOptions: '',
},
CloudSettings: {
CWSURL: 'https://customers.mattermost.com',
CWSAPIURL: 'https://portal.internal.prod.cloud.mattermost.com',
},
FeatureFlags: {
TestFeature: 'off',
TestBoolFeature: false,
EnableRemoteClusterService: false,
AppsEnabled: true,
PluginPlaybooks: '',
PluginApps: '',
PluginFocalboard: '',
PluginCalls: '',
PermalinkPreviews: true,
CallsEnabled: true,
BoardsFeatureFlags: '',
BoardsDataRetention: false,
NormalizeLdapDNs: false,
EnableInactivityCheckJob: true,
UseCaseOnboarding: true,
GraphQL: false,
InsightsEnabled: true,
CommandPalette: false,
BoardsProduct: false,
SendWelcomePost: true,
WorkTemplate: false,
PostPriority: true,
WysiwygEditor: false,
PeopleProduct: false,
ReduceOnBoardingTaskList: false,
OnboardingAutoShowLinkedBoard: true,
ThreadsEverywhere: false,
GlobalDrafts: true,
OnboardingTourTips: true,
},
ImportSettings: {
Directory: './import',
RetentionDays: 30,
},
ExportSettings: {
Directory: './export',
RetentionDays: 30,
},
};

Просмотреть файл

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {Client, makeClient} from './client';
export {createRandomChannel} from './channel';
export {getOnPremServerConfig} from './default_config';
export {initSetup, getAdminClient} from './init';
export {createRandomTeam} from './team';
export {createRandomUser, getDefaultAdminUser} from './user';

100
e2e-tests/playwright/support/server/init.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'node:path';
import {expect} from '@playwright/test';
import {PreferenceType} from '@mattermost/types/preferences';
import testConfig from '@e2e-test.config';
import {makeClient} from '.';
import {getOnPremServerConfig} from './default_config';
import {createRandomTeam} from './team';
import {createRandomUser} from './user';
const boardsUserConfigPatch = {
updatedFields: {
welcomePageViewed: '1',
onboardingTourStep: '999',
tourCategory: 'board',
version72MessageCanceled: 'true',
},
};
export async function initSetup({
userPrefix = 'user',
teamPrefix = {name: 'team', displayName: 'Team'},
withDefaultProfileImage = true,
skipBoardsUserConfig = true,
} = {}) {
try {
// Login the admin user via API
const {adminClient, adminUser} = await getAdminClient();
if (!adminClient) {
throw new Error(
"Failed to setup admin: Check that you're able to access the server using the same admin credential."
);
}
// Reset server config
const adminConfig = await adminClient.updateConfig(getOnPremServerConfig());
// Create new team
const team = await adminClient.createTeam(createRandomTeam(teamPrefix.name, teamPrefix.displayName));
// Create new user and add to newly created team
const randomUser = createRandomUser(userPrefix);
const user = await adminClient.createUser(randomUser, '', '');
user.password = randomUser.password;
await adminClient.addToTeam(team.id, user.id);
// Log in new user via API
const {client: userClient} = await makeClient(user);
if (withDefaultProfileImage) {
// Set user profile image
const fullPath = path.join(path.resolve(__dirname), '../', 'asset/mattermost-icon_128x128.png');
await userClient.uploadProfileImageX(user.id, fullPath);
}
// Update user preference
const preferences: PreferenceType[] = [
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
];
await userClient.savePreferences(user.id, preferences);
if (skipBoardsUserConfig) {
await userClient.patchUserConfig(user.id, boardsUserConfigPatch);
}
return {
adminClient,
adminUser,
adminConfig,
user,
userClient,
team,
offTopicUrl: getUrl(team.name, 'off-topic'),
townSquareUrl: getUrl(team.name, 'town-square'),
};
} catch (err) {
// log an error for debugging
// eslint-disable-next-line no-console
console.log(err);
expect(err, 'Should not throw an error').toBeFalsy();
throw err;
}
}
export async function getAdminClient() {
const {client: adminClient, user: adminUser} = await makeClient({
username: testConfig.adminUsername,
password: testConfig.adminPassword,
});
return {adminClient, adminUser};
}
function getUrl(teamName: string, channelName: string) {
return `/${teamName}/channels/${channelName}`;
}

17
e2e-tests/playwright/support/server/team.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Team, TeamType} from '@mattermost/types/teams';
import {getRandomId} from '@e2e-support/util';
export function createRandomTeam(name = 'team', displayName = 'Team', type: TeamType = 'O', unique = true): Team {
const randomSuffix = getRandomId();
const team = {
name: unique ? `${name}-${randomSuffix}` : name,
display_name: unique ? `${displayName} ${randomSuffix}` : displayName,
type,
};
return team as Team;
}

33
e2e-tests/playwright/support/server/user.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserProfile} from '@mattermost/types/users';
import {getRandomId} from '@e2e-support/util';
import testConfig from '@e2e-test.config';
export function createRandomUser(prefix = 'user') {
const randomId = getRandomId();
const user = {
email: `${prefix}${randomId}@sample.mattermost.com`,
username: `${prefix}${randomId}`,
password: 'passwd',
first_name: `First${randomId}`,
last_name: `Last${randomId}`,
nickname: `Nickname${randomId}`,
};
return user as UserProfile;
}
export function getDefaultAdminUser() {
const admin = {
username: testConfig.adminUsername,
password: testConfig.adminPassword,
first_name: 'Kenneth',
last_name: 'Moreno',
email: testConfig.adminEmail,
};
return admin as UserProfile;
}

21
e2e-tests/playwright/support/test_action.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator, Page} from '@playwright/test';
export {waitUntil} from 'async-wait-until';
const visibilityHidden = 'visibility: hidden !important;';
const hideTeamHeader = `.test-team-header {${visibilityHidden}} `;
const hidePostHeaderTime = `.post__time {${visibilityHidden}} `;
const hidePostProfileIcon = `.profile-icon {${visibilityHidden}} `;
export async function hideDynamicChannelsContent(page: Page) {
await page.addStyleTag({content: hideTeamHeader + hidePostHeaderTime + hidePostProfileIcon});
}
export async function waitForAnimationEnd(locator: Locator) {
return locator.evaluate((element) =>
Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished))
);
}

Просмотреть файл

@@ -0,0 +1,86 @@
import {test as base, Browser} from '@playwright/test';
import {TestBrowser} from './browser_context';
import {
shouldHaveBoardsEnabled,
shouldHaveCallsEnabled,
shouldHaveFeatureFlag,
shouldSkipInSmallScreen,
shouldRunInLinux,
} from './flag';
import {initSetup, getAdminClient} from './server';
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
import {pages} from './ui/pages';
import {matchSnapshot} from './visual';
export {expect} from '@playwright/test';
type ExtendedFixtures = {
pw: PlaywrightExtended;
pages: typeof pages;
};
export const test = base.extend<ExtendedFixtures>({
pw: async ({browser}, use) => {
const pw = new PlaywrightExtended(browser);
await use(pw);
},
// eslint-disable-next-line no-empty-pattern
pages: async ({}, use) => {
await use(pages);
},
});
class PlaywrightExtended {
// ./browser_context
readonly testBrowser: TestBrowser;
// ./flag
readonly shouldHaveBoardsEnabled;
readonly shouldHaveCallsEnabled;
readonly shouldHaveFeatureFlag;
readonly shouldSkipInSmallScreen;
readonly shouldRunInLinux;
// ./server
readonly getAdminClient;
readonly initSetup;
// ./test_action
readonly hideDynamicChannelsContent;
readonly waitForAnimationEnd;
readonly waitUntil;
// ./ui/pages
readonly pages;
// ./visual
readonly matchSnapshot;
constructor(browser: Browser) {
// ./browser_context
this.testBrowser = new TestBrowser(browser);
// ./flag
this.shouldHaveBoardsEnabled = shouldHaveBoardsEnabled;
this.shouldHaveCallsEnabled = shouldHaveCallsEnabled;
this.shouldHaveFeatureFlag = shouldHaveFeatureFlag;
this.shouldSkipInSmallScreen = shouldSkipInSmallScreen;
this.shouldRunInLinux = shouldRunInLinux;
// ./server
this.initSetup = initSetup;
this.getAdminClient = getAdminClient;
// ./test_action
this.hideDynamicChannelsContent = hideDynamicChannelsContent;
this.waitForAnimationEnd = waitForAnimationEnd;
this.waitUntil = waitUntil;
// ./ui/pages
this.pages = pages;
// ./visual
this.matchSnapshot = matchSnapshot;
}
}

Просмотреть файл

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class BoardsCreateModal {
readonly container: Locator;
readonly productSwitchMenu;
constructor(container: Locator) {
this.container = container;
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
}
async switchProduct(name: string) {
await this.productSwitchMenu.click();
await this.container.getByRole('link', {name: `${name}`}).click();
}
async toBeVisible(name: string) {
await expect(this.container.getByRole('heading', {name})).toBeVisible();
}
}
export {BoardsCreateModal};

Просмотреть файл

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator} from '@playwright/test';
export default class BoardsSidebar {
readonly container: Locator;
readonly plusButton;
readonly createNewBoardMenuItem;
readonly createNewCategoryMenuItem;
readonly titles;
constructor(container: Locator) {
this.container = container;
this.plusButton = container.locator('.add-board-icon');
this.createNewBoardMenuItem = container.getByRole('button', {name: 'Create new board'});
this.createNewCategoryMenuItem = container.getByRole('button', {name: 'Create New Category'});
this.titles = container.locator('.SidebarBoardItem > .octo-sidebar-title');
}
async waitForTitle(name: string) {
await this.container.getByRole('button', {name: `${name}`}).waitFor({state: 'visible'});
}
}
export {BoardsSidebar};

Просмотреть файл

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class ChannelsAppBar {
readonly container: Locator;
readonly playbooksIcon;
constructor(container: Locator) {
this.container = container;
this.playbooksIcon = container.locator('#app-bar-icon-playbooks').getByRole('img');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}
export {ChannelsAppBar};

Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class ChannelsHeader {
readonly container: Locator;
constructor(container: Locator) {
this.container = container;
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}
export {ChannelsHeader};

Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class ChannelsPost {
readonly container: Locator;
readonly body;
readonly profileIcon;
readonly replyButton;
constructor(container: Locator) {
this.container = container;
this.body = container.locator('.post__body');
this.profileIcon = container.locator('.profile-icon');
this.replyButton = container.getByRole('button', {name: 'reply'});
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async getId() {
const id = await this.container.getAttribute('id');
expect(id, 'No post ID found.').toBeTruthy();
return (id || '').substring('post_'.length);
}
async getProfileImage(username: string) {
return await this.profileIcon.getByAltText(`${username} profile image`);
}
async openRHS() {
await this.container.hover();
await this.replyButton.waitFor();
await this.replyButton.click();
}
}
export {ChannelsPost};

Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class ChannelsPostCreate {
readonly container: Locator;
readonly input;
readonly attachmentButton;
readonly emojiButton;
constructor(container: Locator) {
this.container = container;
this.input = container.getByTestId('post_textbox');
this.attachmentButton = container.getByLabel('attachment');
this.emojiButton = container.getByLabel('select an emoji');
}
async postMessage(message: string) {
await this.input.fill(message);
}
async toBeVisible() {
await expect(this.container).toBeVisible();
await expect(this.input).toBeVisible();
}
}
export {ChannelsPostCreate};

Просмотреть файл

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class ChannelsSidebarRight {
readonly container: Locator;
constructor(container: Locator) {
this.container = container;
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}
export {ChannelsSidebarRight};

Просмотреть файл

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class GlobalHeader {
readonly container: Locator;
readonly productSwitchMenu;
constructor(container: Locator) {
this.container = container;
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
}
async switchProduct(name: string) {
await this.productSwitchMenu.click();
await this.container.getByRole('link', {name: `${name}`}).click();
}
async toBeVisible(name: string) {
await expect(this.container.getByRole('heading', {name})).toBeVisible();
}
}
export {GlobalHeader};

Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BoardsSidebar} from './boards/sidebar';
import {ChannelsHeader} from './channels/header';
import {ChannelsAppBar} from './channels/app_bar';
import {ChannelsPostCreate} from './channels/post_create';
import {ChannelsPost} from './channels/post';
import {ChannelsSidebarRight} from './channels/sidebar_right';
import {GlobalHeader} from './global_header';
const components = {
BoardsSidebar,
ChannelsAppBar,
ChannelsHeader,
ChannelsPostCreate,
ChannelsPost,
ChannelsSidebarRight,
GlobalHeader,
};
export {
components,
BoardsSidebar,
ChannelsAppBar,
ChannelsHeader,
ChannelsPostCreate,
ChannelsPost,
ChannelsSidebarRight,
GlobalHeader,
};

Просмотреть файл

@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {components} from '@e2e-support/ui/components';
export default class BoardsCreatePage {
readonly boards = 'Boards';
readonly page: Page;
readonly globalHeader;
readonly createBoardHeading;
readonly createEmptyBoardButton;
readonly useTemplateButton;
constructor(page: Page) {
this.page = page;
this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header'));
this.createBoardHeading = page.getByRole('heading', {name: 'Create a board'});
this.createEmptyBoardButton = page.getByRole('button', {name: ' Create an empty board'});
this.useTemplateButton = page.getByRole('button', {name: 'Use this template'});
}
async goto(teamId = '') {
let boardsUrl = '/boards';
if (teamId) {
boardsUrl += `/team/${teamId}`;
}
await this.page.goto(boardsUrl);
}
async toBeVisible() {
await this.globalHeader.toBeVisible(this.boards);
await expect(this.createEmptyBoardButton).toBeVisible();
await expect(this.useTemplateButton).toBeVisible();
await expect(this.createBoardHeading).toBeVisible();
}
async createEmptyBoard() {
await this.createEmptyBoardButton.click();
}
}
export {BoardsCreatePage};

Просмотреть файл

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {components} from '@e2e-support/ui/components';
export default class BoardsViewPage {
readonly boards = 'Boards';
readonly page: Page;
readonly sidebar;
readonly globalHeader;
readonly topHead;
readonly editableTitle;
readonly shareButton;
constructor(page: Page) {
this.page = page;
this.sidebar = new components.BoardsSidebar(page.locator('.octo-sidebar'));
this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header'));
this.topHead = page.locator('.top-head');
this.editableTitle = this.topHead.getByPlaceholder('Untitled board');
this.shareButton = page.getByRole('button', {name: '󰍁 Share'});
}
async goto(teamId = '', boardId = '', viewId = '', cardId = '') {
let boardsUrl = '/boards';
if (teamId) {
boardsUrl += `/team/${teamId}`;
if (boardId) {
boardsUrl += `/${boardId}`;
if (viewId) {
boardsUrl += `/${viewId}`;
if (cardId) {
boardsUrl += `/${cardId}`;
}
}
}
}
await this.page.goto(boardsUrl);
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await this.globalHeader.toBeVisible(this.boards);
await expect(this.shareButton).toBeVisible();
await expect(this.topHead).toBeVisible();
}
async shouldHaveUntitledBoard() {
await this.editableTitle.isVisible();
expect(await this.editableTitle.getAttribute('value')).toBe('');
await expect(this.page.getByTitle('(Untitled Board)')).toBeVisible();
}
}
export {BoardsViewPage};

Просмотреть файл

@@ -0,0 +1,106 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Page} from '@playwright/test';
import {waitUntil} from '@e2e-support/test_action';
import {components} from '@e2e-support/ui/components';
import {duration, isSmallScreen} from '@e2e-support/util';
export default class ChannelsPage {
readonly channels = 'Channels';
readonly page: Page;
readonly postCreate;
readonly globalHeader;
readonly header;
readonly appBar;
readonly sidebarRight;
constructor(page: Page) {
this.page = page;
this.postCreate = new components.ChannelsPostCreate(page.locator('#post-create'));
this.globalHeader = new components.GlobalHeader(page.locator('#global-header'));
this.header = new components.ChannelsHeader(page.locator('.channel-header'));
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
}
async goto(teamName = '', channelName = '') {
let channelsUrl = '/';
if (teamName) {
channelsUrl += `${teamName}`;
if (channelName) {
channelsUrl += `/${channelName}`;
}
}
await this.page.goto(channelsUrl);
}
async toBeVisible() {
if (!isSmallScreen(this.page.viewportSize())) {
await this.globalHeader.toBeVisible(this.channels);
}
await this.postCreate.toBeVisible();
}
async postMessage(message: string) {
await this.postCreate.input.waitFor();
await this.postCreate.postMessage(message);
}
async getFirstPost() {
await this.page.getByTestId('postView').first().waitFor();
const post = await this.page.getByTestId('postView').first();
return new components.ChannelsPost(post);
}
async getLastPost() {
await this.page.getByTestId('postView').last().waitFor();
const post = await this.page.getByTestId('postView').last();
return new components.ChannelsPost(post);
}
async getNthPost(index: number) {
await this.page.getByTestId('postView').nth(index).waitFor();
const post = await this.page.getByTestId('postView').nth(index);
return new components.ChannelsPost(post);
}
async getPostById(id: string) {
await this.page.locator(`[id="post_${id}"]`).waitFor();
const post = await this.page.locator(`[id="post_${id}"]`);
return new components.ChannelsPost(post);
}
async getRHSPostById(id: string) {
await this.page.locator(`[id="rhsPost_${id}"]`).waitFor();
const post = await this.page.locator(`[id="rhsPost_${id}"]`);
return new components.ChannelsPost(post);
}
async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getLastPost();
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout}
);
}
async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getPostById(id);
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout}
);
}
}
export {ChannelsPage};

Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BoardsCreatePage} from './boards_create';
import {BoardsViewPage} from './boards_view';
import {ChannelsPage} from './channels';
import {LandingLoginPage} from './landing_login';
import {LoginPage} from './login';
import {SignupPage} from './signup';
const pages = {
BoardsCreatePage,
BoardsViewPage,
ChannelsPage,
LandingLoginPage,
LoginPage,
SignupPage,
};
export {pages, BoardsCreatePage, BoardsViewPage, ChannelsPage, LandingLoginPage, LoginPage, SignupPage};

Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
export default class LandingLoginPage {
readonly page: Page;
readonly isMobile?: boolean;
readonly viewInAppButton;
readonly viewInDesktopAppButton;
readonly viewInBrowserButton;
constructor(page: Page, isMobile?: boolean) {
this.page = page;
this.isMobile = isMobile;
this.viewInAppButton = page.locator('text=View in App');
this.viewInDesktopAppButton = page.locator('text=View in Desktop App');
this.viewInBrowserButton = page.locator('text=View in Browser');
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
if (this.isMobile) {
await expect(this.viewInAppButton).toBeVisible();
} else {
await expect(this.viewInDesktopAppButton).toBeVisible();
}
await expect(this.viewInBrowserButton).toBeVisible();
}
async goto() {
await this.page.goto('/landing#/login');
}
}
export {LandingLoginPage};

Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {AdminConfig} from '@mattermost/types/config';
import {UserProfile} from '@mattermost/types/users';
export default class LoginPage {
readonly adminConfig: AdminConfig;
readonly page: Page;
readonly title;
readonly subtitle;
readonly bodyCard;
readonly loginInput;
readonly loginPlaceholder;
readonly passwordInput;
readonly signInButton;
readonly createAccountLink;
readonly forgotPasswordLink;
readonly userErrorLabel;
readonly fieldWithError;
readonly formContainer;
constructor(page: Page, adminConfig: AdminConfig) {
this.page = page;
this.adminConfig = adminConfig;
const loginInputPlaceholder = adminConfig.LdapSettings.Enable
? 'Email, Username or AD/LDAP Username'
: 'Email or Username';
this.title = page.locator('h1:has-text("Log in to your account")');
this.subtitle = page.locator('text=Collaborate with your team in real-time');
this.bodyCard = page.locator('.login-body-card');
this.loginInput = page.locator('#input_loginId');
this.loginPlaceholder = page.locator(`[placeholder="${loginInputPlaceholder}"]`);
this.passwordInput = page.locator('#input_password-input');
this.signInButton = page.locator('button:has-text("Log in")');
this.createAccountLink = page.locator("text=Don't have an account?");
this.forgotPasswordLink = page.locator('text=Forgot your password?');
this.userErrorLabel = page.locator('text=Please enter your email or username');
this.fieldWithError = page.locator('.with-error');
this.formContainer = page.locator('.signup-team__container');
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await expect(this.title).toBeVisible();
await expect(this.loginInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
}
async goto() {
await this.page.goto('/login');
}
async login(user: UserProfile, useUsername = true) {
await this.loginInput.fill(useUsername ? user.username : user.email);
await this.passwordInput.fill(user.password);
await Promise.all([this.page.waitForNavigation(), this.signInButton.click()]);
}
}
export {LoginPage};

Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {duration, wait} from '@e2e-support/util';
export default class SignupPage {
readonly page: Page;
readonly title;
readonly subtitle;
readonly bodyCard;
readonly emailInput;
readonly usernameInput;
readonly passwordInput;
readonly createAccountButton;
readonly loginLink;
readonly emailError;
readonly usernameError;
readonly passwordError;
constructor(page: Page) {
this.page = page;
this.title = page.locator('h1:has-text("Lets get started")');
this.subtitle = page.locator('text=Create your Mattermost account to start collaborating with your team');
this.bodyCard = page.locator('.signup-body-card');
this.emailInput = page.locator('#input_email');
this.usernameInput = page.locator('#input_name');
this.passwordInput = page.locator('#input_password-input');
this.createAccountButton = page.locator('button:has-text("Create Account")');
this.loginLink = page.locator('text=Click here to sign in.');
this.emailError = page.locator('text=Please enter a valid email address');
this.usernameError = page.locator(
'text=Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.'
);
this.passwordError = page.locator('text=Must be 5-64 characters long.');
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await this.page.waitForLoadState('domcontentloaded');
await wait(duration.half_sec);
await expect(this.title).toBeVisible();
await expect(this.emailInput).toBeVisible();
await expect(this.usernameInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
}
async goto() {
await this.page.goto('/signup_email');
}
async create(user: {email: string; username: string; password: string}, waitForRedirect = true) {
await this.emailInput.fill(user.email);
await this.usernameInput.fill(user.username);
await this.passwordInput.fill(user.password);
await this.createAccountButton.click();
if (waitForRedirect) {
await this.page.waitForNavigation();
}
}
}
export {SignupPage};

52
e2e-tests/playwright/support/util.ts Обычный файл
Просмотреть файл

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {v4 as uuidv4} from 'uuid';
import {ViewportSize} from '@playwright/test';
const second = 1000;
const minute = 60 * 1000;
export const duration = {
half_sec: second / 2,
one_sec: second,
two_sec: second * 2,
four_sec: second * 4,
ten_sec: second * 10,
half_min: minute / 2,
one_min: minute,
two_min: minute * 2,
four_min: minute * 4,
};
/**
* Explicit `wait` should not normally used but made available for special cases.
* @param {number} ms - duration in millisecond
* @return {Promise} promise with timeout
*/
export const wait = async (ms = 0) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
/**
* @param {Number} length - length on random string to return, e.g. 7 (default)
* @return {String} random string
*/
export function getRandomId(length = 7): string {
const MAX_SUBSTRING_INDEX = 27;
return uuidv4()
.replace(/-/g, '')
.substring(MAX_SUBSTRING_INDEX - length, MAX_SUBSTRING_INDEX);
}
// Default team is meant for sysadmin's primary team,
// selected for compatibility with existing local development.
// It should not be used for testing.
export const defaultTeam = {name: 'ad-1', displayName: 'eligendi', type: 'O'};
export const illegalRe = /[/?<>\\:*|":&();]/g;
export function isSmallScreen(viewport?: ViewportSize | {width: number; height: number} | null) {
return viewport?.width ? Boolean(viewport?.width <= 390) : true;
}

Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import os from 'node:os';
import chalk from 'chalk';
import {expect, TestInfo} from '@playwright/test';
import {illegalRe} from '@e2e-support/util';
import testConfig, {TestArgs} from '@e2e-test.config';
import snapshotWithPercy from './percy';
export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs) {
if (os.platform() !== 'linux') {
// eslint-disable-next-line no-console
console.log(
chalk.yellow(
`^ Warning: No visual test performed. Run in Linux or Playwright docker image to match snapshot.`
)
);
return;
}
if (testConfig.snapshotEnabled) {
// Visual test with built-in snapshot
const filename = testInfo.title.replace(illegalRe, '').replace(/\s/g, '-').trim().toLowerCase();
expect(await testArgs.page.screenshot({fullPage: true})).toMatchSnapshot(`${filename}.png`);
}
if (testConfig.percyEnabled) {
// Used to easily identify the screenshot when viewing from third-party service provider.
const name = `[${testInfo.project.name}, ${testArgs?.viewport?.width}px] > ${testInfo.file} > ${testInfo.title}`;
// Visual test with Percy
await snapshotWithPercy(name, testArgs);
}
}

Просмотреть файл

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import percySnapshot from '@percy/playwright';
import testConfig, {TestArgs} from '@e2e-test.config';
export default async function snapshotWithPercy(name: string, testArgs: TestArgs) {
if (testArgs.browserName === 'chromium' && testConfig.percyEnabled && testArgs.viewport) {
if (!testConfig.percyToken) {
// eslint-disable-next-line no-console
console.error('Error: Token is missing! Please set using: "export PERCY_TOKEN=<change_me>"');
}
const {page, viewport} = testArgs;
// Ignore since percy is using Playwright.Page
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await percySnapshot(page, name, {widths: [viewport.width], minHeight: viewport.height});
}
}