MM-62954 E2E/Playwright shared library (#30177)
* feat: Add package.json for Playwright library with dependencies * feat: Add explicit exports for test.config in playwright-lib package * feat: Add initialization setup for Mattermost E2E testing with admin and user client * fix: Update package dependencies and resolve TypeScript build errors * feat: Update package exports for test.config to support both CommonJS and ESM * playwright shared library * add README, fix pipeline * keep file structures, move report up to playwright * minimize API, use the prerelease versions of client and types * bump version * update package*.json * resolve merge conflict * update depedencies and merge conflicts * update readme and fix ci * remove unnecessary export and list all external packages * fix import for Client4
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ce9632cca3
Коммит
a47269cfe2
Двоичные данные
e2e-tests/playwright/lib/src/asset/mattermost-icon_128x128.png
Обычный файл
Двоичные данные
e2e-tests/playwright/lib/src/asset/mattermost-icon_128x128.png
Обычный файл
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 13 KiB |
85
e2e-tests/playwright/lib/src/browser_context.ts
Обычный файл
85
e2e-tests/playwright/lib/src/browser_context.ts
Обычный файл
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {writeFile} from 'node:fs/promises';
|
||||
|
||||
import {Browser, BrowserContext, request} from '@playwright/test';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {testConfig} from './test_config';
|
||||
import {pages} from './ui/pages';
|
||||
|
||||
export class TestBrowser {
|
||||
readonly browser: Browser;
|
||||
context: BrowserContext | null;
|
||||
|
||||
constructor(browser: Browser) {
|
||||
this.browser = browser;
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
async login(user: UserProfile) {
|
||||
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();
|
||||
|
||||
const channelsPage = new pages.ChannelsPage(page);
|
||||
const systemConsolePage = new pages.SystemConsolePage(page);
|
||||
const scheduledDraftPage = new pages.ScheduledDraftPage(page);
|
||||
const draftPage = new pages.DraftPage(page);
|
||||
|
||||
this.context = context;
|
||||
|
||||
return {context, page, channelsPage, systemConsolePage, scheduledDraftPage, draftPage};
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.context) {
|
||||
await this.context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
6
e2e-tests/playwright/lib/src/constant.ts
Обычный файл
6
e2e-tests/playwright/lib/src/constant.ts
Обычный файл
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const appsPluginId = 'com.mattermost.apps';
|
||||
export const callsPluginId = 'com.mattermost.calls';
|
||||
export const playbooksPluginId = 'playbooks';
|
||||
93
e2e-tests/playwright/lib/src/file.ts
Обычный файл
93
e2e-tests/playwright/lib/src/file.ts
Обычный файл
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import mime from 'mime-types';
|
||||
|
||||
const commonAssetPath = path.resolve(__dirname, 'asset');
|
||||
export const assetPath = path.resolve(process.cwd(), 'asset');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const availableFiles = ['mattermost-icon_128x128.png'] as const;
|
||||
type AvailableFilename = (typeof availableFiles)[number];
|
||||
|
||||
/**
|
||||
* Reads file data and creates a File object.
|
||||
* @param filePath - The path to the file.
|
||||
* @returns A File object containing the file data.
|
||||
* @throws If the file does not exist.
|
||||
*/
|
||||
export function getFileData(filePath: string): File {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at path: ${filePath}`);
|
||||
}
|
||||
|
||||
const mimeType = mime.lookup(filePath) || undefined;
|
||||
const fileName = path.basename(filePath);
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
return new File([fileBuffer], fileName, {type: mimeType});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data and creates a Blob object.
|
||||
* @param filePath - The path to the file.
|
||||
* @returns A Blob object containing the file data.
|
||||
* @throws If the file does not exist.
|
||||
*/
|
||||
export function getBlobData(filePath: string): Blob {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at path: ${filePath}`);
|
||||
}
|
||||
|
||||
const mimeType = mime.lookup(filePath) || undefined;
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
return new Blob([fileBuffer], {type: mimeType});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the "asset" directory and creates a File object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @returns An object containing a File object
|
||||
*/
|
||||
export function getFileFromAsset(filename: string) {
|
||||
const filePath = path.join(assetPath, filename);
|
||||
|
||||
return getFileData(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the "asset" directory and creates a Blob object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @returns An object containing a Blob object
|
||||
*/
|
||||
export function getBlobFromAsset(filename: string) {
|
||||
const filePath = path.join(assetPath, filename);
|
||||
|
||||
return getBlobData(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the lib "asset" directory and creates a File object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @returns An object containing a File object
|
||||
*/
|
||||
export function getFileFromCommonAsset(filename: AvailableFilename) {
|
||||
const filePath = path.join(commonAssetPath, filename);
|
||||
|
||||
return getFileData(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the lib "asset" directory and creates a Blob object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @returns An object containing a Blob object
|
||||
*/
|
||||
export function getBlobFromCommonAsset(filename: AvailableFilename) {
|
||||
const filePath = path.join(commonAssetPath, filename);
|
||||
|
||||
return getBlobData(filePath);
|
||||
}
|
||||
88
e2e-tests/playwright/lib/src/flag.ts
Обычный файл
88
e2e-tests/playwright/lib/src/flag.ts
Обычный файл
@@ -0,0 +1,88 @@
|
||||
// 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 {callsPluginId} from './constant';
|
||||
import {getAdminClient} from './server/init';
|
||||
|
||||
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 async function shouldRunInLinux() {
|
||||
const platform = os.platform();
|
||||
expect(platform, 'Run in Linux or Playwright docker image only').toBe('linux');
|
||||
}
|
||||
|
||||
export async function ensureLicense() {
|
||||
const {adminClient} = await getAdminClient();
|
||||
let license = await adminClient.getClientLicenseOld();
|
||||
|
||||
if (license?.IsLicensed !== 'true') {
|
||||
const config = await adminClient.getClientConfigOld();
|
||||
expect(
|
||||
config.ServiceEnvironment === 'dev',
|
||||
'The trial license request fails in the local development environment. Please manually upload the test license.',
|
||||
).toBeFalsy();
|
||||
|
||||
await requestTrialLicense();
|
||||
|
||||
license = await adminClient.getClientLicenseOld();
|
||||
}
|
||||
|
||||
expect(license?.IsLicensed === 'true', 'Ensure server has license').toBeTruthy();
|
||||
}
|
||||
|
||||
export async function requestTrialLicense() {
|
||||
const {adminClient} = await getAdminClient();
|
||||
const admin = await adminClient.getMe();
|
||||
try {
|
||||
await adminClient.requestTrialLicense({
|
||||
receive_emails_accepted: true,
|
||||
terms_accepted: true,
|
||||
users: 100,
|
||||
contact_name: admin.first_name + ' ' + admin.last_name,
|
||||
contact_email: admin.email,
|
||||
company_name: 'Mattermost Playwright E2E Tests',
|
||||
company_size: '101-250',
|
||||
company_country: 'United States',
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error, 'Failed to request trial license').toBeFalsy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function skipIfNoLicense() {
|
||||
const {adminClient} = await getAdminClient();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(license.IsLicensed === 'false', 'Skipping test - server not licensed');
|
||||
}
|
||||
|
||||
export async function skipIfFeatureFlagNotSet(name: string, value: string | boolean) {
|
||||
const {adminClient} = await getAdminClient();
|
||||
const cfg = await adminClient.getConfig();
|
||||
|
||||
test.skip(cfg.FeatureFlags[name] !== value, `Skipping test - Feature Flag ${name} needs to be set to ${value}`);
|
||||
}
|
||||
210
e2e-tests/playwright/lib/src/global_setup.ts
Обычный файл
210
e2e-tests/playwright/lib/src/global_setup.ts
Обычный файл
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {Client4} from '@mattermost/client';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {PluginManifest} from '@mattermost/types/plugins';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {defaultTeam} from './util';
|
||||
import {createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server';
|
||||
import {testConfig} from './test_config';
|
||||
|
||||
export async function baseGlobalSetup() {
|
||||
let adminClient: Client4;
|
||||
let adminUser: UserProfile | null;
|
||||
({adminClient, adminUser} = await getAdminClient({skipLog: true}));
|
||||
|
||||
if (!adminUser) {
|
||||
const firstClient = new Client4();
|
||||
firstClient.setUrl(testConfig.baseURL);
|
||||
const defaultAdmin = getDefaultAdminUser();
|
||||
await firstClient.createUser(defaultAdmin, '', '');
|
||||
|
||||
({client: adminClient, user: adminUser} = await makeClient(defaultAdmin));
|
||||
}
|
||||
|
||||
await sysadminSetup(adminClient, adminUser);
|
||||
}
|
||||
|
||||
async function sysadminSetup(client: Client4, user: UserProfile | null) {
|
||||
// Ensure admin's email is verified.
|
||||
if (!user) {
|
||||
await client.verifyUserEmail(client.token);
|
||||
}
|
||||
|
||||
// Log license and config info
|
||||
await printLicenseInfo(client);
|
||||
await printClientInfo(client);
|
||||
|
||||
// Create default team if not present.
|
||||
// Otherwise, create other teams and channels other than the default team cna channels (town-square and off-topic).
|
||||
const myTeams = await client.getMyTeams();
|
||||
const myDefaultTeam = myTeams && myTeams.length > 0 && myTeams.find((team) => team.name === defaultTeam.name);
|
||||
if (!myDefaultTeam) {
|
||||
await client.createTeam(createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false));
|
||||
} else if (myDefaultTeam && testConfig.resetBeforeTest) {
|
||||
await Promise.all(
|
||||
myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)),
|
||||
);
|
||||
|
||||
const myChannels = await client.getMyChannels(myDefaultTeam.id);
|
||||
await Promise.all(
|
||||
myChannels
|
||||
.filter((channel) => {
|
||||
return (
|
||||
channel.team_id === myDefaultTeam.id &&
|
||||
channel.name !== 'town-square' &&
|
||||
channel.name !== 'off-topic'
|
||||
);
|
||||
})
|
||||
.map((channel) => client.deleteChannel(channel.id)),
|
||||
);
|
||||
}
|
||||
|
||||
// Set default preferences
|
||||
await savePreferences(client, user?.id ?? '');
|
||||
|
||||
// Ensure all products as plugin are installed and active.
|
||||
await ensurePluginsLoaded(client);
|
||||
|
||||
// Log plugin details
|
||||
await printPluginDetails(client);
|
||||
|
||||
// Ensure server deployment type is as expected
|
||||
await ensureServerDeployment(client);
|
||||
}
|
||||
|
||||
async function printLicenseInfo(client: Client4) {
|
||||
const license = await client.getClientLicenseOld();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Server License:
|
||||
- IsLicensed = ${license.IsLicensed}
|
||||
- IsTrial = ${license.IsTrial}
|
||||
- SkuName = ${license.SkuName}
|
||||
- SkuShortName = ${license.SkuShortName}
|
||||
- Cloud = ${license.Cloud}
|
||||
- Users = ${license.Users}`);
|
||||
}
|
||||
|
||||
async function printClientInfo(client: Client4) {
|
||||
const config = await client.getClientConfigOld();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Build Info:
|
||||
- BuildNumber = ${config.BuildNumber}
|
||||
- BuildDate = ${config.BuildDate}
|
||||
- Version = ${config.Version}
|
||||
- BuildHash = ${config.BuildHash}
|
||||
- BuildHashEnterprise = ${config.BuildHashEnterprise}
|
||||
- BuildEnterpriseReady = ${config.BuildEnterpriseReady}
|
||||
- TelemetryId = ${config.TelemetryId}
|
||||
- ServiceEnvironment = ${config.ServiceEnvironment}`);
|
||||
|
||||
const {LogSettings, ServiceSettings} = await client.getConfig();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Notable Server Config:
|
||||
- ServiceSettings.EnableSecurityFixAlert = ${ServiceSettings?.EnableSecurityFixAlert}
|
||||
- LogSettings.EnableDiagnostics = ${LogSettings?.EnableDiagnostics}`);
|
||||
}
|
||||
|
||||
export async function ensurePluginsLoaded(client: Client4) {
|
||||
const pluginStatus = await client.getPluginStatuses();
|
||||
const plugins = await client.getPlugins();
|
||||
|
||||
testConfig.ensurePluginsInstalled.forEach(async (pluginId) => {
|
||||
const isInstalled = pluginStatus.some((plugin) => plugin.plugin_id === pluginId);
|
||||
if (!isInstalled) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${pluginId} is not installed. Related visual test will fail.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive = plugins.active.some((plugin: PluginManifest) => plugin.id === pluginId);
|
||||
if (!isActive) {
|
||||
await client.enablePlugin(pluginId);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${pluginId} is installed and has been activated.`);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${pluginId} is installed and active.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function printPluginDetails(client: Client4) {
|
||||
const plugins = await client.getPlugins();
|
||||
|
||||
if (plugins.active.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Active plugins:');
|
||||
}
|
||||
|
||||
plugins.active.forEach((plugin: PluginManifest) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`);
|
||||
});
|
||||
|
||||
if (plugins.inactive.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Inactive plugins:');
|
||||
}
|
||||
|
||||
plugins.inactive.forEach((plugin: PluginManifest) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async function ensureServerDeployment(client: Client4) {
|
||||
if (testConfig.haClusterEnabled) {
|
||||
const {haClusterNodeCount, haClusterName} = testConfig;
|
||||
|
||||
const {Enable, ClusterName} = (await client.getConfig()).ClusterSettings;
|
||||
expect(Enable, Enable ? '' : 'Should have cluster enabled').toBe(true);
|
||||
|
||||
const sameClusterName = ClusterName === haClusterName;
|
||||
expect(
|
||||
sameClusterName,
|
||||
sameClusterName
|
||||
? ''
|
||||
: `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${haClusterName}"`,
|
||||
).toBe(true);
|
||||
|
||||
const clusterInfo = await client.getClusterStatus();
|
||||
const sameCount = clusterInfo?.length === haClusterNodeCount;
|
||||
expect(
|
||||
sameCount,
|
||||
sameCount
|
||||
? ''
|
||||
: `Should match number of nodes in a cluster as expected. Got "${clusterInfo?.length}" but expected "${haClusterNodeCount}"`,
|
||||
).toBe(true);
|
||||
|
||||
clusterInfo.forEach((info) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferences(client: Client4, userId: UserProfile['id']) {
|
||||
try {
|
||||
if (!userId) {
|
||||
throw new Error('userId is not defined');
|
||||
}
|
||||
|
||||
const preferences: PreferenceType[] = [
|
||||
{user_id: userId, category: 'tutorial_step', name: userId, value: '999'},
|
||||
{user_id: userId, category: 'crt_thread_pane_step', name: userId, value: '999'},
|
||||
];
|
||||
|
||||
await client.savePreferences(userId, preferences);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error saving preferences', error);
|
||||
}
|
||||
}
|
||||
22
e2e-tests/playwright/lib/src/index.ts
Обычный файл
22
e2e-tests/playwright/lib/src/index.ts
Обычный файл
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {test, expect, PlaywrightExtended} from './test_fixture';
|
||||
export {testConfig} from './test_config';
|
||||
export {baseGlobalSetup} from './global_setup';
|
||||
export {TestBrowser} from './browser_context';
|
||||
export {getBlobFromAsset, getFileFromAsset} from './file';
|
||||
export {duration, wait} from './util';
|
||||
|
||||
export {
|
||||
ChannelsPage,
|
||||
LandingLoginPage,
|
||||
LoginPage,
|
||||
ResetPasswordPage,
|
||||
SignupPage,
|
||||
ScheduledDraftPage,
|
||||
SystemConsolePage,
|
||||
DraftPage,
|
||||
} from './ui/pages';
|
||||
|
||||
export {TestArgs, ScreenshotOptions} from './types';
|
||||
90
e2e-tests/playwright/lib/src/mock_browser_api.ts
Обычный файл
90
e2e-tests/playwright/lib/src/mock_browser_api.ts
Обычный файл
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page} from '@playwright/test';
|
||||
|
||||
type NotificationData = {title: string} & NotificationOptions;
|
||||
|
||||
// Extend the Window interface to add custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
_originalNotification: typeof Notification;
|
||||
_notifications: NotificationData[];
|
||||
getNotifications: () => NotificationData[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `stubNotification` intercepts the Notification API to capture notifications.
|
||||
*
|
||||
* Note:
|
||||
* - Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.
|
||||
* - An `Error: page.evaluate: window.getNotifications is not a function` may occur if the `stubNotification` function is called before the page has fully loaded.
|
||||
*
|
||||
* @param page Page object
|
||||
* @param permission Permission setting for notifications, with possible values: "default" | "granted" | "denied". Note: A notification sound may still occur even when set to "denied", as the browser might attempt to trigger system notifications.
|
||||
*/
|
||||
export async function stubNotification(page: Page, permission: NotificationPermission) {
|
||||
await page.evaluate((notificationPermission: NotificationPermission) => {
|
||||
// Override the Notification.requestPermission method
|
||||
window.Notification.requestPermission = () => Promise.resolve(permission);
|
||||
|
||||
// Copy the original Notification
|
||||
if (!window._originalNotification) {
|
||||
window._originalNotification = window.Notification;
|
||||
}
|
||||
|
||||
// Initialize a list where to capture the notifications
|
||||
window._notifications = [];
|
||||
|
||||
// Override the Notification constructor
|
||||
class CustomNotification extends window._originalNotification {
|
||||
constructor(title: string, options?: NotificationOptions) {
|
||||
super(title, options);
|
||||
const notification = {title, ...options};
|
||||
window._notifications.push(notification);
|
||||
}
|
||||
}
|
||||
|
||||
// Set static properties and permission status
|
||||
Object.defineProperties(CustomNotification, {
|
||||
permission: {
|
||||
get: () => notificationPermission,
|
||||
},
|
||||
requestPermission: {
|
||||
value: () => Promise.resolve(notificationPermission),
|
||||
},
|
||||
});
|
||||
|
||||
// Replace the global Notification with the custom one
|
||||
window.Notification = CustomNotification as unknown as typeof Notification;
|
||||
|
||||
// Method to get all notifications
|
||||
window.getNotifications = () => window._notifications;
|
||||
}, permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* `waitForNotification` waits for a specified number of notifications to be received on the page within a given timeout.
|
||||
* @param page Page object
|
||||
* @param expectedCount Number of notifications to wait for before returning. (default: 1)
|
||||
* @param timeout Wait time in milliseconds. (default: 5000ms)
|
||||
* @returns An array of notifications received
|
||||
*/
|
||||
export async function waitForNotification(
|
||||
page: Page,
|
||||
expectedCount = 1,
|
||||
timeout: number = 5000,
|
||||
): Promise<NotificationData[]> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
const notifications = await page.evaluate(() => window.getNotifications());
|
||||
if (notifications.length >= expectedCount) {
|
||||
return notifications;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Notification not received within the timeout period of ${timeout}ms`);
|
||||
return [];
|
||||
}
|
||||
36
e2e-tests/playwright/lib/src/server/channel.ts
Обычный файл
36
e2e-tests/playwright/lib/src/server/channel.ts
Обычный файл
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Channel, ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
|
||||
type ChannelInput = {
|
||||
teamId: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
type?: ChannelType;
|
||||
purpose?: string;
|
||||
header?: string;
|
||||
unique?: boolean;
|
||||
};
|
||||
|
||||
export function createRandomChannel(channelInput: ChannelInput): Channel {
|
||||
const channel = {
|
||||
team_id: channelInput.teamId,
|
||||
name: channelInput.name,
|
||||
display_name: channelInput.displayName,
|
||||
type: channelInput.type || 'O',
|
||||
purpose: channelInput.type || '',
|
||||
header: channelInput.type || '',
|
||||
};
|
||||
|
||||
if (channelInput.unique) {
|
||||
const randomSuffix = getRandomId();
|
||||
|
||||
channel.name = `${channelInput.name}-${randomSuffix}`;
|
||||
channel.display_name = `${channelInput.displayName} ${randomSuffix}`;
|
||||
}
|
||||
|
||||
return channel as Channel;
|
||||
}
|
||||
58
e2e-tests/playwright/lib/src/server/client.ts
Обычный файл
58
e2e-tests/playwright/lib/src/server/client.ts
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
// Variable to hold cache
|
||||
const clients: Record<string, ClientCache> = {};
|
||||
|
||||
export async function makeClient(
|
||||
userRequest?: UserRequest,
|
||||
opts: {useCache?: boolean; skipLog?: boolean} = {useCache: true, skipLog: false},
|
||||
): Promise<ClientCache> {
|
||||
const client = new Client4();
|
||||
client.setUrl(testConfig.baseURL);
|
||||
|
||||
try {
|
||||
if (!userRequest) {
|
||||
return {client, user: null};
|
||||
}
|
||||
|
||||
const cacheKey = userRequest.username + userRequest.password;
|
||||
if (opts?.useCache && clients[cacheKey] != null) {
|
||||
return clients[cacheKey];
|
||||
}
|
||||
|
||||
const userProfile = await client.login(userRequest.username, userRequest.password);
|
||||
const user = {...userProfile, password: userRequest.password};
|
||||
|
||||
if (opts?.useCache) {
|
||||
clients[cacheKey] = {client, user};
|
||||
}
|
||||
|
||||
return {client, user};
|
||||
} catch (err) {
|
||||
if (!opts?.skipLog) {
|
||||
// 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: Client4;
|
||||
user: UserProfile | null;
|
||||
};
|
||||
778
e2e-tests/playwright/lib/src/server/default_config.ts
Обычный файл
778
e2e-tests/playwright/lib/src/server/default_config.ts
Обычный файл
@@ -0,0 +1,778 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {
|
||||
AdminConfig,
|
||||
ClusterSettings,
|
||||
CollapsedThreads,
|
||||
EmailSettings,
|
||||
ExperimentalSettings,
|
||||
LogSettings,
|
||||
PasswordSettings,
|
||||
PluginSettings,
|
||||
ServiceSettings,
|
||||
TeamSettings,
|
||||
} from '@mattermost/types/config';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
export function getOnPremServerConfig(): AdminConfig {
|
||||
return merge<AdminConfig>(defaultServerConfig, onPremServerConfig() as AdminConfig);
|
||||
}
|
||||
|
||||
type TestAdminConfig = {
|
||||
ClusterSettings: Partial<ClusterSettings>;
|
||||
EmailSettings: Partial<EmailSettings>;
|
||||
ExperimentalSettings: Partial<ExperimentalSettings>;
|
||||
LogSettings: Partial<LogSettings>;
|
||||
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,
|
||||
},
|
||||
EmailSettings: {
|
||||
PushNotificationServer: testConfig.pushNotificationServer,
|
||||
},
|
||||
LogSettings: {
|
||||
EnableDiagnostics: false,
|
||||
},
|
||||
PasswordSettings: {
|
||||
MinimumLength: 5,
|
||||
Lowercase: false,
|
||||
Number: false,
|
||||
Uppercase: false,
|
||||
Symbol: false,
|
||||
EnableForgotLink: true,
|
||||
},
|
||||
PluginSettings: {
|
||||
EnableUploads: true,
|
||||
PluginStates: {
|
||||
'com.mattermost.calls': {
|
||||
Enable: false,
|
||||
},
|
||||
'com.mattermost.nps': {
|
||||
Enable: false,
|
||||
},
|
||||
playbooks: {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceSettings: {
|
||||
SiteURL: testConfig.baseURL,
|
||||
EnableOnboardingFlow: false,
|
||||
EnableSecurityFixAlert: false,
|
||||
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
|
||||
EnableTesting: true,
|
||||
},
|
||||
TeamSettings: {
|
||||
EnableOpenServer: true,
|
||||
MaxUsersPerTeam: 2000,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Should be based only from the generated default config from ./server via "make config-reset"
|
||||
// Based on v10.5 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,
|
||||
EnableOutgoingOAuthConnections: false,
|
||||
EnableCommands: true,
|
||||
OutgoingIntegrationRequestsTimeout: 30,
|
||||
EnablePostUsernameOverride: false,
|
||||
EnablePostIconOverride: false,
|
||||
GoogleDeveloperKey: '',
|
||||
EnableLinkPreviews: true,
|
||||
EnablePermalinkPreviews: true,
|
||||
RestrictLinkPreviews: '',
|
||||
EnableTesting: false,
|
||||
EnableDeveloper: false,
|
||||
DeveloperFlags: '',
|
||||
EnableClientPerformanceDebugging: false,
|
||||
EnableSecurityFixAlert: true,
|
||||
EnableInsecureOutgoingConnections: false,
|
||||
AllowedUntrustedInternalConnections: '',
|
||||
EnableMultifactorAuthentication: false,
|
||||
EnforceMultifactorAuthentication: false,
|
||||
EnableUserAccessTokens: false,
|
||||
AllowCorsFrom: '',
|
||||
CorsExposedHeaders: '',
|
||||
CorsAllowCredentials: false,
|
||||
CorsDebug: false,
|
||||
AllowCookiesForSubdomains: false,
|
||||
ExtendSessionLengthWithActivity: true,
|
||||
TerminateSessionsOnPasswordChange: 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,
|
||||
GiphySdkKey: '',
|
||||
EnableCustomEmoji: true,
|
||||
EnableEmojiPicker: true,
|
||||
PostEditTimeLimit: -1,
|
||||
TimeBetweenUserTypingUpdatesMilliseconds: 5000,
|
||||
EnableCrossTeamSearch: true,
|
||||
EnablePostSearch: true,
|
||||
EnableFileSearch: true,
|
||||
MinimumHashtagLength: 3,
|
||||
EnableUserTypingMessages: true,
|
||||
EnableChannelViewedMessages: true,
|
||||
EnableUserStatuses: true,
|
||||
ExperimentalEnableAuthenticationTransfer: true,
|
||||
ClusterLogTimeoutMilliseconds: 2000,
|
||||
EnableTutorial: true,
|
||||
EnableOnboardingFlow: true,
|
||||
ExperimentalEnableDefaultChannelLeaveJoinMessages: true,
|
||||
ExperimentalGroupUnreadChannels: 'disabled',
|
||||
EnableAPITeamDeletion: false,
|
||||
EnableAPITriggerAdminNotifications: false,
|
||||
EnableAPIUserDeletion: false,
|
||||
EnableAPIPostDeletion: false,
|
||||
EnableDesktopLandingPage: true,
|
||||
ExperimentalEnableHardenedMode: false,
|
||||
ExperimentalStrictCSRFEnforcement: false,
|
||||
EnableEmailInvitations: false,
|
||||
DisableBotsWhenOwnerIsDeactivated: true,
|
||||
EnableBotAccountCreation: false,
|
||||
EnableSVGs: false,
|
||||
EnableLatex: false,
|
||||
EnableInlineLatex: true,
|
||||
PostPriority: true,
|
||||
AllowPersistentNotifications: true,
|
||||
AllowPersistentNotificationsForGuests: false,
|
||||
PersistentNotificationIntervalMinutes: 5,
|
||||
PersistentNotificationMaxCount: 6,
|
||||
PersistentNotificationMaxRecipients: 5,
|
||||
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,
|
||||
AllowSyncedDrafts: true,
|
||||
UniqueEmojiReactionLimitPerPost: 50,
|
||||
RefreshPostStatsRunTime: '00:00',
|
||||
MaximumPayloadSizeBytes: 300000,
|
||||
MaximumURLLength: 2048,
|
||||
ScheduledPosts: true,
|
||||
},
|
||||
TeamSettings: {
|
||||
SiteName: 'Mattermost',
|
||||
MaxUsersPerTeam: 50,
|
||||
EnableJoinLeaveMessageByDefault: true,
|
||||
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: [],
|
||||
ReplicaMonitorIntervalSeconds: 5,
|
||||
},
|
||||
LogSettings: {
|
||||
EnableConsole: true,
|
||||
ConsoleLevel: 'DEBUG',
|
||||
ConsoleJson: true,
|
||||
EnableColor: false,
|
||||
EnableFile: true,
|
||||
FileLevel: 'INFO',
|
||||
FileJson: true,
|
||||
FileLocation: '',
|
||||
EnableWebhookDebugging: true,
|
||||
EnableDiagnostics: true,
|
||||
VerboseDiagnostics: false,
|
||||
EnableSentry: true,
|
||||
AdvancedLoggingJSON: {},
|
||||
MaxFieldSize: 2048,
|
||||
},
|
||||
ExperimentalAuditSettings: {
|
||||
FileEnabled: false,
|
||||
FileName: '',
|
||||
FileMaxSizeMB: 100,
|
||||
FileMaxAgeDays: 0,
|
||||
FileMaxBackups: 0,
|
||||
FileCompress: false,
|
||||
FileMaxQueueSize: 1000,
|
||||
AdvancedLoggingJSON: {},
|
||||
},
|
||||
NotificationLogSettings: {
|
||||
EnableConsole: true,
|
||||
ConsoleLevel: 'DEBUG',
|
||||
ConsoleJson: true,
|
||||
EnableColor: false,
|
||||
EnableFile: true,
|
||||
FileLevel: 'INFO',
|
||||
FileJson: true,
|
||||
FileLocation: '',
|
||||
AdvancedLoggingJSON: {},
|
||||
},
|
||||
PasswordSettings: {
|
||||
MinimumLength: 8,
|
||||
Lowercase: false,
|
||||
Number: false,
|
||||
Uppercase: false,
|
||||
Symbol: false,
|
||||
EnableForgotLink: true,
|
||||
},
|
||||
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,
|
||||
AmazonS3UploadPartSizeBytes: 5242880,
|
||||
AmazonS3StorageClass: '',
|
||||
DedicatedExportStore: false,
|
||||
ExportDriverName: 'local',
|
||||
ExportDirectory: './data/',
|
||||
ExportAmazonS3AccessKeyId: '',
|
||||
ExportAmazonS3SecretAccessKey: '',
|
||||
ExportAmazonS3Bucket: '',
|
||||
ExportAmazonS3PathPrefix: '',
|
||||
ExportAmazonS3Region: '',
|
||||
ExportAmazonS3Endpoint: 's3.amazonaws.com',
|
||||
ExportAmazonS3SSL: true,
|
||||
ExportAmazonS3SignV2: false,
|
||||
ExportAmazonS3SSE: false,
|
||||
ExportAmazonS3Trace: false,
|
||||
ExportAmazonS3RequestTimeoutMilliseconds: 30000,
|
||||
ExportAmazonS3PresignExpiresSeconds: 21600,
|
||||
ExportAmazonS3UploadPartSizeBytes: 104857600,
|
||||
ExportAmazonS3StorageClass: '',
|
||||
},
|
||||
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',
|
||||
PushNotificationServerType: 'custom',
|
||||
PushNotificationServerLocation: 'us',
|
||||
PushNotificationContents: 'full',
|
||||
PushNotificationBuffer: 1000,
|
||||
EnableEmailBatching: false,
|
||||
EmailBatchingBufferSize: 256,
|
||||
EmailBatchingInterval: 30,
|
||||
EnablePreviewModeBanner: true,
|
||||
SkipServerCertificateVerification: false,
|
||||
EmailNotificationContentsType: 'full',
|
||||
LoginButtonColor: '#0000',
|
||||
LoginButtonBorderColor: '#2389D7',
|
||||
LoginButtonTextColor: '#2389D7',
|
||||
},
|
||||
RateLimitSettings: {
|
||||
Enable: false,
|
||||
PerSec: 10,
|
||||
MaxBurst: 100,
|
||||
MemoryStoreSize: 10000,
|
||||
VaryByRemoteAddr: true,
|
||||
VaryByUser: false,
|
||||
VaryByHeader: '',
|
||||
},
|
||||
PrivacySettings: {
|
||||
ShowEmailAddress: true,
|
||||
ShowFullName: true,
|
||||
},
|
||||
SupportSettings: {
|
||||
TermsOfServiceLink: 'https://mattermost.com/pl/terms-of-use/',
|
||||
PrivacyPolicyLink: 'https://mattermost.com/pl/privacy-policy/',
|
||||
AboutLink: 'https://mattermost.com/pl/about-mattermost',
|
||||
HelpLink: 'https://mattermost.com/pl/help/',
|
||||
ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug',
|
||||
ForgotPasswordLink: '',
|
||||
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,
|
||||
MaximumLoginAttempts: 10,
|
||||
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',
|
||||
},
|
||||
ComplianceSettings: {
|
||||
Enable: false,
|
||||
Directory: './data/',
|
||||
EnableDaily: false,
|
||||
BatchSize: 30000,
|
||||
},
|
||||
LocalizationSettings: {
|
||||
DefaultServerLocale: 'en',
|
||||
DefaultClientLocale: 'en',
|
||||
AvailableLocales: '',
|
||||
EnableExperimentalLocales: false,
|
||||
},
|
||||
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/pl/download-apps',
|
||||
AndroidAppDownloadLink: 'https://mattermost.com/pl/android-app/',
|
||||
IosAppDownloadLink: 'https://mattermost.com/pl/ios-app/',
|
||||
MobileExternalBrowser: false,
|
||||
},
|
||||
CacheSettings: {
|
||||
CacheType: 'lru',
|
||||
RedisAddress: '',
|
||||
RedisPassword: '',
|
||||
RedisDB: -1,
|
||||
DisableClientCache: false,
|
||||
},
|
||||
ClusterSettings: {
|
||||
Enable: false,
|
||||
ClusterName: '',
|
||||
OverrideHostname: '',
|
||||
NetworkInterface: '',
|
||||
BindAddress: '',
|
||||
AdvertiseAddress: '',
|
||||
UseIPAddress: true,
|
||||
EnableGossipCompression: true,
|
||||
EnableExperimentalGossipEncryption: false,
|
||||
ReadOnlyConfig: true,
|
||||
GossipPort: 8074,
|
||||
},
|
||||
MetricsSettings: {
|
||||
Enable: false,
|
||||
BlockProfileRate: 0,
|
||||
ListenAddress: ':8067',
|
||||
EnableClientMetrics: true,
|
||||
EnableNotificationMetrics: true,
|
||||
},
|
||||
ExperimentalSettings: {
|
||||
ClientSideCertEnable: false,
|
||||
ClientSideCertCheck: 'secondary',
|
||||
LinkMetadataTimeoutMilliseconds: 5000,
|
||||
RestrictSystemAdmin: false,
|
||||
EnableSharedChannels: false,
|
||||
EnableRemoteClusterService: false,
|
||||
DisableAppBar: false,
|
||||
DisableRefetchingOnBrowserFocus: false,
|
||||
DelayChannelAutocomplete: false,
|
||||
DisableWakeUpReconnectHandler: false,
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds: 3000,
|
||||
YoutubeReferrerPolicy: false,
|
||||
},
|
||||
AnalyticsSettings: {
|
||||
MaxUsersForStatistics: 2500,
|
||||
},
|
||||
ElasticsearchSettings: {
|
||||
ConnectionURL: 'http://localhost:9200',
|
||||
Backend: 'elasticsearch',
|
||||
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: 10,
|
||||
BatchSize: 10000,
|
||||
RequestTimeoutSeconds: 30,
|
||||
SkipTLSVerification: false,
|
||||
CA: '',
|
||||
ClientCert: '',
|
||||
ClientKey: '',
|
||||
Trace: '',
|
||||
IgnoredPurgeIndexes: '',
|
||||
},
|
||||
BleveSettings: {
|
||||
IndexDir: '',
|
||||
EnableIndexing: false,
|
||||
EnableSearching: false,
|
||||
EnableAutocomplete: false,
|
||||
BatchSize: 10000,
|
||||
},
|
||||
DataRetentionSettings: {
|
||||
EnableMessageDeletion: false,
|
||||
EnableFileDeletion: false,
|
||||
EnableBoardsDeletion: false,
|
||||
MessageRetentionDays: 365,
|
||||
MessageRetentionHours: 0,
|
||||
FileRetentionDays: 365,
|
||||
FileRetentionHours: 0,
|
||||
BoardsRetentionDays: 365,
|
||||
DeletionJobStartTime: '02:00',
|
||||
BatchSize: 3000,
|
||||
TimeBetweenBatchesMilliseconds: 100,
|
||||
RetentionIdsBatchSize: 100,
|
||||
},
|
||||
MessageExportSettings: {
|
||||
EnableExport: false,
|
||||
ExportFormat: 'actiance',
|
||||
DailyRunTime: '01:00',
|
||||
ExportFromTimestamp: 0,
|
||||
BatchSize: 10000,
|
||||
DownloadExportResults: false,
|
||||
ChannelBatchSize: 100,
|
||||
ChannelHistoryBatchSize: 10,
|
||||
GlobalRelaySettings: {
|
||||
CustomerType: 'A9',
|
||||
SMTPUsername: '',
|
||||
SMTPPassword: '',
|
||||
EmailAddress: '',
|
||||
SMTPServerTimeout: 1800,
|
||||
CustomSMTPServerName: '',
|
||||
CustomSMTPPort: '25',
|
||||
},
|
||||
},
|
||||
JobSettings: {
|
||||
RunJobs: true,
|
||||
RunScheduler: true,
|
||||
CleanupJobsThresholdDays: -1,
|
||||
CleanupConfigThresholdDays: -1,
|
||||
},
|
||||
PluginSettings: {
|
||||
Enable: true,
|
||||
EnableUploads: false,
|
||||
AllowInsecureDownloadURL: false,
|
||||
EnableHealthCheck: true,
|
||||
Directory: './plugins',
|
||||
ClientDirectory: './client/plugins',
|
||||
Plugins: {},
|
||||
PluginStates: {
|
||||
'com.mattermost.calls': {
|
||||
Enable: true,
|
||||
},
|
||||
'com.mattermost.nps': {
|
||||
Enable: true,
|
||||
},
|
||||
'mattermost-ai': {
|
||||
Enable: true,
|
||||
},
|
||||
playbooks: {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
EnableMarketplace: true,
|
||||
EnableRemoteMarketplace: true,
|
||||
AutomaticPrepackagedPlugins: true,
|
||||
RequirePluginSignature: false,
|
||||
MarketplaceURL: 'https://api.integrations.mattermost.com',
|
||||
SignaturePublicKeyFiles: [],
|
||||
ChimeraOAuthProxyURL: '',
|
||||
},
|
||||
DisplaySettings: {
|
||||
CustomURLSchemes: [],
|
||||
MaxMarkdownNodes: 0,
|
||||
},
|
||||
GuestAccountsSettings: {
|
||||
Enable: false,
|
||||
HideTags: 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',
|
||||
CWSMock: false,
|
||||
Disable: false,
|
||||
},
|
||||
FeatureFlags: {
|
||||
TestFeature: 'off',
|
||||
TestBoolFeature: false,
|
||||
EnableRemoteClusterService: false,
|
||||
EnableSharedChannelsDMs: false,
|
||||
AppsEnabled: false,
|
||||
PermalinkPreviews: false,
|
||||
NormalizeLdapDNs: false,
|
||||
WysiwygEditor: false,
|
||||
OnboardingTourTips: true,
|
||||
DeprecateCloudFree: false,
|
||||
EnableExportDirectDownload: false,
|
||||
MoveThreadsEnabled: false,
|
||||
StreamlinedMarketplace: true,
|
||||
CloudIPFiltering: false,
|
||||
ConsumePostHook: false,
|
||||
CloudAnnualRenewals: false,
|
||||
CloudDedicatedExportUI: false,
|
||||
ChannelBookmarks: true,
|
||||
WebSocketEventScope: true,
|
||||
NotificationMonitoring: true,
|
||||
ExperimentalAuditSettingsSystemConsoleUI: false,
|
||||
CustomProfileAttributes: false,
|
||||
},
|
||||
ImportSettings: {
|
||||
Directory: './import',
|
||||
RetentionDays: 30,
|
||||
},
|
||||
ExportSettings: {
|
||||
Directory: './export',
|
||||
RetentionDays: 30,
|
||||
},
|
||||
WranglerSettings: {
|
||||
PermittedWranglerRoles: [],
|
||||
AllowedEmailDomain: [],
|
||||
MoveThreadMaxCount: 100,
|
||||
MoveThreadToAnotherTeamEnable: false,
|
||||
MoveThreadFromPrivateChannelEnable: false,
|
||||
MoveThreadFromDirectMessageChannelEnable: false,
|
||||
MoveThreadFromGroupMessageChannelEnable: false,
|
||||
},
|
||||
ConnectedWorkspacesSettings: {
|
||||
EnableSharedChannels: false,
|
||||
EnableRemoteClusterService: false,
|
||||
DisableSharedChannelsStatusSync: false,
|
||||
MaxPostsPerSync: 50,
|
||||
},
|
||||
};
|
||||
10
e2e-tests/playwright/lib/src/server/index.ts
Обычный файл
10
e2e-tests/playwright/lib/src/server/index.ts
Обычный файл
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {makeClient} from './client';
|
||||
export {createRandomChannel} from './channel';
|
||||
export {getOnPremServerConfig} from './default_config';
|
||||
export {initSetup, getAdminClient} from './init';
|
||||
export {createRandomPost} from './post';
|
||||
export {createRandomTeam} from './team';
|
||||
export {createRandomUser, getDefaultAdminUser} from './user';
|
||||
89
e2e-tests/playwright/lib/src/server/init.ts
Обычный файл
89
e2e-tests/playwright/lib/src/server/init.ts
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {makeClient} from './client';
|
||||
import {getOnPremServerConfig} from './default_config';
|
||||
import {createRandomTeam} from './team';
|
||||
import {createRandomUser} from './user';
|
||||
|
||||
import {getFileFromCommonAsset} from '@/file';
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
export async function initSetup({
|
||||
userPrefix = 'user',
|
||||
teamPrefix = {name: 'team', displayName: 'Team'},
|
||||
withDefaultProfileImage = true,
|
||||
} = {}) {
|
||||
try {
|
||||
// Login the admin user via API
|
||||
const {adminClient, adminUser} = await getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to setup admin: Admin user not found.');
|
||||
}
|
||||
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() as any);
|
||||
|
||||
// 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) {
|
||||
const file = getFileFromCommonAsset('mattermost-icon_128x128.png');
|
||||
await userClient.uploadProfileImage(user.id, file);
|
||||
}
|
||||
|
||||
// Update user preference
|
||||
const preferences: PreferenceType[] = [
|
||||
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
||||
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
|
||||
];
|
||||
await userClient.savePreferences(user.id, preferences);
|
||||
|
||||
return {
|
||||
adminClient,
|
||||
adminUser,
|
||||
adminConfig,
|
||||
user,
|
||||
userClient,
|
||||
team,
|
||||
offTopicUrl: getUrl(team.name, 'off-topic'),
|
||||
townSquareUrl: getUrl(team.name, 'town-square'),
|
||||
};
|
||||
} catch (error) {
|
||||
expect(error, 'Should not throw an error').toBeFalsy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminClient(opts: {skipLog: boolean} = {skipLog: false}) {
|
||||
const {client: adminClient, user: adminUser} = await makeClient(
|
||||
{
|
||||
username: testConfig.adminUsername,
|
||||
password: testConfig.adminPassword,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
||||
return {adminClient, adminUser};
|
||||
}
|
||||
|
||||
function getUrl(teamName: string, channelName: string) {
|
||||
return `/${teamName}/channels/${channelName}`;
|
||||
}
|
||||
33
e2e-tests/playwright/lib/src/server/post.ts
Обычный файл
33
e2e-tests/playwright/lib/src/server/post.ts
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Post, PostMetadata} from '@mattermost/types/posts';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
|
||||
export function createRandomPost(post?: Partial<Post>): Post {
|
||||
if (post && post.channel_id && post.user_id) {
|
||||
const time = Date.now();
|
||||
|
||||
const defaultPost = {
|
||||
create_at: time,
|
||||
user_id: post.user_id,
|
||||
channel_id: post.channel_id,
|
||||
root_id: post.root_id || '',
|
||||
message: `${post?.message ?? ''}${getRandomId()}`,
|
||||
pending_post_id: `${post.user_id}:${time}`,
|
||||
props: post?.props || {},
|
||||
file_ids: post?.file_ids || [],
|
||||
metadata: {} as PostMetadata,
|
||||
};
|
||||
|
||||
Reflect.deleteProperty(post, 'user_id');
|
||||
Reflect.deleteProperty(post, 'channel_id');
|
||||
Reflect.deleteProperty(post, 'message');
|
||||
Reflect.deleteProperty(post, 'pending_post_id');
|
||||
|
||||
return {...defaultPost, ...post} as Post;
|
||||
}
|
||||
|
||||
throw new Error('Post is missing channel_id or user_id or both');
|
||||
}
|
||||
18
e2e-tests/playwright/lib/src/server/team.ts
Обычный файл
18
e2e-tests/playwright/lib/src/server/team.ts
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// 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 '@/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;
|
||||
}
|
||||
34
e2e-tests/playwright/lib/src/server/user.ts
Обычный файл
34
e2e-tests/playwright/lib/src/server/user.ts
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
import {testConfig} from '@/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;
|
||||
}
|
||||
20
e2e-tests/playwright/lib/src/test_action.ts
Обычный файл
20
e2e-tests/playwright/lib/src/test_action.ts
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// 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)),
|
||||
);
|
||||
}
|
||||
63
e2e-tests/playwright/lib/src/test_config.ts
Обычный файл
63
e2e-tests/playwright/lib/src/test_config.ts
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// All process.env should be defined here
|
||||
export class TestConfig {
|
||||
baseURL: string;
|
||||
adminUsername: string;
|
||||
adminPassword: string;
|
||||
adminEmail: string;
|
||||
ensurePluginsInstalled: string[];
|
||||
haClusterEnabled: boolean;
|
||||
haClusterNodeCount: number;
|
||||
haClusterName: string;
|
||||
pushNotificationServer: string;
|
||||
resetBeforeTest: boolean;
|
||||
isCI: boolean;
|
||||
headless: boolean;
|
||||
slowMo: number;
|
||||
workers: number;
|
||||
snapshotEnabled: boolean;
|
||||
percyEnabled: boolean;
|
||||
|
||||
constructor() {
|
||||
// Server
|
||||
this.baseURL = process.env.PW_BASE_URL || 'http://localhost:8065';
|
||||
this.adminUsername = process.env.PW_ADMIN_USERNAME || 'sysadmin';
|
||||
this.adminPassword = process.env.PW_ADMIN_PASSWORD || 'Sys@dmin-sample1';
|
||||
this.adminEmail = process.env.PW_ADMIN_EMAIL || 'sysadmin@sample.mattermost.com';
|
||||
this.ensurePluginsInstalled =
|
||||
typeof process.env?.PW_ENSURE_PLUGINS_INSTALLED === 'string'
|
||||
? process.env.PW_ENSURE_PLUGINS_INSTALLED.split(',').filter((plugin) => Boolean(plugin))
|
||||
: [];
|
||||
this.haClusterEnabled = parseBool(process.env.PW_HA_CLUSTER_ENABLED, false);
|
||||
this.haClusterNodeCount = parseNumber(process.env.PW_HA_CLUSTER_NODE_COUNT, 2);
|
||||
this.haClusterName = process.env.PW_HA_CLUSTER_NAME || 'mm_dev_cluster';
|
||||
this.pushNotificationServer = process.env.PW_PUSH_NOTIFICATION_SERVER || 'https://push-test.mattermost.com';
|
||||
this.resetBeforeTest = parseBool(process.env.PW_RESET_BEFORE_TEST, false);
|
||||
// CI
|
||||
this.isCI = !!process.env.CI;
|
||||
// Playwright
|
||||
this.headless = parseBool(process.env.PW_HEADLESS, true);
|
||||
this.slowMo = parseNumber(process.env.PW_SLOWMO, 0);
|
||||
this.workers = parseNumber(process.env.PW_WORKERS, 1);
|
||||
// Visual tests
|
||||
this.snapshotEnabled = parseBool(process.env.PW_SNAPSHOT_ENABLE, false);
|
||||
this.percyEnabled = parseBool(process.env.PW_PERCY_ENABLE, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const testConfig = new TestConfig();
|
||||
|
||||
function parseBool(actualValue: string | undefined, defaultValue: boolean) {
|
||||
return actualValue ? actualValue === 'true' : defaultValue;
|
||||
}
|
||||
|
||||
function parseNumber(actualValue: string | undefined, defaultValue: number) {
|
||||
return actualValue ? parseInt(actualValue, 10) : defaultValue;
|
||||
}
|
||||
220
e2e-tests/playwright/lib/src/test_fixture.ts
Обычный файл
220
e2e-tests/playwright/lib/src/test_fixture.ts
Обычный файл
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Browser, Page, test as base} from '@playwright/test';
|
||||
import {AxeResults} from 'axe-core';
|
||||
import {AxeBuilder} from '@axe-core/playwright';
|
||||
|
||||
import {TestBrowser} from './browser_context';
|
||||
import {
|
||||
ensureLicense,
|
||||
shouldHaveCallsEnabled,
|
||||
shouldHaveFeatureFlag,
|
||||
shouldRunInLinux,
|
||||
skipIfFeatureFlagNotSet,
|
||||
skipIfNoLicense,
|
||||
} from './flag';
|
||||
import {getBlobFromAsset, getFileFromAsset} from './file';
|
||||
import {
|
||||
createRandomChannel,
|
||||
createRandomPost,
|
||||
createRandomTeam,
|
||||
createRandomUser,
|
||||
getAdminClient,
|
||||
initSetup,
|
||||
} from './server';
|
||||
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
|
||||
import {pages} from './ui/pages';
|
||||
import {matchSnapshot} from './visual';
|
||||
import {stubNotification, waitForNotification} from './mock_browser_api';
|
||||
import {duration, getRandomId, simpleEmailRe, wait} from './util';
|
||||
|
||||
export {expect} from '@playwright/test';
|
||||
|
||||
export type ExtendedFixtures = {
|
||||
axe: AxeBuilderExtended;
|
||||
pw: PlaywrightExtended;
|
||||
};
|
||||
|
||||
type AxeBuilderOptions = {
|
||||
disableColorContrast?: boolean;
|
||||
disableLinkInTextBlock?: boolean;
|
||||
};
|
||||
|
||||
export const test = base.extend<ExtendedFixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
axe: async ({}, use) => {
|
||||
const ab = new AxeBuilderExtended();
|
||||
await use(ab);
|
||||
},
|
||||
pw: async ({browser, page, isMobile}, use) => {
|
||||
const pw = new PlaywrightExtended(browser, page, isMobile);
|
||||
await use(pw);
|
||||
await pw.testBrowser.close();
|
||||
},
|
||||
});
|
||||
|
||||
export class PlaywrightExtended {
|
||||
// ./browser_context
|
||||
readonly testBrowser;
|
||||
|
||||
// ./flag
|
||||
readonly shouldHaveCallsEnabled;
|
||||
readonly shouldHaveFeatureFlag;
|
||||
readonly shouldRunInLinux;
|
||||
readonly ensureLicense;
|
||||
readonly skipIfNoLicense;
|
||||
readonly skipIfFeatureFlagNotSet;
|
||||
|
||||
// ./file
|
||||
readonly getBlobFromAsset;
|
||||
readonly getFileFromAsset;
|
||||
|
||||
// ./server
|
||||
readonly getAdminClient;
|
||||
readonly initSetup;
|
||||
|
||||
// ./test_action
|
||||
readonly hideDynamicChannelsContent;
|
||||
readonly waitForAnimationEnd;
|
||||
readonly waitUntil;
|
||||
|
||||
// ./mock_browser_api
|
||||
readonly stubNotification;
|
||||
readonly waitForNotification;
|
||||
|
||||
// ./visual
|
||||
readonly matchSnapshot;
|
||||
|
||||
// ./util
|
||||
readonly duration;
|
||||
readonly simpleEmailRe;
|
||||
readonly wait;
|
||||
|
||||
// random
|
||||
readonly random;
|
||||
|
||||
// unauthenticated page
|
||||
readonly loginPage;
|
||||
readonly landingLoginPage;
|
||||
readonly signupPage;
|
||||
readonly resetPasswordPage;
|
||||
|
||||
readonly hasSeenLandingPage;
|
||||
|
||||
constructor(browser: Browser, page: Page, isMobile: boolean) {
|
||||
// ./browser_context
|
||||
this.testBrowser = new TestBrowser(browser);
|
||||
|
||||
// ./flag
|
||||
this.shouldHaveCallsEnabled = shouldHaveCallsEnabled;
|
||||
this.shouldHaveFeatureFlag = shouldHaveFeatureFlag;
|
||||
this.shouldRunInLinux = shouldRunInLinux;
|
||||
this.ensureLicense = ensureLicense;
|
||||
this.skipIfNoLicense = skipIfNoLicense;
|
||||
this.skipIfFeatureFlagNotSet = skipIfFeatureFlagNotSet;
|
||||
|
||||
// ./file
|
||||
this.getBlobFromAsset = getBlobFromAsset;
|
||||
this.getFileFromAsset = getFileFromAsset;
|
||||
|
||||
// ./server
|
||||
this.initSetup = initSetup;
|
||||
this.getAdminClient = getAdminClient;
|
||||
|
||||
// ./test_action
|
||||
this.hideDynamicChannelsContent = hideDynamicChannelsContent;
|
||||
this.waitForAnimationEnd = waitForAnimationEnd;
|
||||
this.waitUntil = waitUntil;
|
||||
|
||||
// unauthenticated page
|
||||
this.loginPage = new pages.LoginPage(page);
|
||||
this.landingLoginPage = new pages.LandingLoginPage(page, isMobile);
|
||||
this.signupPage = new pages.SignupPage(page);
|
||||
this.resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
|
||||
// ./mock_browser_api
|
||||
this.stubNotification = stubNotification;
|
||||
this.waitForNotification = waitForNotification;
|
||||
|
||||
// ./visual
|
||||
this.matchSnapshot = matchSnapshot;
|
||||
|
||||
// ./util
|
||||
this.duration = duration;
|
||||
this.wait = wait;
|
||||
this.simpleEmailRe = simpleEmailRe;
|
||||
|
||||
this.random = {
|
||||
id: getRandomId,
|
||||
channel: createRandomChannel,
|
||||
post: createRandomPost,
|
||||
team: createRandomTeam,
|
||||
user: createRandomUser,
|
||||
};
|
||||
|
||||
this.hasSeenLandingPage = async () => {
|
||||
// Visit the base URL to be able to set the localStorage
|
||||
await page.goto('/');
|
||||
return await waitUntilLocalStorageIsSet(page, '__landingPageSeen__', 'true');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AxeBuilderExtended {
|
||||
readonly builder: (page: Page, options?: AxeBuilderOptions) => AxeBuilder;
|
||||
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags
|
||||
readonly tags: string[] = ['wcag2a', 'wcag2aa'];
|
||||
|
||||
constructor() {
|
||||
this.builder = (page: Page, options: AxeBuilderOptions = {}) => {
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md#wcag-20-level-a--aa-rules
|
||||
const disabledRules: string[] = [];
|
||||
|
||||
if (options.disableColorContrast) {
|
||||
// Disabled in pages due to impact to overall theme of Mattermost.
|
||||
// Option: make use of custom theme to improve color contrast.
|
||||
disabledRules.push('color-contrast');
|
||||
}
|
||||
|
||||
if (options.disableLinkInTextBlock) {
|
||||
// Disabled in pages due to impact to overall theme of Mattermost.
|
||||
// Option: make use of custom theme to improve color contrast.
|
||||
disabledRules.push('link-in-text-block');
|
||||
}
|
||||
|
||||
return new AxeBuilder({page}).withTags(this.tags).disableRules(disabledRules);
|
||||
};
|
||||
}
|
||||
|
||||
violationFingerprints(accessibilityScanResults: AxeResults) {
|
||||
const fingerprints = accessibilityScanResults.violations.map((violation) => ({
|
||||
rule: violation.id,
|
||||
description: violation.description,
|
||||
helpUrl: violation.helpUrl,
|
||||
targets: violation.nodes.map((node) => {
|
||||
return {target: node.target, impact: node.impact, html: node.html};
|
||||
}),
|
||||
}));
|
||||
|
||||
return JSON.stringify(fingerprints, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilLocalStorageIsSet(page: Page, key: string, value: string, timeout = duration.ten_sec) {
|
||||
await waitUntil(
|
||||
() =>
|
||||
page.evaluate(
|
||||
({key, value}) => {
|
||||
if (localStorage.getItem(key) === value) {
|
||||
return true;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
return false;
|
||||
},
|
||||
{key, value},
|
||||
),
|
||||
{timeout},
|
||||
);
|
||||
}
|
||||
111
e2e-tests/playwright/lib/src/types.ts
Обычный файл
111
e2e-tests/playwright/lib/src/types.ts
Обычный файл
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, Page, ViewportSize} from '@playwright/test';
|
||||
|
||||
export type TestArgs = {
|
||||
page: Page;
|
||||
browserName: string;
|
||||
viewport?: ViewportSize | null;
|
||||
};
|
||||
|
||||
// Based on https://github.com/microsoft/playwright/blob/d6ec1ae3994f127e38b866a231a34efc6a4cac0d/packages/playwright/types/test.d.ts#L5692-L5759
|
||||
export type ScreenshotOptions = {
|
||||
/**
|
||||
* When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different
|
||||
* treatment depending on their duration:
|
||||
* - finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||
* - infinite animations are canceled to initial state, and then played over after the screenshot.
|
||||
*
|
||||
* Defaults to `"disabled"` that disables animations.
|
||||
*/
|
||||
animations?: 'disabled' | 'allow';
|
||||
|
||||
/**
|
||||
* When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be
|
||||
* changed. Defaults to `"hide"`.
|
||||
*/
|
||||
caret?: 'hide' | 'initial';
|
||||
|
||||
/**
|
||||
* An object which specifies clipping of the resulting image.
|
||||
*/
|
||||
clip?: {
|
||||
/**
|
||||
* x-coordinate of top-left corner of clip area
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* y-coordinate of top-left corner of clip area
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* width of clipping area
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* height of clipping area
|
||||
*/
|
||||
height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
||||
* `false`.
|
||||
*/
|
||||
fullPage?: boolean;
|
||||
|
||||
/**
|
||||
* Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink
|
||||
* box `#FF00FF` (customized by `maskColor`) that completely covers its bounding box.
|
||||
*/
|
||||
mask?: Array<Locator>;
|
||||
|
||||
/**
|
||||
* Specify the color of the overlay box for masked elements, in
|
||||
* [CSS color format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default color is pink `#FF00FF`.
|
||||
*/
|
||||
maskColor?: string;
|
||||
|
||||
/**
|
||||
* An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is
|
||||
* configurable with `TestConfig.expect`. Unset by default.
|
||||
*/
|
||||
maxDiffPixelRatio?: number;
|
||||
|
||||
/**
|
||||
* An acceptable amount of pixels that could be different. Default is configurable with `TestConfig.expect`. Unset by
|
||||
* default.
|
||||
*/
|
||||
maxDiffPixels?: number;
|
||||
|
||||
/**
|
||||
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
omitBackground?: boolean;
|
||||
|
||||
/**
|
||||
* When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this
|
||||
* will keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel, so
|
||||
* screenshots of high-dpi devices will be twice as large or even larger.
|
||||
*
|
||||
* Defaults to `"css"`.
|
||||
*/
|
||||
scale?: 'css' | 'device';
|
||||
|
||||
/**
|
||||
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
|
||||
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with
|
||||
* `TestConfig.expect`. Defaults to `0.2`.
|
||||
*/
|
||||
threshold?: number;
|
||||
|
||||
/**
|
||||
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||
*/
|
||||
timeout?: number;
|
||||
};
|
||||
20
e2e-tests/playwright/lib/src/ui/components/channels/app_bar.ts
Обычный файл
20
e2e-tests/playwright/lib/src/ui/components/channels/app_bar.ts
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} 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();
|
||||
}
|
||||
}
|
||||
142
e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts
Обычный файл
142
e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts
Обычный файл
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import ChannelsHeader from './header';
|
||||
import ChannelsPostCreate from './post_create';
|
||||
import ChannelsPostEdit from './post_edit';
|
||||
import ChannelsPost from './post';
|
||||
|
||||
import {duration} from '@/util';
|
||||
import {waitUntil} from '@/test_action';
|
||||
|
||||
export default class ChannelsCenterView {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly header;
|
||||
readonly postCreate;
|
||||
readonly scheduledDraftOptions;
|
||||
readonly postBoxIndicator;
|
||||
readonly scheduledDraftChannelIcon;
|
||||
readonly scheduledDraftChannelInfoMessage;
|
||||
readonly scheduledDraftChannelInfoMessageLocator;
|
||||
readonly scheduledDraftChannelInfoMessageText;
|
||||
readonly scheduledDraftSeeAllLink;
|
||||
readonly postEdit;
|
||||
readonly editedPostIcon;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")';
|
||||
this.header = new ChannelsHeader(this.container.locator('.channel-header'));
|
||||
this.postCreate = new ChannelsPostCreate(container.getByTestId('post-create'));
|
||||
this.scheduledDraftOptions = new ChannelsPostCreate(container.locator('#dropdown_send_post_options'));
|
||||
this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
|
||||
this.postBoxIndicator = container.locator('div.postBoxIndicator');
|
||||
this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator');
|
||||
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
|
||||
this.scheduledDraftChannelInfoMessageText = container.locator(this.scheduledDraftChannelInfoMessageLocator);
|
||||
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
|
||||
this.editedPostIcon = (postID: string) => container.locator(`#postEdited_${postID}`);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await this.postCreate.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on "See all scheduled messages"
|
||||
*/
|
||||
async clickOnSeeAllscheduledDrafts() {
|
||||
await this.scheduledDraftSeeAllLink.isVisible();
|
||||
await this.scheduledDraftSeeAllLink.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first post in the Center
|
||||
*/
|
||||
async getFirstPost() {
|
||||
const firstPost = this.container.getByTestId('postView').first();
|
||||
await firstPost.waitFor();
|
||||
return new ChannelsPost(firstPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last post in the Center
|
||||
*/
|
||||
async getLastPost() {
|
||||
const lastPost = this.container.getByTestId('postView').last();
|
||||
await lastPost.waitFor();
|
||||
return new ChannelsPost(lastPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the last post in the Center
|
||||
*/
|
||||
async getLastPostID() {
|
||||
return this.container
|
||||
.getByTestId('postView')
|
||||
.last()
|
||||
.getAttribute('id')
|
||||
.then((id) => (id ? id.split('_')[1] : null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Nth post in the Center from the top
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
async getNthPost(index: number) {
|
||||
const nthPost = this.container.getByTestId('postView').nth(index);
|
||||
await nthPost.waitFor();
|
||||
return new ChannelsPost(nthPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Center post by post's id
|
||||
* @param postId Just the ID without the prefix
|
||||
*/
|
||||
async getPostById(id: string) {
|
||||
const postById = this.container.locator(`[id="post_${id}"]`);
|
||||
await postById.waitFor();
|
||||
return new ChannelsPost(postById);
|
||||
}
|
||||
|
||||
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},
|
||||
);
|
||||
}
|
||||
|
||||
async verifyscheduledDraftChannelInfo() {
|
||||
await this.postBoxIndicator.isVisible();
|
||||
await this.scheduledDraftChannelIcon.isVisible();
|
||||
const messageLocator = this.scheduledDraftChannelInfoMessage.first();
|
||||
await expect(messageLocator).toContainText('Message scheduled for');
|
||||
}
|
||||
|
||||
async clickOnLastEditedPost(postID: string | null) {
|
||||
if (postID) {
|
||||
await this.editedPostIcon(postID).click();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class DeletePostConfirmationDialog {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly cancelButton;
|
||||
readonly confirmButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.cancelButton = container.locator('button.btn.btn-tertiary');
|
||||
this.confirmButton = container.locator('button#deletePostModalButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await expect(this.cancelButton).toBeVisible();
|
||||
await expect(this.confirmButton).toBeVisible();
|
||||
}
|
||||
|
||||
async notToBeVisible() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
await expect(this.cancelButton).not.toBeVisible();
|
||||
await expect(this.confirmButton).not.toBeVisible();
|
||||
}
|
||||
|
||||
async cancelDeletion() {
|
||||
await this.cancelButton.click();
|
||||
}
|
||||
|
||||
async confirmDeletion() {
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class DeletePostModal {
|
||||
readonly container: Locator;
|
||||
readonly confirmButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.confirmButton = container.locator('#deletePostModalButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.confirmButton.waitFor();
|
||||
await this.confirmButton.click();
|
||||
|
||||
// Wait for the modal to disappear
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class EmojiGifPicker {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly gifTab: Locator;
|
||||
readonly gifSearchInput: Locator;
|
||||
readonly gifPickerItems: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.gifTab = container.getByText('GIFs');
|
||||
this.gifSearchInput = container.getByPlaceholder('Search GIPHY');
|
||||
this.gifPickerItems = container.locator('.gif-picker__items');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async openGifTab() {
|
||||
await expect(this.gifTab).toBeVisible();
|
||||
|
||||
await this.gifTab.click({force: true});
|
||||
|
||||
await expect(this.gifSearchInput).toBeVisible();
|
||||
await expect(this.gifPickerItems).toBeVisible();
|
||||
}
|
||||
|
||||
async searchGif(name: string) {
|
||||
await this.gifSearchInput.fill(name);
|
||||
await expect(this.gifSearchInput).toHaveValue(name);
|
||||
}
|
||||
|
||||
async getNthGif(n: number) {
|
||||
await expect(this.gifPickerItems).toBeVisible();
|
||||
|
||||
await this.gifPickerItems.locator('img').nth(n).waitFor();
|
||||
const nthGif = this.gifPickerItems.locator('img').nth(n);
|
||||
await expect(nthGif).toBeVisible();
|
||||
|
||||
const nthGifSrc = await nthGif.getAttribute('src');
|
||||
const nthGifAlt = await nthGif.getAttribute('alt');
|
||||
|
||||
if (!nthGifSrc || !nthGifAlt) {
|
||||
throw new Error('Gif src or alt is empty');
|
||||
}
|
||||
|
||||
return {
|
||||
src: nthGifSrc,
|
||||
alt: nthGifAlt,
|
||||
img: nthGif,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class FindChannelsModal {
|
||||
readonly container: Locator;
|
||||
readonly input;
|
||||
readonly searchList;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.input = container.getByRole('combobox', {name: 'quick switch input'});
|
||||
this.searchList = container.locator('.suggestion-list__item');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* This is the generic confirm modal that is used in the app.
|
||||
* It has optional cancel button, optional checkbox and confirm button along with title and message body.
|
||||
* It can present in different parts of the app such as channel, system console, etc and hence its constructor
|
||||
* should be able to accept the page object of the app and an optional id to uniquely identify the modal.
|
||||
*/
|
||||
export default class GenericConfirmModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly confirmButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.confirmButton = container.locator('#confirmModalButton');
|
||||
this.cancelButton = container.locator('#cancelModalButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.confirmButton.waitFor();
|
||||
await this.confirmButton.click();
|
||||
|
||||
// Wait for the modal to disappear
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.cancelButton.waitFor();
|
||||
await this.cancelButton.click();
|
||||
|
||||
// Wait for the modal to disappear
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
16
e2e-tests/playwright/lib/src/ui/components/channels/header.ts
Обычный файл
16
e2e-tests/playwright/lib/src/ui/components/channels/header.ts
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelsHeader {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class MessagePriority {
|
||||
readonly container: Locator;
|
||||
readonly priorityIcon: Locator;
|
||||
readonly priorityMenu: Locator;
|
||||
readonly standardPriorityOption: Locator;
|
||||
readonly priorityDialog: Locator;
|
||||
readonly dialogHeader: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
// Formatting bar priority icon
|
||||
this.priorityIcon = container.locator('#messagePriority');
|
||||
|
||||
// Priority menu that opens when clicking the icon
|
||||
this.priorityMenu = container.locator('[role="menu"]');
|
||||
|
||||
// Standard priority option in the menu (id comes from webapp implementation)
|
||||
this.standardPriorityOption = this.priorityMenu.locator('#menu-item-priority-standard');
|
||||
|
||||
// Priority dialog elements
|
||||
this.priorityDialog = container.page().getByRole('menu');
|
||||
this.dialogHeader = container.page().locator('h4.modal-title');
|
||||
}
|
||||
|
||||
async clickPriorityIcon() {
|
||||
await this.priorityIcon.waitFor({state: 'visible'});
|
||||
await this.priorityIcon.click();
|
||||
}
|
||||
|
||||
async verifyPriorityIconVisible() {
|
||||
await this.priorityIcon.waitFor({state: 'visible'});
|
||||
await expect(this.priorityIcon).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyStandardPrioritySelected() {
|
||||
await expect(this.priorityMenu).toBeVisible();
|
||||
await expect(this.standardPriorityOption).toHaveAttribute('aria-checked', 'true');
|
||||
}
|
||||
|
||||
async verifyPriorityMenuVisible() {
|
||||
await expect(this.priorityMenu).toBeVisible();
|
||||
// Look for beta text in header
|
||||
await expect(this.priorityMenu.locator('text=Message Priority')).toBeVisible();
|
||||
}
|
||||
|
||||
async closePriorityMenu() {
|
||||
await this.priorityMenu.press('Escape');
|
||||
await expect(this.priorityMenu).not.toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNoPriorityLabel(postText: string) {
|
||||
const post = this.container.locator(`text=${postText}`);
|
||||
await expect(post).toBeVisible();
|
||||
|
||||
// Verify no priority label exists
|
||||
const priorityLabel = post.locator('[data-testid="post-priority-label"]');
|
||||
await expect(priorityLabel).toHaveCount(0);
|
||||
}
|
||||
|
||||
async verifyPriorityDialog() {
|
||||
await expect(this.priorityDialog).toBeVisible();
|
||||
await expect(this.dialogHeader).toHaveText('Message priority');
|
||||
}
|
||||
|
||||
async verifyStandardOptionSelected() {
|
||||
const standardOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
|
||||
await expect(standardOption).toBeVisible();
|
||||
await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
|
||||
}
|
||||
}
|
||||
75
e2e-tests/playwright/lib/src/ui/components/channels/post.ts
Обычный файл
75
e2e-tests/playwright/lib/src/ui/components/channels/post.ts
Обычный файл
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import PostMenu from './post_menu';
|
||||
import ThreadFooter from './thread_footer';
|
||||
|
||||
export default class ChannelsPost {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly body;
|
||||
readonly profileIcon;
|
||||
|
||||
readonly removePostButton;
|
||||
|
||||
readonly postMenu;
|
||||
readonly threadFooter;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.body = container.locator('.post__body');
|
||||
|
||||
this.profileIcon = container.locator('.profile-icon');
|
||||
|
||||
this.removePostButton = container.locator('.post__remove');
|
||||
|
||||
this.postMenu = new PostMenu(container.locator('.post-menu'));
|
||||
this.threadFooter = new ThreadFooter(container.locator('.ThreadFooter'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over the post. Can be used for post menu to appear.
|
||||
*/
|
||||
async hover() {
|
||||
await this.container.hover();
|
||||
}
|
||||
|
||||
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 this.profileIcon.getByAltText(`${username} profile image`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the deleted post's remove 'x' button.
|
||||
* Also verifies that the post is a deleted post.
|
||||
*/
|
||||
async remove() {
|
||||
// Verify the post is a deleted post
|
||||
await expect(this.container).toContainText(/\(message deleted\)/);
|
||||
|
||||
// Hover over the post and click on the remove post button
|
||||
await this.container.hover();
|
||||
await this.removePostButton.waitFor();
|
||||
await this.removePostButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* `toContainText` verifies if the post contains the specified text.
|
||||
* @param text Text to be verified in the post
|
||||
*/
|
||||
async toContainText(text: string) {
|
||||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
}
|
||||
143
e2e-tests/playwright/lib/src/ui/components/channels/post_create.ts
Обычный файл
143
e2e-tests/playwright/lib/src/ui/components/channels/post_create.ts
Обычный файл
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import {duration} from '@/util';
|
||||
import {assetPath} from '@/file';
|
||||
import {waitUntil} from '@/test_action';
|
||||
|
||||
export default class ChannelsPostCreate {
|
||||
readonly container: Locator;
|
||||
readonly input;
|
||||
|
||||
readonly attachmentButton;
|
||||
readonly emojiButton;
|
||||
readonly sendMessageButton;
|
||||
readonly scheduleDraftMessageButton;
|
||||
readonly priorityButton;
|
||||
readonly suggestionList;
|
||||
readonly filePreview;
|
||||
|
||||
constructor(container: Locator, isRHS = false) {
|
||||
this.container = container;
|
||||
|
||||
if (!isRHS) {
|
||||
this.input = container.getByTestId('post_textbox');
|
||||
} else {
|
||||
this.input = container.getByTestId('reply_textbox');
|
||||
}
|
||||
|
||||
this.attachmentButton = container.locator('#fileUploadButton');
|
||||
this.emojiButton = container.getByLabel('select an emoji');
|
||||
this.sendMessageButton = container.getByTestId('SendMessageButton');
|
||||
this.scheduleDraftMessageButton = container.getByLabel('Schedule message');
|
||||
this.priorityButton = container.getByLabel('Message priority');
|
||||
this.suggestionList = container.getByTestId('suggestionList');
|
||||
this.filePreview = container.locator('.file-preview__container');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
|
||||
await this.input.waitFor();
|
||||
await expect(this.input).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* It just writes the message in the input and doesn't send it
|
||||
* @param message : Message to be written in the input
|
||||
*/
|
||||
async writeMessage(message: string) {
|
||||
await this.input.waitFor();
|
||||
await expect(this.input).toBeVisible();
|
||||
|
||||
await this.input.fill(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the message input
|
||||
*/
|
||||
async getInputValue() {
|
||||
await expect(this.input).toBeVisible();
|
||||
return await this.input.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message already written in the input
|
||||
*/
|
||||
async sendMessage() {
|
||||
await expect(this.input).toBeVisible();
|
||||
const messageInputValue = await this.getInputValue();
|
||||
expect(messageInputValue).not.toBe('');
|
||||
|
||||
await expect(this.sendMessageButton).toBeVisible();
|
||||
await expect(this.sendMessageButton).toBeEnabled();
|
||||
|
||||
await this.sendMessageButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on Scheduled Draft button to open options
|
||||
*/
|
||||
async clickOnScheduleDraftDropdownButton() {
|
||||
await expect(this.input).toBeVisible();
|
||||
|
||||
await expect(this.scheduleDraftMessageButton).toBeVisible();
|
||||
await expect(this.scheduleDraftMessageButton).toBeEnabled();
|
||||
|
||||
await this.scheduleDraftMessageButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the message priority menu
|
||||
*/
|
||||
async openPriorityMenu() {
|
||||
await expect(this.priorityButton).toBeVisible();
|
||||
await expect(this.priorityButton).toBeEnabled();
|
||||
await this.priorityButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes and sends a message
|
||||
*/
|
||||
async postMessage(message: string, files?: string[]) {
|
||||
await this.writeMessage(message);
|
||||
|
||||
if (files) {
|
||||
const filePaths = files.map((file) => path.join(assetPath, file));
|
||||
this.container.page().once('filechooser', async (fileChooser) => {
|
||||
await fileChooser.setFiles(filePaths);
|
||||
});
|
||||
|
||||
// Click on the attachment button
|
||||
await this.attachmentButton.click();
|
||||
|
||||
// Wait until the file preview is displayed
|
||||
await this.waitUntilFilePreviewContains(files);
|
||||
}
|
||||
|
||||
await this.sendMessage();
|
||||
}
|
||||
|
||||
async openEmojiPicker() {
|
||||
await expect(this.emojiButton).toBeVisible();
|
||||
await this.emojiButton.click();
|
||||
}
|
||||
|
||||
async waitUntilFilePreviewContains(files: string[], timeout = duration.ten_sec) {
|
||||
await waitUntil(
|
||||
async () => {
|
||||
const previews = this.filePreview.locator('.file-preview');
|
||||
const details = this.filePreview.locator('.post-image__details');
|
||||
|
||||
const [previewsCount, detailsCount] = await Promise.all([previews.count(), details.count()]);
|
||||
|
||||
return previewsCount === files.length && detailsCount === files.length;
|
||||
},
|
||||
{timeout},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostDotMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly replyMenuItem;
|
||||
readonly forwardMenuItem;
|
||||
readonly followMessageMenuItem;
|
||||
readonly markAsUnreadMenuItem;
|
||||
readonly remindMenuItem;
|
||||
readonly saveMenuItem;
|
||||
readonly removeFromSavedMenuItem;
|
||||
readonly pinToChannelMenuItem;
|
||||
readonly unpinFromChannelMenuItem;
|
||||
readonly moveThreadMenuItem;
|
||||
readonly copyLinkMenuItem;
|
||||
readonly editMenuItem;
|
||||
readonly copyTextMenuItem;
|
||||
readonly deleteMenuItem;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText});
|
||||
|
||||
this.replyMenuItem = getMenuItem('Reply');
|
||||
this.forwardMenuItem = getMenuItem('Forward');
|
||||
this.followMessageMenuItem = getMenuItem('Follow message');
|
||||
this.markAsUnreadMenuItem = getMenuItem('Mark as Unread');
|
||||
this.remindMenuItem = getMenuItem('Remind');
|
||||
this.saveMenuItem = getMenuItem('Save');
|
||||
this.removeFromSavedMenuItem = getMenuItem('Remove from Saved');
|
||||
this.pinToChannelMenuItem = getMenuItem('Pin to Channel');
|
||||
this.unpinFromChannelMenuItem = getMenuItem('Unpin from Channel');
|
||||
this.moveThreadMenuItem = getMenuItem('Move Thread');
|
||||
this.copyLinkMenuItem = getMenuItem('Copy Link');
|
||||
this.editMenuItem = getMenuItem('Edit');
|
||||
this.copyTextMenuItem = getMenuItem('Copy Text');
|
||||
this.deleteMenuItem = getMenuItem('Delete');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
95
e2e-tests/playwright/lib/src/ui/components/channels/post_edit.ts
Обычный файл
95
e2e-tests/playwright/lib/src/ui/components/channels/post_edit.ts
Обычный файл
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import DeletePostConfirmationDialog from './delete_post_confirmation_dialog';
|
||||
import RestorePostConfirmationDialog from './restore_post_confirmation_dialog';
|
||||
|
||||
import {assetPath} from '@/file';
|
||||
|
||||
export default class ChannelsPostEdit {
|
||||
readonly container: Locator;
|
||||
readonly input;
|
||||
|
||||
readonly attachmentButton;
|
||||
readonly emojiButton;
|
||||
readonly sendMessageButton;
|
||||
readonly deleteConfirmationDialog;
|
||||
readonly restorePostConfirmationDialog;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.input = container.getByTestId('edit_textbox');
|
||||
|
||||
this.attachmentButton = container.locator('#fileUploadButton');
|
||||
this.emojiButton = container.getByLabel('select an emoji');
|
||||
this.sendMessageButton = container.locator('.save');
|
||||
this.deleteConfirmationDialog = new DeletePostConfirmationDialog(container.page().locator('#deletePostModal'));
|
||||
this.restorePostConfirmationDialog = new RestorePostConfirmationDialog(
|
||||
container.page().locator('#restorePostModal'),
|
||||
);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
|
||||
await this.input.waitFor();
|
||||
await expect(this.input).toBeVisible();
|
||||
}
|
||||
|
||||
async toNotBeVisible() {
|
||||
await expect(this.input).not.toBeVisible();
|
||||
}
|
||||
|
||||
async writeMessage(message: string) {
|
||||
await this.input.waitFor();
|
||||
await expect(this.input).toBeVisible();
|
||||
|
||||
await this.input.clear();
|
||||
await this.input.fill(message);
|
||||
}
|
||||
|
||||
async addFiles(files: string[]) {
|
||||
const filePaths = files.map((file) => path.join(assetPath, file));
|
||||
this.container.page().once('filechooser', async (fileChooser) => {
|
||||
await fileChooser.setFiles(filePaths);
|
||||
});
|
||||
|
||||
await this.attachmentButton.click();
|
||||
}
|
||||
|
||||
async removeFile(fileName: string) {
|
||||
const files = await this.container.locator(`.file-preview`).all();
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const textContent = await files[i].textContent();
|
||||
if (textContent?.includes(fileName)) {
|
||||
const removeButton = files[i].locator('.icon-close');
|
||||
await removeButton.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
await this.input.scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(this.sendMessageButton).toBeVisible();
|
||||
await expect(this.sendMessageButton).toBeEnabled();
|
||||
|
||||
await this.sendMessageButton.click();
|
||||
}
|
||||
|
||||
async postMessage(message: string) {
|
||||
await this.writeMessage(message);
|
||||
await this.sendMessage();
|
||||
}
|
||||
|
||||
async toContainText(text: string) {
|
||||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
}
|
||||
57
e2e-tests/playwright/lib/src/ui/components/channels/post_menu.ts
Обычный файл
57
e2e-tests/playwright/lib/src/ui/components/channels/post_menu.ts
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly plusOneEmojiButton;
|
||||
readonly grinningEmojiButton;
|
||||
readonly whiteCheckMarkEmojiButton;
|
||||
readonly addReactionButton;
|
||||
readonly saveButton;
|
||||
readonly replyButton;
|
||||
readonly actionsButton;
|
||||
readonly dotMenuButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.plusOneEmojiButton = container.getByRole('button', {name: '+1 emoji'});
|
||||
this.grinningEmojiButton = container.getByRole('button', {name: 'grinning emoji'});
|
||||
this.whiteCheckMarkEmojiButton = container.getByRole('button', {name: 'white check mark emoji'});
|
||||
this.addReactionButton = container.getByRole('button', {name: 'add reaction'});
|
||||
this.saveButton = container.getByRole('button', {name: 'save'});
|
||||
this.actionsButton = container.getByRole('button', {name: 'actions'});
|
||||
this.replyButton = container.getByRole('button', {name: 'reply'});
|
||||
this.dotMenuButton = container.getByRole('button', {name: 'more'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the reply button from the post menu.
|
||||
*/
|
||||
async reply() {
|
||||
await this.replyButton.waitFor();
|
||||
await this.replyButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the dot menu button from the post menu.
|
||||
*/
|
||||
async openDotMenu() {
|
||||
await this.dotMenuButton.waitFor();
|
||||
await this.dotMenuButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on dot menu button.
|
||||
*/
|
||||
async clickOnDotMenu() {
|
||||
await this.dotMenuButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostReminderMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly thirtyMinsMenuItem;
|
||||
readonly oneHourMenuItem;
|
||||
readonly twoHoursMenuItem;
|
||||
readonly tomorrowMenuItem;
|
||||
readonly customMenuItem;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText});
|
||||
|
||||
this.thirtyMinsMenuItem = getMenuItem('30 mins');
|
||||
this.oneHourMenuItem = getMenuItem('1 hour');
|
||||
this.twoHoursMenuItem = getMenuItem('2 hours');
|
||||
this.tomorrowMenuItem = getMenuItem('Tomorrow');
|
||||
this.customMenuItem = getMenuItem('Custom');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class RestorePostConfirmationDialog {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly cancelButton;
|
||||
readonly confirmButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.cancelButton = container.locator('button.btn.btn-tertiary');
|
||||
this.confirmButton = container.locator('button.GenericModal__button.btn.btn-primary.confirm');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await expect(this.cancelButton).toBeVisible();
|
||||
await expect(this.confirmButton).toBeVisible();
|
||||
}
|
||||
|
||||
async notToBeVisible() {
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async confirmRestore() {
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly scheduleDraftMessageCustomTimeOption;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.scheduleDraftMessageCustomTimeOption = container.getByText('Choose a custom time');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async selectCustomTime() {
|
||||
await this.scheduleDraftMessageCustomTimeOption.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly confirmButton;
|
||||
readonly dateInput;
|
||||
readonly timeLocator;
|
||||
readonly timeDropdownOptions;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.confirmButton = container.locator('button.confirm');
|
||||
this.dateInput = container.locator('div.Input_wrapper');
|
||||
this.timeLocator = container.locator('div.dateTime__input');
|
||||
this.timeDropdownOptions = container.locator('ul.dropdown-menu .MenuItem');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
getDaySuffix(day: number): string {
|
||||
if (day > 3 && day < 21) return 'th';
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
return 'st';
|
||||
case 2:
|
||||
return 'nd';
|
||||
case 3:
|
||||
return 'rd';
|
||||
default:
|
||||
return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
dateLocator(day: number, month: string, dayOfWeek: string) {
|
||||
const daySuffix = this.getDaySuffix(day);
|
||||
return this.container.locator(`button[aria-label*='${day}${daySuffix} ${month} (${dayOfWeek})']`);
|
||||
}
|
||||
|
||||
async selectDay(dayFromToday: number = 0) {
|
||||
await this.dateInput.click();
|
||||
|
||||
const pacificDate = this.getPacificDate();
|
||||
const originDate = new Date(pacificDate.getTime());
|
||||
|
||||
// If dayFromToday is provided, add days to the current date
|
||||
if (dayFromToday) {
|
||||
pacificDate.setDate(pacificDate.getDate() + dayFromToday);
|
||||
}
|
||||
|
||||
const day = pacificDate.getDate();
|
||||
const month = pacificDate.toLocaleString('default', {month: 'long'});
|
||||
const dayOfWeek = pacificDate.toLocaleDateString('en-US', {weekday: 'long'});
|
||||
|
||||
const dl = this.dateLocator(day, month, dayOfWeek);
|
||||
|
||||
// If the date is not visible and the month has changed, click the next month button
|
||||
if (!(await dl.isVisible()) && pacificDate.getMonth() !== originDate.getMonth()) {
|
||||
this.container.locator('button[aria-label="Go to next month"]').click();
|
||||
}
|
||||
await dl.click();
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.confirmButton.isVisible();
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selecting the First time option from the dropdown for
|
||||
* scheduled_post_job to send the drafts out
|
||||
*/
|
||||
async selectTime() {
|
||||
await this.timeLocator.click();
|
||||
const timeButton = this.timeDropdownOptions.nth(2);
|
||||
await expect(timeButton).toBeVisible();
|
||||
await timeButton.click();
|
||||
}
|
||||
|
||||
getPacificDate(): Date {
|
||||
const currentDate = new Date();
|
||||
// Convert the current date to Pacific Time
|
||||
const utcTime = currentDate.getTime() + currentDate.getTimezoneOffset() * 60000;
|
||||
const pacificOffset = -7 * 60; // Pacific Daylight Time (UTC-07:00)
|
||||
const pacificTime = new Date(utcTime + pacificOffset * 60000);
|
||||
|
||||
return pacificTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SearchPopover {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly messagesButton;
|
||||
readonly filesButton;
|
||||
readonly searchInput;
|
||||
readonly searchBoxClose;
|
||||
readonly selectedSuggestion;
|
||||
readonly searchHints;
|
||||
readonly clearButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.messagesButton = container.getByRole('button', {name: 'Messages'});
|
||||
this.filesButton = container.getByRole('button', {name: 'Files'});
|
||||
this.searchInput = container.getByLabel('Search messages');
|
||||
this.searchBoxClose = container.getByTestId('searchBoxClose');
|
||||
this.selectedSuggestion = container.locator('.suggestion--selected').locator('.suggestion-list__main');
|
||||
this.searchHints = container.locator('#searchHints');
|
||||
this.clearButton = container.locator('.input-clear-x');
|
||||
}
|
||||
|
||||
// clearIfPossible clears the search input if the clear button is visible. Returns true if the clear button was clicked.
|
||||
async clearIfPossible() {
|
||||
if (await this.clearButton.isVisible()) {
|
||||
await this.clearButton.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
getSelectedSuggestion() {
|
||||
return this.searchHints.locator('.suggestion--selected');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification';
|
||||
|
||||
export default class NotificationsSettings {
|
||||
readonly container: Locator;
|
||||
readonly keysWithHighlightDesc: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.keysWithHighlightDesc = container.locator('#keywordsAndHighlightDesc');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async expandSection(section: NotificationSettingsSection) {
|
||||
if (section === 'keysWithHighlight') {
|
||||
await this.container.getByText('Keywords That Get Highlighted (without notifications)').click();
|
||||
await this.verifySectionIsExpanded('keysWithHighlight');
|
||||
}
|
||||
}
|
||||
|
||||
async verifySectionIsExpanded(section: NotificationSettingsSection) {
|
||||
await expect(this.container.locator(`#${section}Edit`)).not.toBeVisible();
|
||||
|
||||
if (section === 'keysWithHighlight') {
|
||||
await expect(
|
||||
this.container.getByText(
|
||||
'Enter non case-sensitive keywords, press Tab or use commas to separate them:',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
this.container.getByText(
|
||||
'These keywords will be shown to you with a highlight when anyone sends a message that includes them.',
|
||||
),
|
||||
).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async getKeywordsInput() {
|
||||
await expect(this.container.locator('input')).toBeVisible();
|
||||
return this.container.locator('input');
|
||||
}
|
||||
|
||||
async save() {
|
||||
await expect(this.container.getByText('Save')).toBeVisible();
|
||||
await this.container.getByText('Save').click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import NotificationsSettings from './notification_settings';
|
||||
|
||||
export default class SettingsModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly notificationsSettingsTab;
|
||||
readonly notificationsSettings;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.notificationsSettingsTab = container.locator('#notificationsButton');
|
||||
this.notificationsSettings = new NotificationsSettings(container.locator('#notificationsSettings'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async openNotificationsTab() {
|
||||
await expect(this.notificationsSettingsTab).toBeVisible();
|
||||
await this.notificationsSettingsTab.click();
|
||||
|
||||
await this.notificationsSettings.toBeVisible();
|
||||
}
|
||||
|
||||
async closeModal() {
|
||||
await this.container.getByLabel('Close').click();
|
||||
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelsSidebarLeft {
|
||||
readonly container: Locator;
|
||||
readonly findChannelButton;
|
||||
readonly scheduledDraftCountonLHS;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.findChannelButton = container.getByRole('button', {name: 'Find Channels'});
|
||||
this.scheduledDraftCountonLHS = container.locator('span.scheduledPostBadge');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async assertNoPendingScheduledDraft() {
|
||||
await expect(this.scheduledDraftCountonLHS).not.toBeVisible();
|
||||
}
|
||||
|
||||
async assertscheduledDraftCountLHS(count: string) {
|
||||
await expect(this.scheduledDraftCountonLHS).toBeVisible();
|
||||
await expect(this.scheduledDraftCountonLHS).toHaveText(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the sidebar channel link with the given name.
|
||||
* It can be any sidebar item name including channels, direct messages, or group messages, threads, etc.
|
||||
* @param channelName
|
||||
*/
|
||||
async goToItem(channelName: string) {
|
||||
const channel = this.container.locator(`#sidebarItem_${channelName}`);
|
||||
await channel.waitFor();
|
||||
await channel.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies 'Drafts' as a sidebar link exists in LHS.
|
||||
*/
|
||||
async draftsVisible() {
|
||||
const draftSidebarLink = this.container.getByText('Drafts', {exact: true});
|
||||
await draftSidebarLink.waitFor();
|
||||
await expect(draftSidebarLink).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies 'Drafts' as a sidebar link does not exist in LHS.
|
||||
*/
|
||||
async draftsNotVisible() {
|
||||
const channel = this.container.getByText('Drafts', {exact: true});
|
||||
await expect(channel).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
100
e2e-tests/playwright/lib/src/ui/components/channels/sidebar_right.ts
Обычный файл
100
e2e-tests/playwright/lib/src/ui/components/channels/sidebar_right.ts
Обычный файл
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import ChannelsPostCreate from './post_create';
|
||||
import ChannelsPostEdit from './post_edit';
|
||||
import ChannelsPost from './post';
|
||||
|
||||
export default class ChannelsSidebarRight {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly closeButton;
|
||||
readonly postCreate;
|
||||
readonly rhsPostBody;
|
||||
readonly postBoxIndicator;
|
||||
readonly scheduledDraftChannelInfoMessage;
|
||||
readonly scheduledDraftSeeAllLink;
|
||||
readonly scheduledDraftChannelInfoMessageText;
|
||||
readonly editTextbox;
|
||||
readonly postEdit;
|
||||
readonly currentVersionEditedPosttext;
|
||||
readonly restorePreviousPostVersionIcon;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.postBoxIndicator = container.locator('div.postBoxIndicator');
|
||||
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
|
||||
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
|
||||
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
|
||||
this.rhsPostBody = container.locator('.post-message__text');
|
||||
this.postCreate = new ChannelsPostCreate(container.getByTestId('comment-create'), true);
|
||||
this.closeButton = container.locator('.sidebar--right__close');
|
||||
|
||||
this.editTextbox = container.locator('#edit_textbox');
|
||||
this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
|
||||
this.currentVersionEditedPosttext = (postID: any) => container.locator(`#rhsPostMessageText_${postID} p`);
|
||||
this.restorePreviousPostVersionIcon = container.locator(
|
||||
'button[aria-label="Select to restore an old message."]',
|
||||
);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RHS post by post id
|
||||
* @param postId Just the ID without the prefix
|
||||
*/
|
||||
async getPostById(postId: string) {
|
||||
const post = this.container.locator(`[id="rhsPost_${postId}"]`);
|
||||
await post.waitFor();
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last post in the RHS
|
||||
*/
|
||||
async getLastPost() {
|
||||
const post = this.container.getByTestId('rhsPostView').last();
|
||||
await post.waitFor();
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
async getFirstPost() {
|
||||
const post = this.container.getByTestId('rhsPostView').first();
|
||||
await post.waitFor();
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the RHS
|
||||
*/
|
||||
async close() {
|
||||
await this.closeButton.waitFor();
|
||||
await this.closeButton.click();
|
||||
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
|
||||
async clickOnSeeAllscheduledDrafts() {
|
||||
await this.scheduledDraftSeeAllLink.isVisible();
|
||||
await this.scheduledDraftSeeAllLink.click();
|
||||
}
|
||||
|
||||
async toContainText(text: string) {
|
||||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
|
||||
async verifyCurrentVersionPostMessage(postID: string | null, postMessageContent: string) {
|
||||
expect(await this.currentVersionEditedPosttext(postID).textContent()).toBe(postMessageContent);
|
||||
}
|
||||
|
||||
async restorePreviousPostVersion() {
|
||||
await this.restorePreviousPostVersionIcon.isVisible();
|
||||
await this.restorePreviousPostVersionIcon.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ThreadFooter {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly replyButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.replyButton = container.locator('.ReplyButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the reply button in the thread footer to open the thread in RHS.
|
||||
*/
|
||||
async reply() {
|
||||
await this.replyButton.waitFor();
|
||||
await this.replyButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class UserProfilePopover {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.container.getByLabel('Close user profile popover').click();
|
||||
}
|
||||
}
|
||||
28
e2e-tests/playwright/lib/src/ui/components/footer.ts
Обычный файл
28
e2e-tests/playwright/lib/src/ui/components/footer.ts
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class Footer {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly copyright;
|
||||
readonly aboutLink;
|
||||
readonly privacyPolicyLink;
|
||||
readonly termsLink;
|
||||
readonly helpLink;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.copyright = container.locator('.footer-copyright');
|
||||
this.aboutLink = container.locator('text=About');
|
||||
this.privacyPolicyLink = container.locator('text=Privacy Policy');
|
||||
this.termsLink = container.locator('text=Terms');
|
||||
this.helpLink = container.locator('text=Help');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.copyright).toBeVisible();
|
||||
}
|
||||
}
|
||||
51
e2e-tests/playwright/lib/src/ui/components/global_header.ts
Обычный файл
51
e2e-tests/playwright/lib/src/ui/components/global_header.ts
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class GlobalHeader {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly productSwitchMenu;
|
||||
readonly recentMentionsButton;
|
||||
readonly settingsButton;
|
||||
readonly searchBox;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
|
||||
this.recentMentionsButton = container.getByRole('button', {name: 'Recent mentions'});
|
||||
this.settingsButton = container.getByRole('button', {name: 'Settings'});
|
||||
this.searchBox = container.locator('#searchFormContainer');
|
||||
}
|
||||
|
||||
async toBeVisible(name: string) {
|
||||
await expect(this.container.getByRole('heading', {name})).toBeVisible();
|
||||
}
|
||||
|
||||
async switchProduct(name: string) {
|
||||
await this.productSwitchMenu.click();
|
||||
await this.container.getByRole('link', {name}).click();
|
||||
}
|
||||
|
||||
async openSettings() {
|
||||
await expect(this.settingsButton).toBeVisible();
|
||||
await this.settingsButton.click();
|
||||
}
|
||||
|
||||
async openRecentMentions() {
|
||||
await expect(this.recentMentionsButton).toBeVisible();
|
||||
await this.recentMentionsButton.click();
|
||||
}
|
||||
|
||||
async openSearch() {
|
||||
await expect(this.searchBox).toBeVisible();
|
||||
await this.searchBox.click();
|
||||
}
|
||||
|
||||
async closeSearch() {
|
||||
await expect(this.searchBox).toBeVisible();
|
||||
await this.searchBox.getByTestId('searchBoxClose').click();
|
||||
}
|
||||
}
|
||||
96
e2e-tests/playwright/lib/src/ui/components/index.ts
Обычный файл
96
e2e-tests/playwright/lib/src/ui/components/index.ts
Обычный файл
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ChannelsHeader from './channels/header';
|
||||
import ChannelsAppBar from './channels/app_bar';
|
||||
import ChannelsPostCreate from './channels/post_create';
|
||||
import ChannelsPost from './channels/post';
|
||||
import ChannelsCenterView from './channels/center_view';
|
||||
import ChannelsSidebarLeft from './channels/sidebar_left';
|
||||
import ChannelsSidebarRight from './channels/sidebar_right';
|
||||
import DeletePostModal from './channels/delete_post_modal';
|
||||
import FindChannelsModal from './channels/find_channels_modal';
|
||||
import SettingsModal from './channels/settings/settings_modal';
|
||||
import Footer from './footer';
|
||||
import GlobalHeader from './global_header';
|
||||
import SearchPopover from './channels/search_popover';
|
||||
import MainHeader from './main_header';
|
||||
import PostDotMenu from './channels/post_dot_menu';
|
||||
import PostReminderMenu from './channels/post_reminder_menu';
|
||||
import PostMenu from './channels/post_menu';
|
||||
import ThreadFooter from './channels/thread_footer';
|
||||
import EmojiGifPicker from './channels/emoji_gif_picker';
|
||||
import GenericConfirmModal from './channels/generic_confirm_modal';
|
||||
import MessagePriority from './channels/message_priority';
|
||||
import ScheduledDraftMenu from './channels/scheduled_draft_menu';
|
||||
import ScheduledDraftModal from './channels/scheduled_draft_modal';
|
||||
import UserProfilePopover from './channels/user_profile_popover';
|
||||
import SystemConsoleSidebar from './system_console/sidebar';
|
||||
import SystemConsoleNavbar from './system_console/navbar';
|
||||
import SystemUsers from './system_console/sections/system_users/system_users';
|
||||
import SystemUsersFilterPopover from './system_console/sections/system_users/filter_popover';
|
||||
import SystemUsersFilterMenu from './system_console/sections/system_users/filter_menu';
|
||||
import SystemUsersColumnToggleMenu from './system_console/sections/system_users/column_toggle_menu';
|
||||
import ChannelsPostEdit from './channels/post_edit';
|
||||
import DeletePostConfirmationDialog from './channels/delete_post_confirmation_dialog';
|
||||
import RestorePostConfirmationDialog from './channels/restore_post_confirmation_dialog';
|
||||
import SystemConsoleFeatureDiscovery from './system_console/sections/system_users/feature_discovery';
|
||||
import SystemConsoleMobileSecurity from './system_console/sections/system_users/mobile_security';
|
||||
|
||||
const components = {
|
||||
GlobalHeader,
|
||||
SearchPopover,
|
||||
ChannelsCenterView,
|
||||
ChannelsSidebarLeft,
|
||||
ChannelsSidebarRight,
|
||||
ChannelsAppBar,
|
||||
ChannelsHeader,
|
||||
ChannelsPostCreate,
|
||||
ChannelsPostEdit,
|
||||
ChannelsPost,
|
||||
FindChannelsModal,
|
||||
DeletePostModal,
|
||||
SettingsModal,
|
||||
PostDotMenu,
|
||||
PostMenu,
|
||||
ThreadFooter,
|
||||
Footer,
|
||||
MainHeader,
|
||||
PostReminderMenu,
|
||||
EmojiGifPicker,
|
||||
GenericConfirmModal,
|
||||
ScheduledDraftMenu,
|
||||
ScheduledDraftModal,
|
||||
SystemConsoleSidebar,
|
||||
SystemConsoleNavbar,
|
||||
SystemUsers,
|
||||
SystemUsersFilterPopover,
|
||||
SystemUsersFilterMenu,
|
||||
SystemUsersColumnToggleMenu,
|
||||
SystemConsoleFeatureDiscovery,
|
||||
SystemConsoleMobileSecurity,
|
||||
MessagePriority,
|
||||
UserProfilePopover,
|
||||
DeletePostConfirmationDialog,
|
||||
RestorePostConfirmationDialog,
|
||||
};
|
||||
|
||||
export {
|
||||
components,
|
||||
GlobalHeader,
|
||||
ChannelsCenterView,
|
||||
ChannelsSidebarLeft,
|
||||
ChannelsSidebarRight,
|
||||
ChannelsAppBar,
|
||||
ChannelsHeader,
|
||||
ChannelsPostCreate,
|
||||
ChannelsPostEdit,
|
||||
ChannelsPost,
|
||||
FindChannelsModal,
|
||||
DeletePostModal,
|
||||
PostDotMenu,
|
||||
PostMenu,
|
||||
ThreadFooter,
|
||||
MessagePriority,
|
||||
DeletePostConfirmationDialog,
|
||||
};
|
||||
22
e2e-tests/playwright/lib/src/ui/components/main_header.ts
Обычный файл
22
e2e-tests/playwright/lib/src/ui/components/main_header.ts
Обычный файл
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class MainHeader {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly logo;
|
||||
readonly backButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.logo = container.locator('.header-logo-link');
|
||||
this.backButton = container.getByTestId('back_button');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemConsoleNavbar {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemUsersColumnToggleMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locator for the menu item with the given name.
|
||||
*/
|
||||
async getMenuItem(menuItem: string) {
|
||||
const menuItemLocator = this.container.getByRole('menuitemcheckbox').filter({hasText: menuItem});
|
||||
await menuItemLocator.waitFor();
|
||||
|
||||
return menuItemLocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of locators for all the menu items.
|
||||
*/
|
||||
async getAllMenuItems() {
|
||||
const menuItemLocators = this.container.getByRole('menuitemcheckbox');
|
||||
return menuItemLocators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in the item name to check/uncheck the menu item.
|
||||
*/
|
||||
async clickMenuItem(menuItem: string) {
|
||||
const menuItemLocator = await this.getMenuItem(menuItem);
|
||||
await menuItemLocator.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close column toggle menu.
|
||||
*/
|
||||
async close() {
|
||||
await this.container.press('Escape');
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> Feature Discovery
|
||||
*/
|
||||
export default class FeatureDiscovery {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toHaveTitle(title: string) {
|
||||
await expect(this.container.getByTestId('featureDiscovery_title')).toHaveText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* The dropdown menu which appears for both Role and Status filter.
|
||||
*/
|
||||
export default class SystemUsersFilterMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locator for the menu item with the given name.
|
||||
*/
|
||||
async getMenuItem(menuItem: string) {
|
||||
const menuItemLocator = this.container.getByText(menuItem);
|
||||
await menuItemLocator.waitFor();
|
||||
|
||||
return menuItemLocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the menu item with the given name.
|
||||
*/
|
||||
async clickMenuItem(menuItem: string) {
|
||||
const menuItemLocator = await this.getMenuItem(menuItem);
|
||||
await menuItemLocator.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the menu.
|
||||
*/
|
||||
async close() {
|
||||
await this.container.press('Escape');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemUsersFilterPopover {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly teamMenuInput: Locator;
|
||||
readonly roleMenuButton: Locator;
|
||||
readonly statusMenuButton: Locator;
|
||||
|
||||
readonly applyButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.teamMenuInput = this.container.locator('#asyncTeamSelectInput');
|
||||
this.roleMenuButton = this.container.locator('#DropdownInput_filterRole');
|
||||
this.statusMenuButton = this.container.locator('#DropdownInput_filterStatus');
|
||||
|
||||
this.applyButton = this.container.getByText('Apply');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await expect(this.applyButton).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the filter settings.
|
||||
*/
|
||||
async save() {
|
||||
await this.applyButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to type in the team filter for searching.
|
||||
*/
|
||||
async searchInTeamMenu(teamDisplayName: string) {
|
||||
expect(this.teamMenuInput).toBeVisible();
|
||||
await this.teamMenuInput.fill(teamDisplayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the role filter menu.
|
||||
*/
|
||||
async openRoleMenu() {
|
||||
expect(this.roleMenuButton).toBeVisible();
|
||||
await this.roleMenuButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the status filter menu.
|
||||
*/
|
||||
async openStatusMenu() {
|
||||
expect(this.statusMenuButton).toBeVisible();
|
||||
await this.statusMenuButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the filter popover.
|
||||
*/
|
||||
async close() {
|
||||
await this.container.press('Escape');
|
||||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> Environment -> Mobile Security
|
||||
*/
|
||||
export default class MobileSecurity {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly enableBiometricAuthenticationToggleTrue: Locator;
|
||||
readonly enableBiometricAuthenticationToggleFalse: Locator;
|
||||
readonly preventScreenCaptureToggleTrue: Locator;
|
||||
readonly preventScreenCaptureToggleFalse: Locator;
|
||||
readonly jailbreakProtectionToggleTrue: Locator;
|
||||
readonly jailbreakProtectionToggleFalse: Locator;
|
||||
|
||||
readonly saveButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.enableBiometricAuthenticationToggleTrue = this.container.getByTestId(
|
||||
'NativeAppSettings.MobileEnableBiometricstrue',
|
||||
);
|
||||
this.enableBiometricAuthenticationToggleFalse = this.container.getByTestId(
|
||||
'NativeAppSettings.MobileEnableBiometricsfalse',
|
||||
);
|
||||
|
||||
this.preventScreenCaptureToggleTrue = this.container.getByTestId(
|
||||
'NativeAppSettings.MobilePreventScreenCapturetrue',
|
||||
);
|
||||
this.preventScreenCaptureToggleFalse = this.container.getByTestId(
|
||||
'NativeAppSettings.MobilePreventScreenCapturefalse',
|
||||
);
|
||||
|
||||
this.jailbreakProtectionToggleTrue = this.container.getByTestId(
|
||||
'NativeAppSettings.MobileJailbreakProtectiontrue',
|
||||
);
|
||||
this.jailbreakProtectionToggleFalse = this.container.getByTestId(
|
||||
'NativeAppSettings.MobileJailbreakProtectionfalse',
|
||||
);
|
||||
|
||||
this.saveButton = this.container.getByRole('button', {name: 'Save'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async clickEnableBiometricAuthenticationToggleTrue() {
|
||||
await this.enableBiometricAuthenticationToggleTrue.click();
|
||||
}
|
||||
|
||||
async clickEnableBiometricAuthenticationToggleFalse() {
|
||||
await this.enableBiometricAuthenticationToggleFalse.click();
|
||||
}
|
||||
|
||||
async clickPreventScreenCaptureToggleTrue() {
|
||||
await this.preventScreenCaptureToggleTrue.click();
|
||||
}
|
||||
|
||||
async clickPreventScreenCaptureToggleFalse() {
|
||||
await this.preventScreenCaptureToggleFalse.click();
|
||||
}
|
||||
|
||||
async clickJailbreakProtectionToggleTrue() {
|
||||
await this.jailbreakProtectionToggleTrue.click();
|
||||
}
|
||||
|
||||
async clickJailbreakProtectionToggleFalse() {
|
||||
await this.jailbreakProtectionToggleFalse.click();
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> User Management -> Users
|
||||
*/
|
||||
export default class SystemUsers {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly searchInput: Locator;
|
||||
readonly saveRoleChange: Locator;
|
||||
readonly columnToggleMenuButton: Locator;
|
||||
readonly dateRangeSelectorMenuButton: Locator;
|
||||
readonly exportButton: Locator;
|
||||
readonly filterPopoverButton: Locator;
|
||||
readonly actionMenuButtons: Locator[];
|
||||
|
||||
readonly loadingSpinner: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.searchInput = this.container.getByLabel('Search users');
|
||||
this.saveRoleChange = this.container.locator('button.btn-primary:has-text("Save")');
|
||||
this.columnToggleMenuButton = this.container.locator('#systemUsersColumnTogglerMenuButton');
|
||||
this.dateRangeSelectorMenuButton = this.container.locator('#systemUsersDateRangeSelectorMenuButton');
|
||||
this.exportButton = this.container.getByText('Export');
|
||||
this.filterPopoverButton = this.container.getByText(/Filters \(\d+\)/);
|
||||
this.actionMenuButtons = Array.from(Array(10).keys()).map((index) =>
|
||||
this.container.locator(`#actionMenuButton-systemUsersTable-${index}`),
|
||||
);
|
||||
|
||||
this.loadingSpinner = this.container.getByText('Loading');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async isLoadingComplete() {
|
||||
await expect(this.loadingSpinner).toHaveCount(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locator for the header of the given column.
|
||||
*/
|
||||
async getColumnHeader(columnName: string) {
|
||||
const columnHeader = this.container.getByRole('columnheader').filter({hasText: columnName});
|
||||
return columnHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given column exists in the table. By searching for the column header.
|
||||
*/
|
||||
async doesColumnExist(columnName: string) {
|
||||
const columnHeader = await this.getColumnHeader(columnName);
|
||||
return await columnHeader.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the column header of the given column for sorting.
|
||||
*/
|
||||
async clickSortOnColumn(columnName: string) {
|
||||
const columnHeader = await this.getColumnHeader(columnName);
|
||||
await columnHeader.waitFor();
|
||||
await columnHeader.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locator for the given row number. If '0' is passed, it will return the header row.
|
||||
*/
|
||||
async getNthRow(rowNumber: number) {
|
||||
const row = this.container.getByRole('row').nth(rowNumber);
|
||||
await row.waitFor();
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Filter popover
|
||||
*/
|
||||
async openFilterPopover() {
|
||||
expect(this.filterPopoverButton).toBeVisible();
|
||||
await this.filterPopoverButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the column toggle menu
|
||||
*/
|
||||
async openColumnToggleMenu() {
|
||||
expect(this.columnToggleMenuButton).toBeVisible();
|
||||
await this.columnToggleMenuButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the date range selector menu
|
||||
*/
|
||||
async openDateRangeSelectorMenu() {
|
||||
expect(this.dateRangeSelectorMenuButton).toBeVisible();
|
||||
await this.dateRangeSelectorMenuButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the given search term in the search input
|
||||
*/
|
||||
async enterSearchText(searchText: string) {
|
||||
expect(this.searchInput).toBeVisible();
|
||||
await this.searchInput.fill(`${searchText}`);
|
||||
|
||||
await this.isLoadingComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches and verifies that the row with given text is found
|
||||
*/
|
||||
async verifyRowWithTextIsFound(text: string) {
|
||||
const foundUser = this.container.getByText(text);
|
||||
await foundUser.waitFor();
|
||||
|
||||
await expect(foundUser).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches and verifies that the row with given text is not found
|
||||
*/
|
||||
async verifyRowWithTextIsNotFound(text: string) {
|
||||
const foundUser = this.container.getByText(text);
|
||||
await expect(foundUser).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemConsoleSidebar {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly searchInput: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.searchInput = container.getByPlaceholder('Find settings');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
await expect(this.searchInput).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the sidebar section link with the given name. Pass the exact name of the section.
|
||||
* @param sectionName
|
||||
*/
|
||||
async goToItem(sectionName: string) {
|
||||
const section = this.container.getByText(sectionName, {exact: true});
|
||||
await section.waitFor();
|
||||
await section.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the given item in the sidebar search input.
|
||||
* @param itemName
|
||||
*/
|
||||
async searchForItem(itemName: string) {
|
||||
await this.searchInput.fill(itemName);
|
||||
}
|
||||
}
|
||||
92
e2e-tests/playwright/lib/src/ui/pages/channels.ts
Обычный файл
92
e2e-tests/playwright/lib/src/ui/pages/channels.ts
Обычный файл
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page} from '@playwright/test';
|
||||
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class ChannelsPage {
|
||||
readonly channels = 'Channels';
|
||||
|
||||
readonly page: Page;
|
||||
|
||||
readonly globalHeader;
|
||||
readonly searchPopover;
|
||||
readonly centerView;
|
||||
readonly scheduledDraftDropdown;
|
||||
readonly scheduledDraftModal;
|
||||
readonly sidebarLeft;
|
||||
readonly sidebarRight;
|
||||
readonly appBar;
|
||||
readonly userProfilePopover;
|
||||
readonly messagePriority;
|
||||
|
||||
readonly findChannelsModal;
|
||||
readonly deletePostModal;
|
||||
readonly settingsModal;
|
||||
|
||||
readonly postContainer;
|
||||
readonly postDotMenu;
|
||||
readonly postReminderMenu;
|
||||
|
||||
readonly emojiGifPickerPopup;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// The main areas of the app
|
||||
this.globalHeader = new components.GlobalHeader(page.locator('#global-header'));
|
||||
this.searchPopover = new components.SearchPopover(page.locator('#searchPopover'));
|
||||
this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'));
|
||||
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
|
||||
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
|
||||
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
|
||||
this.messagePriority = new components.MessagePriority(page.locator('body'));
|
||||
|
||||
// Modals
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||
|
||||
// Menus
|
||||
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
|
||||
this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'}));
|
||||
|
||||
// Popovers
|
||||
this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker'));
|
||||
this.scheduledDraftDropdown = new components.ScheduledDraftMenu(page.locator('#dropdown_send_post_options'));
|
||||
this.scheduledDraftModal = new components.ScheduledDraftModal(page.locator('div.modal-content'));
|
||||
this.userProfilePopover = new components.UserProfilePopover(page.locator('.user-profile-popover'));
|
||||
|
||||
// Posts
|
||||
this.postContainer = page.locator('div.post-message__text');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.centerView.toBeVisible();
|
||||
}
|
||||
|
||||
async getLastPost() {
|
||||
return this.postContainer.last();
|
||||
}
|
||||
|
||||
async goto(teamName = '', channelName = '') {
|
||||
let channelsUrl = '/';
|
||||
if (teamName) {
|
||||
channelsUrl += `${teamName}`;
|
||||
if (channelName) {
|
||||
const prefix = channelName.startsWith('@') ? '/messages' : '/channels';
|
||||
channelsUrl += `${prefix}/${channelName}`;
|
||||
}
|
||||
}
|
||||
await this.page.goto(channelsUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* `postMessage` posts a message in the current channel
|
||||
* @param message Message to post
|
||||
*/
|
||||
async postMessage(message: string) {
|
||||
await this.centerView.postCreate.postMessage(message);
|
||||
}
|
||||
}
|
||||
126
e2e-tests/playwright/lib/src/ui/pages/drafts.ts
Обычный файл
126
e2e-tests/playwright/lib/src/ui/pages/drafts.ts
Обычный файл
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
export default class DraftPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly badgeCountOnScheduledTab;
|
||||
readonly confirmbutton;
|
||||
readonly datePattern;
|
||||
readonly deleteIcon;
|
||||
readonly deleteIconToolTip;
|
||||
readonly noscheduledDraftIcon;
|
||||
readonly scheduleIcon;
|
||||
readonly rescheduleIconToolTip;
|
||||
readonly draftBody;
|
||||
readonly scheduledDraftPageInfo;
|
||||
readonly scheduledDraftPanel;
|
||||
readonly scheduledDraftSendNowButton;
|
||||
readonly scheduledDraftSendNowButtonToolTip;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.draftBody = page.locator('div.post__body');
|
||||
this.scheduleIcon = page.locator('#draft_icon-clock-send-outline_reschedule');
|
||||
|
||||
this.datePattern =
|
||||
/(Today|Tomorrow|(?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}) at \d{1,2}:\d{2} [AP]M/;
|
||||
this.badgeCountOnScheduledTab = page.locator('a#draft_tabs-tab-0 div.drafts_tab_title span.MuiBadge-badge');
|
||||
// this.scheduledDraftPageInfo = page.locator('span:has-text("Send on")');
|
||||
this.scheduledDraftPageInfo = page.locator('.PanelHeader__info');
|
||||
this.scheduledDraftPanel = (messageContent: string) =>
|
||||
page.locator(`article.Panel:has(div.post__body:has-text("${messageContent}"))`);
|
||||
this.deleteIcon = page.locator('#draft_icon-trash-can-outline_delete');
|
||||
this.deleteIconToolTip = page.locator('text=Delete scheduled post');
|
||||
this.rescheduleIconToolTip = page.locator('text=Schedule draft');
|
||||
this.noscheduledDraftIcon = page.locator('.no-results__wrapper');
|
||||
this.scheduledDraftSendNowButton = page.locator('#draft_icon-send-outline_sendNow');
|
||||
this.scheduledDraftSendNowButtonToolTip = page.locator('text=Send now');
|
||||
this.confirmbutton = this.page.locator('button.btn-primary');
|
||||
}
|
||||
|
||||
async goTo(teamName: string) {
|
||||
await this.page.goto(`/${teamName}/drafts`);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await expect(this.page).toHaveURL(/.*drafts/);
|
||||
}
|
||||
|
||||
async assertBadgeCountOnTab(badgeCount: string) {
|
||||
await this.badgeCountOnScheduledTab.isVisible();
|
||||
await expect(this.badgeCountOnScheduledTab).toHaveText(badgeCount);
|
||||
}
|
||||
|
||||
async assertDraftBody(draftMessage: string) {
|
||||
await expect(this.draftBody).toBeVisible();
|
||||
await expect(this.draftBody).toHaveText(draftMessage);
|
||||
}
|
||||
|
||||
async verifyOnHoverActionItems(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifyDeleteIcon();
|
||||
await this.verifyScheduleIcon(messageContent);
|
||||
await this.verifySendNowIcon();
|
||||
}
|
||||
|
||||
async verifyDeleteIcon() {
|
||||
await this.deleteIcon.isVisible();
|
||||
await this.deleteIcon.hover();
|
||||
await expect(this.deleteIconToolTip).toBeVisible();
|
||||
await expect(this.deleteIconToolTip).toHaveText('Delete scheduled post');
|
||||
}
|
||||
|
||||
async verifyScheduleIcon(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await expect(this.scheduleIcon).toBeVisible();
|
||||
await this.scheduleIcon.hover();
|
||||
await expect(this.rescheduleIconToolTip).toBeVisible();
|
||||
await expect(this.rescheduleIconToolTip).toHaveText('Schedule draft');
|
||||
}
|
||||
|
||||
async verifySendNowIcon() {
|
||||
await this.scheduledDraftSendNowButton.isVisible();
|
||||
await this.scheduledDraftSendNowButton.hover();
|
||||
await expect(this.scheduledDraftSendNowButtonToolTip).toBeVisible();
|
||||
await expect(this.scheduledDraftSendNowButtonToolTip).toHaveText('Send now');
|
||||
}
|
||||
|
||||
async getTimeStampOfMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
return this.scheduledDraftPanel(messageContent).locator(this.scheduledDraftPageInfo).innerHTML();
|
||||
}
|
||||
|
||||
async openScheduleModal(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.scheduleIcon.hover();
|
||||
await expect(this.rescheduleIconToolTip).toBeVisible();
|
||||
await expect(this.rescheduleIconToolTip).toHaveText('Schedule draft');
|
||||
await this.scheduleIcon.click();
|
||||
}
|
||||
|
||||
async deleteScheduledMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifyDeleteIcon();
|
||||
await this.deleteIcon.click();
|
||||
expect(await this.confirmbutton.textContent()).toEqual('Yes, delete');
|
||||
await this.confirmbutton.click();
|
||||
}
|
||||
|
||||
async sendScheduledMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifySendNowIcon();
|
||||
await this.scheduledDraftSendNowButton.click();
|
||||
expect(await this.confirmbutton.textContent()).toEqual('Yes, send now');
|
||||
await this.confirmbutton.click();
|
||||
}
|
||||
}
|
||||
34
e2e-tests/playwright/lib/src/ui/pages/index.ts
Обычный файл
34
e2e-tests/playwright/lib/src/ui/pages/index.ts
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ChannelsPage from './channels';
|
||||
import LandingLoginPage from './landing_login';
|
||||
import LoginPage from './login';
|
||||
import ResetPasswordPage from './reset_password';
|
||||
import SignupPage from './signup';
|
||||
import SystemConsolePage from './system_console';
|
||||
import ScheduledDraftPage from './scheduled_draft';
|
||||
import DraftPage from './drafts';
|
||||
|
||||
const pages = {
|
||||
ChannelsPage,
|
||||
LandingLoginPage,
|
||||
LoginPage,
|
||||
ResetPasswordPage,
|
||||
SignupPage,
|
||||
ScheduledDraftPage,
|
||||
SystemConsolePage,
|
||||
DraftPage,
|
||||
};
|
||||
|
||||
export {
|
||||
pages,
|
||||
ChannelsPage,
|
||||
LandingLoginPage,
|
||||
LoginPage,
|
||||
ResetPasswordPage,
|
||||
SignupPage,
|
||||
ScheduledDraftPage,
|
||||
SystemConsolePage,
|
||||
DraftPage,
|
||||
};
|
||||
39
e2e-tests/playwright/lib/src/ui/pages/landing_login.ts
Обычный файл
39
e2e-tests/playwright/lib/src/ui/pages/landing_login.ts
Обычный файл
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} 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');
|
||||
}
|
||||
}
|
||||
68
e2e-tests/playwright/lib/src/ui/pages/login.ts
Обычный файл
68
e2e-tests/playwright/lib/src/ui/pages/login.ts
Обычный файл
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} from '@playwright/test';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class LoginPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly title;
|
||||
readonly subtitle;
|
||||
readonly bodyCard;
|
||||
readonly loginInput;
|
||||
readonly loginPlaceholder;
|
||||
readonly loginWithAdLdapPlaceholder;
|
||||
readonly passwordInput;
|
||||
readonly passwordToggleButton;
|
||||
readonly signInButton;
|
||||
readonly createAccountLink;
|
||||
readonly forgotPasswordLink;
|
||||
readonly userErrorLabel;
|
||||
readonly fieldWithError;
|
||||
readonly formContainer;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
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-content');
|
||||
this.loginInput = page.locator('#input_loginId');
|
||||
this.loginPlaceholder = page.locator(`[placeholder="Email or Username"]`);
|
||||
this.loginWithAdLdapPlaceholder = page.locator(`[placeholder="Email, Username or AD/LDAP Username"]`);
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.getByRole('button', {name: 'Show or hide password'});
|
||||
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');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.hfroute-header'));
|
||||
this.footer = new components.Footer(page.locator('.hfroute-footer'));
|
||||
}
|
||||
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
49
e2e-tests/playwright/lib/src/ui/pages/reset_password.ts
Обычный файл
49
e2e-tests/playwright/lib/src/ui/pages/reset_password.ts
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class ResetPasswordPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly title;
|
||||
readonly subtitle;
|
||||
readonly emailInput;
|
||||
readonly resetButton;
|
||||
readonly formContainer;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.title = page.locator('h1:has-text("Password Reset")');
|
||||
this.subtitle = page.locator('text=To reset your password, enter the email address you used to sign up');
|
||||
this.emailInput = page.locator(`[placeholder="Email"]`);
|
||||
this.resetButton = page.locator('#passwordResetButton');
|
||||
this.formContainer = page.locator('.signup-team__container');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.signup-header'));
|
||||
this.footer = new components.Footer(page.locator('#footer_section'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.subtitle).toBeVisible();
|
||||
await expect(this.emailInput).toBeVisible();
|
||||
await expect(this.resetButton).toBeVisible();
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/reset_password');
|
||||
}
|
||||
|
||||
async reset(email: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await Promise.all([this.page.waitForNavigation(), this.resetButton.click()]);
|
||||
}
|
||||
}
|
||||
159
e2e-tests/playwright/lib/src/ui/pages/scheduled_draft.ts
Обычный файл
159
e2e-tests/playwright/lib/src/ui/pages/scheduled_draft.ts
Обычный файл
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly badgeCountOnScheduledTab;
|
||||
readonly confirmbutton;
|
||||
readonly copyIcon;
|
||||
readonly copyIconToolTip;
|
||||
readonly datePattern;
|
||||
readonly deleteIcon;
|
||||
readonly deleteIconToolTip;
|
||||
readonly noscheduledDraftIcon;
|
||||
readonly rescheduleIcon;
|
||||
readonly rescheduleIconToolTip;
|
||||
readonly scheduledDraftBody;
|
||||
readonly scheduledDraftPageInfo;
|
||||
readonly scheduledDraftPanel;
|
||||
readonly scheduledDraftSendNowButton;
|
||||
readonly scheduledDraftSendNowButtonToolTip;
|
||||
readonly editIcon;
|
||||
readonly editBox;
|
||||
readonly editorSaveButton;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.datePattern =
|
||||
/(Today|Tomorrow|(?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}) at \d{1,2}:\d{2} [AP]M/;
|
||||
this.scheduledDraftBody = page.locator('div.post__body');
|
||||
this.badgeCountOnScheduledTab = page.locator('a#draft_tabs-tab-1 div.drafts_tab_title span.MuiBadge-badge');
|
||||
this.scheduledDraftPageInfo = page.locator('.PanelHeader__info');
|
||||
this.scheduledDraftPanel = (messageContent: string) =>
|
||||
page.locator(`article.Panel:has(div.post__body:has-text("${messageContent}"))`);
|
||||
this.deleteIcon = page.locator('#draft_icon-trash-can-outline_delete');
|
||||
this.deleteIconToolTip = page.locator('text=Delete scheduled post');
|
||||
this.copyIcon = page.locator('#draft_icon-content-copy_copy_text');
|
||||
this.copyIconToolTip = page.locator('text=Copy text');
|
||||
this.rescheduleIcon = page.locator('#draft_icon-clock-send-outline_reschedule');
|
||||
this.rescheduleIconToolTip = page.locator('text=Reschedule post');
|
||||
this.noscheduledDraftIcon = page.locator('.no-results__wrapper');
|
||||
this.scheduledDraftSendNowButton = page.locator('#draft_icon-send-outline_sendNow');
|
||||
this.scheduledDraftSendNowButtonToolTip = page.locator('text=Send now');
|
||||
this.confirmbutton = this.page.locator('button.btn-primary');
|
||||
this.editIcon = page.locator('#draft_icon-pencil-outline_edit');
|
||||
this.editBox = page.locator('textarea#edit_textbox');
|
||||
this.editorSaveButton = page.locator('button.save');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await expect(this.page).toHaveURL(/.*scheduled_posts/);
|
||||
}
|
||||
|
||||
async assertBadgeCountOnTab(badgeCount: string) {
|
||||
await this.badgeCountOnScheduledTab.isVisible();
|
||||
await expect(this.badgeCountOnScheduledTab).toHaveText(badgeCount);
|
||||
}
|
||||
|
||||
async assertscheduledDraftBody(draftMessage: string) {
|
||||
await expect(this.scheduledDraftBody).toBeVisible();
|
||||
await expect(this.scheduledDraftBody).toHaveText(draftMessage);
|
||||
}
|
||||
|
||||
async verifyOnHoverActionItems(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifyDeleteIcon();
|
||||
await this.verifyCopyIcon();
|
||||
await this.verifyRescheduleIcon();
|
||||
await this.verifySendNowIcon();
|
||||
}
|
||||
|
||||
async verifyDeleteIcon() {
|
||||
await this.deleteIcon.isVisible();
|
||||
await this.deleteIcon.hover();
|
||||
await expect(this.deleteIconToolTip).toBeVisible();
|
||||
await expect(this.deleteIconToolTip).toHaveText('Delete scheduled post');
|
||||
}
|
||||
|
||||
async verifyCopyIcon() {
|
||||
await this.copyIcon.isVisible();
|
||||
await this.copyIcon.hover();
|
||||
await expect(this.copyIconToolTip).toBeVisible();
|
||||
await expect(this.copyIconToolTip).toHaveText('Copy text');
|
||||
}
|
||||
|
||||
async verifyRescheduleIcon() {
|
||||
await expect(this.rescheduleIcon).toBeVisible();
|
||||
await this.rescheduleIcon.hover();
|
||||
await expect(this.rescheduleIconToolTip).toBeVisible();
|
||||
await expect(this.rescheduleIconToolTip).toHaveText('Reschedule post');
|
||||
}
|
||||
|
||||
async verifySendNowIcon() {
|
||||
await this.scheduledDraftSendNowButton.isVisible();
|
||||
await this.scheduledDraftSendNowButton.hover();
|
||||
await expect(this.scheduledDraftSendNowButtonToolTip).toBeVisible();
|
||||
await expect(this.scheduledDraftSendNowButtonToolTip).toHaveText('Send now');
|
||||
}
|
||||
|
||||
async getTimeStampOfMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
return this.scheduledDraftPanel(messageContent).locator(this.scheduledDraftPageInfo).innerHTML();
|
||||
}
|
||||
|
||||
async openRescheduleModal(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).scrollIntoViewIfNeeded();
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.rescheduleIcon.hover();
|
||||
await expect(this.rescheduleIconToolTip).toBeVisible();
|
||||
await expect(this.rescheduleIconToolTip).toHaveText('Reschedule post');
|
||||
await this.rescheduleIcon.click();
|
||||
}
|
||||
|
||||
async deleteScheduledMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifyDeleteIcon();
|
||||
await this.deleteIcon.click();
|
||||
expect(await this.confirmbutton.textContent()).toEqual('Yes, delete');
|
||||
await this.confirmbutton.click();
|
||||
}
|
||||
|
||||
async sendScheduledMessage(messageContent: string) {
|
||||
await this.scheduledDraftPanel(messageContent).isVisible();
|
||||
await this.scheduledDraftPanel(messageContent).hover();
|
||||
await this.verifySendNowIcon();
|
||||
await this.scheduledDraftSendNowButton.click();
|
||||
expect(await this.confirmbutton.textContent()).toEqual('Yes, send now');
|
||||
await this.confirmbutton.click();
|
||||
}
|
||||
|
||||
async goTo(teamName: string) {
|
||||
await this.page.goto(`/${teamName}/scheduled_posts`);
|
||||
}
|
||||
|
||||
async editText(newText: string) {
|
||||
await this.editIcon.click();
|
||||
await this.editBox.isVisible();
|
||||
await this.editBox.fill(newText);
|
||||
await this.editorSaveButton.isVisible();
|
||||
await this.editorSaveButton.click();
|
||||
await this.editBox.isHidden();
|
||||
await this.scheduledDraftPanel(newText).isVisible();
|
||||
}
|
||||
|
||||
async copyScheduledMessage(draftMessage: string) {
|
||||
await this.scheduledDraftPanel(draftMessage).isVisible();
|
||||
await this.scheduledDraftPanel(draftMessage).hover();
|
||||
await this.verifyCopyIcon();
|
||||
await this.copyIcon.click();
|
||||
}
|
||||
}
|
||||
88
e2e-tests/playwright/lib/src/ui/pages/signup.ts
Обычный файл
88
e2e-tests/playwright/lib/src/ui/pages/signup.ts
Обычный файл
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
import {duration, wait} from '@/util';
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class SignupPage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly title;
|
||||
readonly subtitle;
|
||||
readonly bodyCard;
|
||||
readonly emailInput;
|
||||
readonly usernameInput;
|
||||
readonly passwordInput;
|
||||
readonly passwordToggleButton;
|
||||
readonly newsLetterCheckBox;
|
||||
readonly newsLetterPrivacyPolicyLink;
|
||||
readonly newsLetterUnsubscribeLink;
|
||||
readonly agreementTermsOfUseLink;
|
||||
readonly agreementPrivacyPolicyLink;
|
||||
readonly createAccountButton;
|
||||
readonly loginLink;
|
||||
readonly emailError;
|
||||
readonly usernameError;
|
||||
readonly passwordError;
|
||||
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.title = page.locator('h1:has-text("Let’s get started")');
|
||||
this.subtitle = page.locator('text=Create your Mattermost account to start collaborating with your team');
|
||||
this.bodyCard = page.locator('.signup-body-card-content');
|
||||
this.loginLink = page.locator('text=Log in');
|
||||
this.emailInput = page.locator('#input_email');
|
||||
this.usernameInput = page.locator('#input_name');
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.getByRole('button', {name: 'Show or hide password'});
|
||||
this.createAccountButton = page.locator('button:has-text("Create Account")');
|
||||
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-72 characters long.');
|
||||
|
||||
const newsletterBlock = page.locator('.check-input');
|
||||
this.newsLetterCheckBox = newsletterBlock.getByRole('checkbox', {name: 'newsletter checkbox'});
|
||||
this.newsLetterPrivacyPolicyLink = newsletterBlock.locator('text=Privacy Policy');
|
||||
this.newsLetterUnsubscribeLink = newsletterBlock.locator('text=unsubscribe');
|
||||
|
||||
const agreementBlock = page.locator('.signup-body-card-agreement');
|
||||
this.agreementTermsOfUseLink = agreementBlock.locator('text=Terms of Use');
|
||||
this.agreementPrivacyPolicyLink = agreementBlock.locator('text=Privacy Policy');
|
||||
|
||||
this.header = new components.MainHeader(page.locator('.hfroute-header'));
|
||||
this.footer = new components.Footer(page.locator('.hfroute-footer'));
|
||||
}
|
||||
|
||||
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_user_complete');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
e2e-tests/playwright/lib/src/ui/pages/system_console.ts
Обычный файл
87
e2e-tests/playwright/lib/src/ui/pages/system_console.ts
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page} from '@playwright/test';
|
||||
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class SystemConsolePage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly sidebar;
|
||||
readonly navbar;
|
||||
|
||||
/**
|
||||
* System Console -> User Management -> Users
|
||||
*/
|
||||
readonly systemUsers;
|
||||
readonly systemUsersFilterPopover;
|
||||
readonly systemUsersRoleMenu;
|
||||
readonly systemUsersStatusMenu;
|
||||
readonly systemUsersDateRangeMenu;
|
||||
readonly systemUsersColumnToggleMenu;
|
||||
readonly systemUsersActionMenus;
|
||||
|
||||
readonly mobileSecurity;
|
||||
|
||||
readonly featureDiscovery;
|
||||
|
||||
// modal
|
||||
readonly confirmModal;
|
||||
readonly exportModal;
|
||||
readonly saveChangesModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Areas of the page
|
||||
this.navbar = new components.SystemConsoleNavbar(page.locator('.backstage-navbar'));
|
||||
this.sidebar = new components.SystemConsoleSidebar(page.locator('.admin-sidebar'));
|
||||
|
||||
// Sections and sub-sections
|
||||
this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection'));
|
||||
this.mobileSecurity = new components.SystemConsoleMobileSecurity(
|
||||
page.getByTestId('sysconsole_section_MobileSecuritySettings'),
|
||||
);
|
||||
this.featureDiscovery = new components.SystemConsoleFeatureDiscovery(page.getByTestId('featureDiscovery'));
|
||||
|
||||
// Menus & Popovers
|
||||
this.systemUsersFilterPopover = new components.SystemUsersFilterPopover(
|
||||
page.locator('#systemUsersFilterPopover'),
|
||||
);
|
||||
this.systemUsersRoleMenu = new components.SystemUsersFilterMenu(page.locator('#DropdownInput_filterRole'));
|
||||
this.systemUsersStatusMenu = new components.SystemUsersFilterMenu(page.locator('#DropdownInput_filterStatus'));
|
||||
this.systemUsersColumnToggleMenu = new components.SystemUsersColumnToggleMenu(
|
||||
page.locator('#systemUsersColumnTogglerMenu'),
|
||||
);
|
||||
this.systemUsersDateRangeMenu = new components.SystemUsersFilterMenu(
|
||||
page.locator('#systemUsersDateRangeSelectorMenu'),
|
||||
);
|
||||
this.systemUsersActionMenus = Array.from(Array(10).keys()).map(
|
||||
(index) => new components.SystemUsersFilterMenu(page.locator(`#actionMenu-systemUsersTable-${index}`)),
|
||||
);
|
||||
|
||||
this.confirmModal = new components.GenericConfirmModal(page.locator('#confirmModal'));
|
||||
this.exportModal = new components.GenericConfirmModal(page.getByRole('dialog', {name: 'Export user data'}));
|
||||
this.saveChangesModal = new components.SystemUsers(page.locator('div.modal-content'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
await this.sidebar.toBeVisible();
|
||||
await this.navbar.toBeVisible();
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/admin_console');
|
||||
}
|
||||
|
||||
async saveRoleChange() {
|
||||
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Save")').click();
|
||||
}
|
||||
|
||||
async clickResetButton() {
|
||||
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click();
|
||||
}
|
||||
}
|
||||
48
e2e-tests/playwright/lib/src/util.ts
Обычный файл
48
e2e-tests/playwright/lib/src/util.ts
Обычный файл
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
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 const simpleEmailRe = /\S+@\S+\.\S+/;
|
||||
45
e2e-tests/playwright/lib/src/visual/index.ts
Обычный файл
45
e2e-tests/playwright/lib/src/visual/index.ts
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// 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 {TestInfo, expect} from '@playwright/test';
|
||||
|
||||
import snapshotWithPercy from './percy';
|
||||
|
||||
import {duration, illegalRe, wait} from '@/util';
|
||||
import {testConfig} from '@/test_config';
|
||||
import {ScreenshotOptions, TestArgs} from '@/types';
|
||||
|
||||
export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs, options: ScreenshotOptions = {}) {
|
||||
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 || testConfig.percyEnabled) {
|
||||
await testArgs.page.waitForLoadState('networkidle');
|
||||
await testArgs.page.waitForLoadState('domcontentloaded');
|
||||
await wait(duration.half_sec);
|
||||
}
|
||||
|
||||
if (testConfig.snapshotEnabled) {
|
||||
// Visual test with built-in snapshot
|
||||
const filename = testInfo.title.trim().replace(illegalRe, '').replace(/\s/g, '-').trim().toLowerCase();
|
||||
await expect(testArgs.page).toHaveScreenshot(`${filename}.png`, {fullPage: true, ...options});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
21
e2e-tests/playwright/lib/src/visual/percy.ts
Обычный файл
21
e2e-tests/playwright/lib/src/visual/percy.ts
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
import {TestArgs} from '@/types';
|
||||
|
||||
export default async function snapshotWithPercy(name: string, testArgs: TestArgs) {
|
||||
if (testArgs.browserName === 'chromium' && testConfig.percyEnabled && testArgs.viewport) {
|
||||
const {page, viewport} = testArgs;
|
||||
|
||||
try {
|
||||
await percySnapshot(page, name, {widths: [viewport.width], minHeight: viewport.height});
|
||||
} catch (error) {
|
||||
// log an error for debugging
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${error}\nIn addition, check if token is properly set by "export PERCY_TOKEN=<change_me>"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user