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
Этот коммит содержится в:
Saturnino Abril
2025-04-01 08:52:56 +08:00
коммит произвёл GitHub
родитель ce9632cca3
Коммит a47269cfe2
163 изменённых файлов: 7203 добавлений и 5048 удалений

2
.github/workflows/e2e-fulltests-ci.yml поставляемый
Просмотреть файл

@@ -70,7 +70,7 @@ jobs:
SERVER: "${{ steps.generate.outputs.SERVER }}" SERVER: "${{ steps.generate.outputs.SERVER }}"
ENABLED_DOCKER_SERVICES: "${{ steps.generate.outputs.ENABLED_DOCKER_SERVICES }}" ENABLED_DOCKER_SERVICES: "${{ steps.generate.outputs.ENABLED_DOCKER_SERVICES }}"
TEST_FILTER_CYPRESS: "${{ steps.generate.outputs.TEST_FILTER_CYPRESS }}" TEST_FILTER_CYPRESS: "${{ steps.generate.outputs.TEST_FILTER_CYPRESS }}"
TEST_FILTER_PLAYWRIGHT: "tests/ --project=chrome" # Note: Run on chrome but eventually will enable to all projects which include firefox and ipad. TEST_FILTER_PLAYWRIGHT: "tests"
BUILD_ID: "${{ steps.generate.outputs.BUILD_ID }}" BUILD_ID: "${{ steps.generate.outputs.BUILD_ID }}"
TM4J_ENABLE: "${{ steps.generate.outputs.TM4J_ENABLE }}" TM4J_ENABLE: "${{ steps.generate.outputs.TM4J_ENABLE }}"
REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}" REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}"

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

@@ -98,7 +98,7 @@ case "${TEST:-$TEST_DEFAULT}" in
cypress ) cypress )
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;; export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;;
playwright ) playwright )
export TEST_FILTER_DEFAULT='tests/functional/system_console/system_users/actions.spec.ts --project=chrome' ;; export TEST_FILTER_DEFAULT='functional/system_console/system_users/actions.spec.ts' ;;
* ) * )
export TEST_FILTER_DEFAULT='' ;; export TEST_FILTER_DEFAULT='' ;;
esac esac

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

@@ -105,7 +105,7 @@ case "$TEST" in
if [ -n "$WEBHOOK_URL" ]; then if [ -n "$WEBHOOK_URL" ]; then
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
# Utilize environment data and report files to generate the webhook body # Utilize environment data and report files to generate the webhook body
./report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL" node report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL"
fi fi
;; ;;
esac esac

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

@@ -276,7 +276,7 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then $(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
echo ' echo '
playwright: playwright:
image: mcr.microsoft.com/playwright:v1.49.1-noble image: mcr.microsoft.com/playwright:v1.51.1-noble
entrypoint: ["/bin/bash", "-c"] entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"] command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
env_file: env_file:

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

@@ -37,13 +37,18 @@ EOF
# Run Playwright test # Run Playwright test
# NB: do not exit the script if some testcases fail # NB: do not exit the script if some testcases fail
${MME2E_DC_SERVER} exec -i -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/playwright && npm run test -- ${TEST_FILTER}" | tee ../playwright/logs/playwright.log || true ${MME2E_DC_SERVER} exec -i -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/playwright && npm run test:ci -- ${TEST_FILTER}" | tee ../playwright/logs/playwright.log || true
# Collect run results # Collect run results
# Documentation on the results.json file: https://playwright.dev/docs/api/class-testcase#test-case-expected-status # Documentation on the results.json file: https://playwright.dev/docs/api/class-testcase#test-case-expected-status
# NB: the following line is needed only for compatibility reasons, to support RollingRelease tests for versions prior to v10.1.0 # NB: the following line is needed only for compatibility reasons, to support RollingRelease tests for versions prior to v10.1.0
# It can be removed after releases <=v10.0.x are phased out # It can be removed after releases <=v10.0.x are phased out
mv -v ../playwright/playwright-report/results.json ../playwright/results/reporter/results.json 2>/dev/null || true mv -v ../playwright/playwright-report/results.json ../playwright/results/reporter/results.json 2>/dev/null || true
# NB: the following line is needed only for compatibility reasons, to support RollingRelease tests for versions prior to v10.6.0
# It can be removed once 10.5 (ESR) is no longer supported
mv -v ../playwright/test/results/ ../playwright/ 2>/dev/null || true
jq -f /dev/stdin ../playwright/results/reporter/results.json > ../playwright/results/summary.json <<EOF jq -f /dev/stdin ../playwright/results/reporter/results.json > ../playwright/results/summary.json <<EOF
{ {
passed: .stats.expected, passed: .stats.expected,

16
e2e-tests/playwright/.gitignore поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
# playwright tests
test/logs
test/node_modules
test/playwright-report
test/test-results
test/results
test/storage_state
test/specs-results
test/specs/**/*-darwin.png
test/specs/**/*-window.png
test/specs/accessibility/**/*-snapshots
test/.eslintcache
# build
dist
*.tsbuildinfo

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

@@ -5,6 +5,7 @@ import path from 'node:path';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
import js from '@eslint/js'; import js from '@eslint/js';
import {FlatCompat} from '@eslint/eslintrc'; import {FlatCompat} from '@eslint/eslintrc';
import eslintPluginHeader from 'eslint-plugin-header';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -14,18 +15,23 @@ const compat = new FlatCompat({
allConfig: js.configs.all, allConfig: js.configs.all,
}); });
eslintPluginHeader.rules.header.meta.schema = false;
export default [ export default [
{ {
ignores: ['**/node_modules', '**/playwright-report', '**/test-results', '**/results'], ignores: ['**/node_modules', '**/dist', '**/playwright-report', '**/test-results', '**/results'],
}, },
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended').map((config) => ({ ...compat
...config, .extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:import/recommended')
files: ['**/*.ts', '**/*.s'], .map((config) => ({
})), ...config,
files: ['**/*.ts', '**/*.js'],
})),
{ {
files: ['**/*.ts', '**/*.s'], files: ['**/*.ts', '**/*.js'],
plugins: { plugins: {
'@typescript-eslint': typescriptEslint, '@typescript-eslint': typescriptEslint,
header: eslintPluginHeader,
}, },
languageOptions: { languageOptions: {
globals: { globals: {
@@ -35,11 +41,31 @@ export default [
ecmaVersion: 5, ecmaVersion: 5,
sourceType: 'module', sourceType: 'module',
}, },
settings: {
'import/resolver': {
typescript: true,
node: true,
},
},
rules: { rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'no-console': 'error', 'no-console': 'error',
'header/header': [
'error',
'line',
' Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\n See LICENSE.txt for license information.',
2,
],
'import/order': [
'error',
{
'newlines-between': 'always',
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
},
],
'import/no-unresolved': 'off',
}, },
}, },
]; ];

214
e2e-tests/playwright/lib/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,214 @@
# @mattermost/playwright-lib
A comprehensive end-to-end testing library for Mattermost web, desktop and plugin applications using Playwright.
## Overview
This library provides:
- Pre-built page objects and components for common Mattermost UI elements
- Server configuration and initialization utilities
- Test fixtures and helpers
- Visual testing support with Percy integration
- Accessibility testing support with [axe-core](https://github.com/dequelabs/axe-core)
- Browser notification mocking
- File handling utilities
- Common test actions and assertions
## Installation
```bash
npm install @mattermost/playwright-lib
```
## Usage
Basic example of logging in and posting a message:
```typescript
import {test, expect} from '@mattermost/playwright-lib';
test('user can post message', async ({pw}) => {
// # Create and login a new user
const {user} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
// # Navigate and post a message
await channelsPage.goto();
const message = 'Hello World!';
await channelsPage.postMessage(message);
// * Verify message appears
const lastPost = await channelsPage.getLastPost();
await expect(lastPost).toHaveText(message);
});
```
## Key Components
### Page Objects
Ready-to-use page objects for common Mattermost pages:
- Login
- Signup
- Channels
- System Console
- And more...
### UI Components
Reusable component objects for UI elements:
- Headers
- Posts
- Menus
- Modals
- And more...
### Test Utilities
Helper functions for common testing needs:
- Server setup and configuration
- User/team creation
- File handling
- Visual testing
- And more...
## Configuration
The library can be configured via optional environment variables:
### Environment Variables
All environment variables are optional with sensible defaults.
#### Server Configuration
| Variable | Description | Default |
| ----------------------------- | ------------------------------------------ | -------------------------------- |
| `PW_BASE_URL` | Server URL | `http://localhost:8065` |
| `PW_ADMIN_USERNAME` | Admin username | `sysadmin` |
| `PW_ADMIN_PASSWORD` | Admin password | `Sys@dmin-sample1` |
| `PW_ADMIN_EMAIL` | Admin email | `sysadmin@sample.mattermost.com` |
| `PW_ENSURE_PLUGINS_INSTALLED` | Comma-separated list of plugins to install | `[]` |
| `PW_RESET_BEFORE_TEST` | Reset server before test | `false` |
#### High Availability Cluster Settings
| Variable | Description | Default |
| -------------------------- | ----------------------- | ---------------- |
| `PW_HA_CLUSTER_ENABLED` | Enable HA cluster | `false` |
| `PW_HA_CLUSTER_NODE_COUNT` | Number of cluster nodes | `2` |
| `PW_HA_CLUSTER_NAME` | Cluster name | `mm_dev_cluster` |
#### Push Notifications
| Variable | Description | Default |
| ----------------------------- | ---------------------------- | ---------------------------------- |
| `PW_PUSH_NOTIFICATION_SERVER` | Push notification server URL | `https://push-test.mattermost.com` |
#### Playwright Settings
| Variable | Description | Default |
| ------------- | ------------------------------- | ------- |
| `PW_HEADLESS` | Run tests headless | `true` |
| `PW_SLOWMO` | Add delay between actions in ms | `0` |
| `PW_WORKERS` | Number of parallel workers | `1` |
#### Visual Testing
| Variable | Description | Default |
| -------------------- | --------------------------- | ------- |
| `PW_SNAPSHOT_ENABLE` | Enable snapshot testing | `false` |
| `PW_PERCY_ENABLE` | Enable Percy visual testing | `false` |
#### CI Settings
| Variable | Description | Default |
| -------- | ------------------------------------ | ------- |
| `CI` | Set automatically in CI environments | N/A |
## Accessibility Testing
The library includes built-in accessibility testing using [axe-core](https://github.com/dequelabs/axe-core):
```typescript
import {test, expect} from '@mattermost/playwright-lib';
test('verify login page accessibility', async ({page, axe}) => {
// # Navigate to login page
await page.goto('/login');
// # Run accessibility scan
const results = await axe.builder(page).analyze();
// * Verify no accessibility violations
expect(results.violations).toHaveLength(0);
});
```
The axe-core integration:
- Runs WCAG 2.0 Level A & AA rules by default
- Provides detailed violation reports
- Supports rule customization
- Can be configured per-test or globally
## Visual Testing
The library supports visual testing through [Playwright's built-in visual comparisons](https://playwright.dev/docs/test-snapshots) and [Percy](https://www.browserstack.com/percy) integration:
```typescript
import {test, expect} from '@mattermost/playwright-lib';
test('verify channel header appearance', async ({pw, browserName, viewport}, testInfo) => {
// # Setup and login
const {user} = await pw.initSetup();
const {page, channelsPage} = await pw.testBrowser.login(user);
// # Navigate and prepare page
await channelsPage.goto();
await expect(channelsPage.appBar.playbooksIcon).toBeVisible();
await pw.hideDynamicChannelsContent(page);
// * Take and verify snapshot
await pw.matchSnapshot(testInfo, {page, browserName, viewport});
});
```
## Browser Notifications
Mock and verify browser notifications:
```typescript
import {test, expect} from '@mattermost/playwright-lib';
test('verify notification on mention', async ({pw}) => {
// # Setup users and team
const {team, adminUser, user} = await pw.initSetup();
// # Setup admin browser with notifications
const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
await adminChannelsPage.goto(team.name, 'town-square');
await pw.stubNotification(adminPage, 'granted');
// # Setup user browser and post mention
const {channelsPage: userChannelsPage} = await pw.testBrowser.login(user);
await userChannelsPage.goto(team.name, 'off-topic');
await userChannelsPage.postMessage(`@ALL good morning, ${team.name}!`);
// * Verify notification received
const notifications = await pw.waitForNotification(adminPage);
expect(notifications.length).toBe(1);
});
```
## Contributing
See [CONTRIBUTING.md](https://github.com/mattermost/mattermost/blob/master/CONTRIBUTING.md) for development setup and guidelines.
## License
See [LICENSE.txt](https://github.com/mattermost/mattermost/blob/master/LICENSE.txt) for license information.

68
e2e-tests/playwright/lib/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,68 @@
{
"name": "@mattermost/playwright-lib",
"version": "10.6.1",
"description": "A comprehensive end-to-end testing library for Mattermost web, desktop and plugin applications using Playwright",
"repository": {
"type": "git",
"url": "git+https://github.com/mattermost/mattermost.git"
},
"author": "mattermost",
"license": "MIT",
"bugs": {
"url": "https://github.com/mattermost/mattermost/issues"
},
"homepage": "https://github.com/mattermost/mattermost/tree/master/e2e-tests/playwright/lib#readme",
"type": "commonjs",
"files": [
"dist"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"keywords": [
"mattermost",
"e2e",
"playwright",
"test-automation"
],
"scripts": {
"build": "rollup -c --bundleConfigAsCjs",
"build:watch": "npm run build -- --watch",
"build-tsc": "tsc --build --verbose",
"build-tsc:watch": "tsc --watch --preserveWatchOutput",
"clean": "rm -rf dist node_modules *.tsbuildinfo",
"tsc": "tsc -b"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@axe-core/playwright": "4.10.1",
"@mattermost/client": "file:../../../webapp/platform/client",
"@mattermost/types": "file:../../../webapp/platform/types",
"@percy/cli": "1.30.7",
"@percy/playwright": "1.0.7",
"async-wait-until": "2.0.23",
"axe-core": "4.10.3",
"deepmerge": "4.3.1",
"dotenv": "16.4.7",
"mime-types": "3.0.1",
"uuid": "11.1.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "12.1.2",
"@types/mime-types": "2.1.4",
"@types/node": "22.13.14",
"@types/react": "19.0.12",
"rollup": "4.38.0",
"rollup-plugin-copy": "3.5.0"
},
"peerDependencies": {
"@playwright/test": "1.51.1"
}
}

41
e2e-tests/playwright/lib/rollup.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import typescript from '@rollup/plugin-typescript';
import copy from 'rollup-plugin-copy';
export default {
input: 'src/index.ts',
output: [
{
dir: 'dist',
format: 'cjs', // CommonJS for Playwright
sourcemap: true,
preserveModules: true, // Keep file structure
preserveModulesRoot: 'src',
},
],
plugins: [
typescript(),
copy({
targets: [{src: 'src/asset/**/*', dest: 'dist/asset'}], // Copy assets to dist/
}),
],
external: [
'@playwright/test',
'@mattermost/client',
'@mattermost/types/config',
'@axe-core/playwright',
'@percy/playwright',
'dotenv',
'node:fs/promises',
'node:path',
'node:fs',
'node:os',
'mime-types',
'uuid',
'async-wait-until',
'chalk',
'deepmerge',
],
};

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

До

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

После

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

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

@@ -3,11 +3,11 @@
import {writeFile} from 'node:fs/promises'; import {writeFile} from 'node:fs/promises';
import {request, Browser, BrowserContext} from '@playwright/test'; import {Browser, BrowserContext, request} from '@playwright/test';
import {UserProfile} from '@mattermost/types/users'; import {UserProfile} from '@mattermost/types/users';
import testConfig from '@e2e-test.config';
import pages from '@e2e-support/ui/pages'; import {testConfig} from './test_config';
import {pages} from './ui/pages';
export class TestBrowser { export class TestBrowser {
readonly browser: Browser; readonly browser: Browser;

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);
}

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

@@ -3,9 +3,8 @@
import os from 'node:os'; import os from 'node:os';
import {expect} from '@playwright/test'; import {expect, test} from '@playwright/test';
import {test} from './test_fixture';
import {callsPluginId} from './constant'; import {callsPluginId} from './constant';
import {getAdminClient} from './server/init'; import {getAdminClient} from './server/init';
@@ -56,12 +55,17 @@ export async function ensureLicense() {
export async function requestTrialLicense() { export async function requestTrialLicense() {
const {adminClient} = await getAdminClient(); const {adminClient} = await getAdminClient();
const admin = await adminClient.getMe();
try { try {
// @ts-expect-error This may fail requesting for trial license
await adminClient.requestTrialLicense({ await adminClient.requestTrialLicense({
receive_emails_accepted: true, receive_emails_accepted: true,
terms_accepted: true, terms_accepted: true,
users: 100, 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) { } catch (error) {
expect(error, 'Failed to request trial license').toBeFalsy(); expect(error, 'Failed to request trial license').toBeFalsy();

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

@@ -2,21 +2,22 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect} from '@playwright/test'; import {expect} from '@playwright/test';
import {Client4} from '@mattermost/client';
import {UserProfile} from '@mattermost/types/users'; import {UserProfile} from '@mattermost/types/users';
import {PluginManifest} from '@mattermost/types/plugins';
import {PreferenceType} from '@mattermost/types/preferences'; import {PreferenceType} from '@mattermost/types/preferences';
import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server'; import {defaultTeam} from './util';
import {defaultTeam} from './support/util'; import {createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server';
import testConfig from './test.config'; import {testConfig} from './test_config';
async function globalSetup() { export async function baseGlobalSetup() {
let adminClient: Client; let adminClient: Client4;
let adminUser: UserProfile | null; let adminUser: UserProfile | null;
({adminClient, adminUser} = await getAdminClient({skipLog: true})); ({adminClient, adminUser} = await getAdminClient({skipLog: true}));
if (!adminUser) { if (!adminUser) {
const firstClient = new Client(); const firstClient = new Client4();
firstClient.setUrl(testConfig.baseURL); firstClient.setUrl(testConfig.baseURL);
const defaultAdmin = getDefaultAdminUser(); const defaultAdmin = getDefaultAdminUser();
await firstClient.createUser(defaultAdmin, '', ''); await firstClient.createUser(defaultAdmin, '', '');
@@ -25,13 +26,9 @@ async function globalSetup() {
} }
await sysadminSetup(adminClient, adminUser); await sysadminSetup(adminClient, adminUser);
return function () {
// placeholder for teardown setup
};
} }
async function sysadminSetup(client: Client, user: UserProfile | null) { async function sysadminSetup(client: Client4, user: UserProfile | null) {
// Ensure admin's email is verified. // Ensure admin's email is verified.
if (!user) { if (!user) {
await client.verifyUserEmail(client.token); await client.verifyUserEmail(client.token);
@@ -79,7 +76,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) {
await ensureServerDeployment(client); await ensureServerDeployment(client);
} }
async function printLicenseInfo(client: Client) { async function printLicenseInfo(client: Client4) {
const license = await client.getClientLicenseOld(); const license = await client.getClientLicenseOld();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Server License: console.log(`Server License:
@@ -91,7 +88,7 @@ async function printLicenseInfo(client: Client) {
- Users = ${license.Users}`); - Users = ${license.Users}`);
} }
async function printClientInfo(client: Client) { async function printClientInfo(client: Client4) {
const config = await client.getClientConfigOld(); const config = await client.getClientConfigOld();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Build Info: console.log(`Build Info:
@@ -111,7 +108,7 @@ async function printClientInfo(client: Client) {
- LogSettings.EnableDiagnostics = ${LogSettings?.EnableDiagnostics}`); - LogSettings.EnableDiagnostics = ${LogSettings?.EnableDiagnostics}`);
} }
export async function ensurePluginsLoaded(client: Client) { export async function ensurePluginsLoaded(client: Client4) {
const pluginStatus = await client.getPluginStatuses(); const pluginStatus = await client.getPluginStatuses();
const plugins = await client.getPlugins(); const plugins = await client.getPlugins();
@@ -123,7 +120,7 @@ export async function ensurePluginsLoaded(client: Client) {
return; return;
} }
const isActive = plugins.active.some((plugin) => plugin.id === pluginId); const isActive = plugins.active.some((plugin: PluginManifest) => plugin.id === pluginId);
if (!isActive) { if (!isActive) {
await client.enablePlugin(pluginId); await client.enablePlugin(pluginId);
@@ -136,7 +133,7 @@ export async function ensurePluginsLoaded(client: Client) {
}); });
} }
async function printPluginDetails(client: Client) { async function printPluginDetails(client: Client4) {
const plugins = await client.getPlugins(); const plugins = await client.getPlugins();
if (plugins.active.length) { if (plugins.active.length) {
@@ -144,7 +141,7 @@ async function printPluginDetails(client: Client) {
console.log('Active plugins:'); console.log('Active plugins:');
} }
plugins.active.forEach((plugin) => { plugins.active.forEach((plugin: PluginManifest) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`); console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`);
}); });
@@ -154,7 +151,7 @@ async function printPluginDetails(client: Client) {
console.log('Inactive plugins:'); console.log('Inactive plugins:');
} }
plugins.inactive.forEach((plugin) => { plugins.inactive.forEach((plugin: PluginManifest) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`); console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`);
}); });
@@ -163,7 +160,7 @@ async function printPluginDetails(client: Client) {
console.log(''); console.log('');
} }
async function ensureServerDeployment(client: Client) { async function ensureServerDeployment(client: Client4) {
if (testConfig.haClusterEnabled) { if (testConfig.haClusterEnabled) {
const {haClusterNodeCount, haClusterName} = testConfig; const {haClusterNodeCount, haClusterName} = testConfig;
@@ -194,7 +191,7 @@ async function ensureServerDeployment(client: Client) {
} }
} }
async function savePreferences(client: Client, userId: UserProfile['id']) { async function savePreferences(client: Client4, userId: UserProfile['id']) {
try { try {
if (!userId) { if (!userId) {
throw new Error('userId is not defined'); throw new Error('userId is not defined');
@@ -211,5 +208,3 @@ async function savePreferences(client: Client, userId: UserProfile['id']) {
console.log('Error saving preferences', error); console.log('Error saving preferences', error);
} }
} }
export default globalSetup;

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';

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

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {getRandomId} from '@e2e-support/util';
import {Channel, ChannelType} from '@mattermost/types/channels'; import {Channel, ChannelType} from '@mattermost/types/channels';
import {getRandomId} from '@/util';
type ChannelInput = { type ChannelInput = {
teamId: string; teamId: string;
name: string; name: string;

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

@@ -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;
};

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

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import merge from 'deepmerge'; import merge from 'deepmerge';
import { import {
AdminConfig, AdminConfig,
ClusterSettings, ClusterSettings,
@@ -15,7 +14,8 @@ import {
ServiceSettings, ServiceSettings,
TeamSettings, TeamSettings,
} from '@mattermost/types/config'; } from '@mattermost/types/config';
import testConfig from '@e2e-test.config';
import {testConfig} from '@/test_config';
export function getOnPremServerConfig(): AdminConfig { export function getOnPremServerConfig(): AdminConfig {
return merge<AdminConfig>(defaultServerConfig, onPremServerConfig() as AdminConfig); return merge<AdminConfig>(defaultServerConfig, onPremServerConfig() as AdminConfig);

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

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

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

@@ -2,16 +2,16 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect} from '@playwright/test'; import {expect} from '@playwright/test';
import {PreferenceType} from '@mattermost/types/preferences'; import {PreferenceType} from '@mattermost/types/preferences';
import testConfig from '@e2e-test.config';
import {getFileDataFromAsset} from '@e2e-support/file';
import {makeClient} from '.'; import {makeClient} from './client';
import {getOnPremServerConfig} from './default_config'; import {getOnPremServerConfig} from './default_config';
import {createRandomTeam} from './team'; import {createRandomTeam} from './team';
import {createRandomUser} from './user'; import {createRandomUser} from './user';
import {getFileFromCommonAsset} from '@/file';
import {testConfig} from '@/test_config';
export async function initSetup({ export async function initSetup({
userPrefix = 'user', userPrefix = 'user',
teamPrefix = {name: 'team', displayName: 'Team'}, teamPrefix = {name: 'team', displayName: 'Team'},
@@ -30,7 +30,7 @@ export async function initSetup({
} }
// Reset server config // Reset server config
const adminConfig = await adminClient.updateConfig(getOnPremServerConfig()); const adminConfig = await adminClient.updateConfig(getOnPremServerConfig() as any);
// Create new team // Create new team
const team = await adminClient.createTeam(createRandomTeam(teamPrefix.name, teamPrefix.displayName)); const team = await adminClient.createTeam(createRandomTeam(teamPrefix.name, teamPrefix.displayName));
@@ -45,7 +45,7 @@ export async function initSetup({
const {client: userClient} = await makeClient(user); const {client: userClient} = await makeClient(user);
if (withDefaultProfileImage) { if (withDefaultProfileImage) {
const {file} = getFileDataFromAsset('mattermost-icon_128x128.png', 'image/png'); const file = getFileFromCommonAsset('mattermost-icon_128x128.png');
await userClient.uploadProfileImage(user.id, file); await userClient.uploadProfileImage(user.id, file);
} }

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

@@ -2,7 +2,8 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Post, PostMetadata} from '@mattermost/types/posts'; import {Post, PostMetadata} from '@mattermost/types/posts';
import {getRandomId} from '@e2e-support/util';
import {getRandomId} from '@/util';
export function createRandomPost(post?: Partial<Post>): Post { export function createRandomPost(post?: Partial<Post>): Post {
if (post && post.channel_id && post.user_id) { if (post && post.channel_id && post.user_id) {

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

@@ -2,7 +2,8 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Team, TeamType} from '@mattermost/types/teams'; import {Team, TeamType} from '@mattermost/types/teams';
import {getRandomId} from '@e2e-support/util';
import {getRandomId} from '@/util';
export function createRandomTeam(name = 'team', displayName = 'Team', type: TeamType = 'O', unique = true): Team { export function createRandomTeam(name = 'team', displayName = 'Team', type: TeamType = 'O', unique = true): Team {
const randomSuffix = getRandomId(); const randomSuffix = getRandomId();

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

@@ -2,8 +2,9 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {UserProfile} from '@mattermost/types/users'; import {UserProfile} from '@mattermost/types/users';
import {getRandomId} from '@e2e-support/util';
import testConfig from '@e2e-test.config'; import {getRandomId} from '@/util';
import {testConfig} from '@/test_config';
export function createRandomUser(prefix = 'user') { export function createRandomUser(prefix = 'user') {
const randomId = getRandomId(); const randomId = getRandomId();

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

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Locator, Page} from '@playwright/test'; import {Locator, Page} from '@playwright/test';
export {waitUntil} from 'async-wait-until'; export {waitUntil} from 'async-wait-until';
const visibilityHidden = 'visibility: hidden !important;'; const visibilityHidden = 'visibility: hidden !important;';
@@ -19,7 +18,3 @@ export async function waitForAnimationEnd(locator: Locator) {
Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)), Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)),
); );
} }
export async function moveMouseToCenter(page: Page) {
await page.mouse.move(0, 0);
}

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;
}

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

@@ -1,26 +1,37 @@
import {test as base, Browser, Page} from '@playwright/test'; // 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 {AxeResults} from 'axe-core';
import AxeBuilder from '@axe-core/playwright'; import {AxeBuilder} from '@axe-core/playwright';
import {TestBrowser} from './browser_context'; import {TestBrowser} from './browser_context';
import { import {
ensureLicense,
shouldHaveCallsEnabled, shouldHaveCallsEnabled,
shouldHaveFeatureFlag, shouldHaveFeatureFlag,
shouldRunInLinux, shouldRunInLinux,
ensureLicense,
skipIfNoLicense,
skipIfFeatureFlagNotSet, skipIfFeatureFlagNotSet,
skipIfNoLicense,
} from './flag'; } from './flag';
import {initSetup, getAdminClient} from './server'; import {getBlobFromAsset, getFileFromAsset} from './file';
import {
createRandomChannel,
createRandomPost,
createRandomTeam,
createRandomUser,
getAdminClient,
initSetup,
} from './server';
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action'; import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
import pages from './ui/pages'; import {pages} from './ui/pages';
import {matchSnapshot} from './visual'; import {matchSnapshot} from './visual';
import {stubNotification, waitForNotification} from './mock_browser_api'; import {stubNotification, waitForNotification} from './mock_browser_api';
import {duration} from './util'; import {duration, getRandomId, simpleEmailRe, wait} from './util';
export {expect} from '@playwright/test'; export {expect} from '@playwright/test';
type ExtendedFixtures = { export type ExtendedFixtures = {
axe: AxeBuilderExtended; axe: AxeBuilderExtended;
pw: PlaywrightExtended; pw: PlaywrightExtended;
}; };
@@ -43,7 +54,7 @@ export const test = base.extend<ExtendedFixtures>({
}, },
}); });
class PlaywrightExtended { export class PlaywrightExtended {
// ./browser_context // ./browser_context
readonly testBrowser; readonly testBrowser;
@@ -55,6 +66,10 @@ class PlaywrightExtended {
readonly skipIfNoLicense; readonly skipIfNoLicense;
readonly skipIfFeatureFlagNotSet; readonly skipIfFeatureFlagNotSet;
// ./file
readonly getBlobFromAsset;
readonly getFileFromAsset;
// ./server // ./server
readonly getAdminClient; readonly getAdminClient;
readonly initSetup; readonly initSetup;
@@ -64,19 +79,27 @@ class PlaywrightExtended {
readonly waitForAnimationEnd; readonly waitForAnimationEnd;
readonly waitUntil; readonly waitUntil;
// ./mock_browser_api
readonly stubNotification;
readonly waitForNotification;
// ./visual
readonly matchSnapshot;
// ./util
readonly duration;
readonly simpleEmailRe;
readonly wait;
// random
readonly random;
// unauthenticated page // unauthenticated page
readonly loginPage; readonly loginPage;
readonly landingLoginPage; readonly landingLoginPage;
readonly signupPage; readonly signupPage;
readonly resetPasswordPage; readonly resetPasswordPage;
// ./visual
readonly matchSnapshot;
// ./mock_browser_api
readonly stubNotification;
readonly waitForNotification;
readonly hasSeenLandingPage; readonly hasSeenLandingPage;
constructor(browser: Browser, page: Page, isMobile: boolean) { constructor(browser: Browser, page: Page, isMobile: boolean) {
@@ -91,6 +114,10 @@ class PlaywrightExtended {
this.skipIfNoLicense = skipIfNoLicense; this.skipIfNoLicense = skipIfNoLicense;
this.skipIfFeatureFlagNotSet = skipIfFeatureFlagNotSet; this.skipIfFeatureFlagNotSet = skipIfFeatureFlagNotSet;
// ./file
this.getBlobFromAsset = getBlobFromAsset;
this.getFileFromAsset = getFileFromAsset;
// ./server // ./server
this.initSetup = initSetup; this.initSetup = initSetup;
this.getAdminClient = getAdminClient; this.getAdminClient = getAdminClient;
@@ -106,13 +133,26 @@ class PlaywrightExtended {
this.signupPage = new pages.SignupPage(page); this.signupPage = new pages.SignupPage(page);
this.resetPasswordPage = new pages.ResetPasswordPage(page); this.resetPasswordPage = new pages.ResetPasswordPage(page);
// ./visual
this.matchSnapshot = matchSnapshot;
// ./mock_browser_api // ./mock_browser_api
this.stubNotification = stubNotification; this.stubNotification = stubNotification;
this.waitForNotification = waitForNotification; 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 () => { this.hasSeenLandingPage = async () => {
// Visit the base URL to be able to set the localStorage // Visit the base URL to be able to set the localStorage
await page.goto('/'); await page.goto('/');
@@ -121,7 +161,7 @@ class PlaywrightExtended {
} }
} }
class AxeBuilderExtended { export class AxeBuilderExtended {
readonly builder: (page: Page, options?: AxeBuilderOptions) => AxeBuilder; readonly builder: (page: Page, options?: AxeBuilderOptions) => AxeBuilder;
// See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags // See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags

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

@@ -9,29 +9,6 @@ export type TestArgs = {
viewport?: ViewportSize | null; viewport?: ViewportSize | null;
}; };
export type TestConfig = {
// Server
baseURL: string;
adminUsername: string;
adminPassword: string;
adminEmail: string;
ensurePluginsInstalled: string[];
resetBeforeTest: boolean;
haClusterEnabled: boolean;
haClusterNodeCount: number;
haClusterName: string;
pushNotificationServer: string;
// CI
isCI: boolean;
// Playwright
headless: boolean;
slowMo: number;
workers: number;
// Visual tests
snapshotEnabled: boolean;
percyEnabled: boolean;
};
// Based on https://github.com/microsoft/playwright/blob/d6ec1ae3994f127e38b866a231a34efc6a4cac0d/packages/playwright/types/test.d.ts#L5692-L5759 // Based on https://github.com/microsoft/playwright/blob/d6ec1ae3994f127e38b866a231a34efc6a4cac0d/packages/playwright/types/test.d.ts#L5692-L5759
export type ScreenshotOptions = { export type ScreenshotOptions = {
/** /**

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

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

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

@@ -1,11 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
import {components} from '@e2e-support/ui/components'; import ChannelsHeader from './header';
import {waitUntil} from '@e2e-support/test_action'; import ChannelsPostCreate from './post_create';
import {duration} from '@e2e-support/util'; import ChannelsPostEdit from './post_edit';
import ChannelsPost from './post';
import {duration} from '@/util';
import {waitUntil} from '@/test_action';
export default class ChannelsCenterView { export default class ChannelsCenterView {
readonly container: Locator; readonly container: Locator;
@@ -25,12 +29,10 @@ export default class ChannelsCenterView {
constructor(container: Locator) { constructor(container: Locator) {
this.container = container; this.container = container;
this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")'; this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")';
this.header = new components.ChannelsHeader(this.container.locator('.channel-header')); this.header = new ChannelsHeader(this.container.locator('.channel-header'));
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('post-create')); this.postCreate = new ChannelsPostCreate(container.getByTestId('post-create'));
this.scheduledDraftOptions = new components.ChannelsPostCreate( this.scheduledDraftOptions = new ChannelsPostCreate(container.locator('#dropdown_send_post_options'));
container.locator('#dropdown_send_post_options'), this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
);
this.postEdit = new components.ChannelsPostEdit(container.locator('.post-edit__container'));
this.postBoxIndicator = container.locator('div.postBoxIndicator'); this.postBoxIndicator = container.locator('div.postBoxIndicator');
this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator'); this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator');
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span'); this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
@@ -58,7 +60,7 @@ export default class ChannelsCenterView {
async getFirstPost() { async getFirstPost() {
const firstPost = this.container.getByTestId('postView').first(); const firstPost = this.container.getByTestId('postView').first();
await firstPost.waitFor(); await firstPost.waitFor();
return new components.ChannelsPost(firstPost); return new ChannelsPost(firstPost);
} }
/** /**
@@ -67,7 +69,7 @@ export default class ChannelsCenterView {
async getLastPost() { async getLastPost() {
const lastPost = this.container.getByTestId('postView').last(); const lastPost = this.container.getByTestId('postView').last();
await lastPost.waitFor(); await lastPost.waitFor();
return new components.ChannelsPost(lastPost); return new ChannelsPost(lastPost);
} }
/** /**
@@ -89,7 +91,7 @@ export default class ChannelsCenterView {
async getNthPost(index: number) { async getNthPost(index: number) {
const nthPost = this.container.getByTestId('postView').nth(index); const nthPost = this.container.getByTestId('postView').nth(index);
await nthPost.waitFor(); await nthPost.waitFor();
return new components.ChannelsPost(nthPost); return new ChannelsPost(nthPost);
} }
/** /**
@@ -99,7 +101,7 @@ export default class ChannelsCenterView {
async getPostById(id: string) { async getPostById(id: string) {
const postById = this.container.locator(`[id="post_${id}"]`); const postById = this.container.locator(`[id="post_${id}"]`);
await postById.waitFor(); await postById.waitFor();
return new components.ChannelsPost(postById); return new ChannelsPost(postById);
} }
async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) { async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) {
@@ -138,5 +140,3 @@ export default class ChannelsCenterView {
} }
} }
} }
export {ChannelsCenterView};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class DeletePostConfirmationDialog { export default class DeletePostConfirmationDialog {
readonly container: Locator; readonly container: Locator;
@@ -36,5 +36,3 @@ export default class DeletePostConfirmationDialog {
await this.confirmButton.click(); await this.confirmButton.click();
} }
} }
export {DeletePostConfirmationDialog};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class DeletePostModal { export default class DeletePostModal {
readonly container: Locator; readonly container: Locator;
@@ -24,5 +24,3 @@ export default class DeletePostModal {
await expect(this.container).not.toBeVisible(); await expect(this.container).not.toBeVisible();
} }
} }
export {DeletePostModal};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class EmojiGifPicker { export default class EmojiGifPicker {
readonly container: Locator; readonly container: Locator;
@@ -57,5 +57,3 @@ export default class EmojiGifPicker {
}; };
} }
} }
export {EmojiGifPicker};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class FindChannelsModal { export default class FindChannelsModal {
readonly container: Locator; readonly container: Locator;
@@ -19,5 +19,3 @@ export default class FindChannelsModal {
await expect(this.container).toBeVisible(); await expect(this.container).toBeVisible();
} }
} }
export {FindChannelsModal};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
/** /**
* This is the generic confirm modal that is used in the app. * This is the generic confirm modal that is used in the app.
@@ -41,5 +41,3 @@ export default class GenericConfirmModal {
await expect(this.container).not.toBeVisible(); await expect(this.container).not.toBeVisible();
} }
} }
export {GenericConfirmModal};

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

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

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class MessagePriority { export default class MessagePriority {
readonly container: Locator; readonly container: Locator;
@@ -74,5 +74,3 @@ export default class MessagePriority {
await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible(); await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
} }
} }
export {MessagePriority};

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

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
import {components} from '@e2e-support/ui/components'; import PostMenu from './post_menu';
import ThreadFooter from './thread_footer';
export default class ChannelsPost { export default class ChannelsPost {
readonly container: Locator; readonly container: Locator;
@@ -25,8 +26,8 @@ export default class ChannelsPost {
this.removePostButton = container.locator('.post__remove'); this.removePostButton = container.locator('.post__remove');
this.postMenu = new components.PostMenu(container.locator('.post-menu')); this.postMenu = new PostMenu(container.locator('.post-menu'));
this.threadFooter = new components.ThreadFooter(container.locator('.ThreadFooter')); this.threadFooter = new ThreadFooter(container.locator('.ThreadFooter'));
} }
async toBeVisible() { async toBeVisible() {
@@ -72,5 +73,3 @@ export default class ChannelsPost {
await expect(this.container).toContainText(text); await expect(this.container).toContainText(text);
} }
} }
export {ChannelsPost};

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

@@ -1,9 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
import path from 'node:path'; import path from 'node:path';
import {waitUntil} from '@e2e-support/test_action';
import {Locator, expect} from '@playwright/test';
import {duration} from '@/util';
import {assetPath} from '@/file';
import {waitUntil} from '@/test_action';
export default class ChannelsPostCreate { export default class ChannelsPostCreate {
readonly container: Locator; readonly container: Locator;
@@ -15,6 +19,7 @@ export default class ChannelsPostCreate {
readonly scheduleDraftMessageButton; readonly scheduleDraftMessageButton;
readonly priorityButton; readonly priorityButton;
readonly suggestionList; readonly suggestionList;
readonly filePreview;
constructor(container: Locator, isRHS = false) { constructor(container: Locator, isRHS = false) {
this.container = container; this.container = container;
@@ -31,6 +36,7 @@ export default class ChannelsPostCreate {
this.scheduleDraftMessageButton = container.getByLabel('Schedule message'); this.scheduleDraftMessageButton = container.getByLabel('Schedule message');
this.priorityButton = container.getByLabel('Message priority'); this.priorityButton = container.getByLabel('Message priority');
this.suggestionList = container.getByTestId('suggestionList'); this.suggestionList = container.getByTestId('suggestionList');
this.filePreview = container.locator('.file-preview__container');
} }
async toBeVisible() { async toBeVisible() {
@@ -101,18 +107,16 @@ export default class ChannelsPostCreate {
await this.writeMessage(message); await this.writeMessage(message);
if (files) { if (files) {
const filePaths = files.map((file) => path.join(path.resolve(__dirname), '../../../asset', file)); const filePaths = files.map((file) => path.join(assetPath, file));
this.container.page().once('filechooser', async (fileChooser) => { this.container.page().once('filechooser', async (fileChooser) => {
await fileChooser.setFiles(filePaths); await fileChooser.setFiles(filePaths);
}); });
// Click on the attachment button
await this.attachmentButton.click(); await this.attachmentButton.click();
// wait for all files to be uploaded // Wait until the file preview is displayed
await waitUntil(async () => { await this.waitUntilFilePreviewContains(files);
const attachment = await this.container.locator('.file-preview').count();
return attachment === files.length;
});
} }
await this.sendMessage(); await this.sendMessage();
@@ -122,6 +126,18 @@ export default class ChannelsPostCreate {
await expect(this.emojiButton).toBeVisible(); await expect(this.emojiButton).toBeVisible();
await this.emojiButton.click(); await this.emojiButton.click();
} }
}
export {ChannelsPostCreate}; 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},
);
}
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class PostDotMenu { export default class PostDotMenu {
readonly container: Locator; readonly container: Locator;
@@ -46,5 +46,3 @@ export default class PostDotMenu {
await expect(this.container).toBeVisible(); await expect(this.container).toBeVisible();
} }
} }
export {PostDotMenu};

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

@@ -1,9 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
import path from 'node:path'; import path from 'node:path';
import {components} from '@e2e-support/ui/components';
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 { export default class ChannelsPostEdit {
readonly container: Locator; readonly container: Locator;
@@ -23,10 +28,8 @@ export default class ChannelsPostEdit {
this.attachmentButton = container.locator('#fileUploadButton'); this.attachmentButton = container.locator('#fileUploadButton');
this.emojiButton = container.getByLabel('select an emoji'); this.emojiButton = container.getByLabel('select an emoji');
this.sendMessageButton = container.locator('.save'); this.sendMessageButton = container.locator('.save');
this.deleteConfirmationDialog = new components.DeletePostConfirmationDialog( this.deleteConfirmationDialog = new DeletePostConfirmationDialog(container.page().locator('#deletePostModal'));
container.page().locator('#deletePostModal'), this.restorePostConfirmationDialog = new RestorePostConfirmationDialog(
);
this.restorePostConfirmationDialog = new components.RestorePostConfirmationDialog(
container.page().locator('#restorePostModal'), container.page().locator('#restorePostModal'),
); );
} }
@@ -51,7 +54,7 @@ export default class ChannelsPostEdit {
} }
async addFiles(files: string[]) { async addFiles(files: string[]) {
const filePaths = files.map((file) => path.join(path.resolve(__dirname), '../../../asset', file)); const filePaths = files.map((file) => path.join(assetPath, file));
this.container.page().once('filechooser', async (fileChooser) => { this.container.page().once('filechooser', async (fileChooser) => {
await fileChooser.setFiles(filePaths); await fileChooser.setFiles(filePaths);
}); });
@@ -90,5 +93,3 @@ export default class ChannelsPostEdit {
await expect(this.container).toContainText(text); await expect(this.container).toContainText(text);
} }
} }
export {ChannelsPostEdit};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class PostMenu { export default class PostMenu {
readonly container: Locator; readonly container: Locator;
@@ -55,5 +55,3 @@ export default class PostMenu {
await this.dotMenuButton.click(); await this.dotMenuButton.click();
} }
} }
export {PostMenu};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class PostReminderMenu { export default class PostReminderMenu {
readonly container: Locator; readonly container: Locator;
@@ -28,5 +28,3 @@ export default class PostReminderMenu {
await expect(this.container).toBeVisible(); await expect(this.container).toBeVisible();
} }
} }
export {PostReminderMenu};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class RestorePostConfirmationDialog { export default class RestorePostConfirmationDialog {
readonly container: Locator; readonly container: Locator;
@@ -30,5 +30,3 @@ export default class RestorePostConfirmationDialog {
await this.confirmButton.click(); await this.confirmButton.click();
} }
} }
export {RestorePostConfirmationDialog};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class ScheduledDraftMenu { export default class ScheduledDraftMenu {
readonly container: Locator; readonly container: Locator;
@@ -22,5 +22,3 @@ export default class ScheduledDraftMenu {
await this.scheduleDraftMessageCustomTimeOption.click(); await this.scheduleDraftMessageCustomTimeOption.click();
} }
} }
export {ScheduledDraftMenu};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class ScheduledDraftModal { export default class ScheduledDraftModal {
readonly container: Locator; readonly container: Locator;
@@ -93,5 +93,3 @@ export default class ScheduledDraftModal {
return pacificTime; return pacificTime;
} }
} }
export {ScheduledDraftModal};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class SearchPopover { export default class SearchPopover {
readonly container: Locator; readonly container: Locator;
@@ -43,5 +43,3 @@ export default class SearchPopover {
return this.searchHints.locator('.suggestion--selected'); return this.searchHints.locator('.suggestion--selected');
} }
} }
export {SearchPopover};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification'; type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification';
@@ -52,5 +52,3 @@ export default class NotificationsSettings {
await this.container.getByText('Save').click(); await this.container.getByText('Save').click();
} }
} }
export {NotificationsSettings};

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

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
import {NotificationsSettings} from './notification_settings'; import NotificationsSettings from './notification_settings';
export default class SettingsModal { export default class SettingsModal {
readonly container: Locator; readonly container: Locator;
@@ -35,5 +35,3 @@ export default class SettingsModal {
await expect(this.container).not.toBeVisible(); await expect(this.container).not.toBeVisible();
} }
} }
export {SettingsModal};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class ChannelsSidebarLeft { export default class ChannelsSidebarLeft {
readonly container: Locator; readonly container: Locator;
@@ -56,5 +56,3 @@ export default class ChannelsSidebarLeft {
await expect(channel).not.toBeVisible(); await expect(channel).not.toBeVisible();
} }
} }
export {ChannelsSidebarLeft};

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

@@ -1,9 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
import {components} from '@e2e-support/ui/components'; import ChannelsPostCreate from './post_create';
import ChannelsPostEdit from './post_edit';
import ChannelsPost from './post';
export default class ChannelsSidebarRight { export default class ChannelsSidebarRight {
readonly container: Locator; readonly container: Locator;
@@ -28,11 +30,11 @@ export default class ChannelsSidebarRight {
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")'); this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")'); this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
this.rhsPostBody = container.locator('.post-message__text'); this.rhsPostBody = container.locator('.post-message__text');
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('comment-create'), true); this.postCreate = new ChannelsPostCreate(container.getByTestId('comment-create'), true);
this.closeButton = container.locator('.sidebar--right__close'); this.closeButton = container.locator('.sidebar--right__close');
this.editTextbox = container.locator('#edit_textbox'); this.editTextbox = container.locator('#edit_textbox');
this.postEdit = new components.ChannelsPostEdit(container.locator('.post-edit__container')); this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
this.currentVersionEditedPosttext = (postID: any) => container.locator(`#rhsPostMessageText_${postID} p`); this.currentVersionEditedPosttext = (postID: any) => container.locator(`#rhsPostMessageText_${postID} p`);
this.restorePreviousPostVersionIcon = container.locator( this.restorePreviousPostVersionIcon = container.locator(
'button[aria-label="Select to restore an old message."]', 'button[aria-label="Select to restore an old message."]',
@@ -50,7 +52,7 @@ export default class ChannelsSidebarRight {
async getPostById(postId: string) { async getPostById(postId: string) {
const post = this.container.locator(`[id="rhsPost_${postId}"]`); const post = this.container.locator(`[id="rhsPost_${postId}"]`);
await post.waitFor(); await post.waitFor();
return new components.ChannelsPost(post); return new ChannelsPost(post);
} }
/** /**
@@ -59,13 +61,13 @@ export default class ChannelsSidebarRight {
async getLastPost() { async getLastPost() {
const post = this.container.getByTestId('rhsPostView').last(); const post = this.container.getByTestId('rhsPostView').last();
await post.waitFor(); await post.waitFor();
return new components.ChannelsPost(post); return new ChannelsPost(post);
} }
async getFirstPost() { async getFirstPost() {
const post = this.container.getByTestId('rhsPostView').first(); const post = this.container.getByTestId('rhsPostView').first();
await post.waitFor(); await post.waitFor();
return new components.ChannelsPost(post); return new ChannelsPost(post);
} }
/** /**
@@ -96,5 +98,3 @@ export default class ChannelsSidebarRight {
await this.restorePreviousPostVersionIcon.click(); await this.restorePreviousPostVersionIcon.click();
} }
} }
export {ChannelsSidebarRight};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class ThreadFooter { export default class ThreadFooter {
readonly container: Locator; readonly container: Locator;
@@ -26,5 +26,3 @@ export default class ThreadFooter {
await this.replyButton.click(); await this.replyButton.click();
} }
} }
export {ThreadFooter};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class UserProfilePopover { export default class UserProfilePopover {
readonly container: Locator; readonly container: Locator;
@@ -18,5 +18,3 @@ export default class UserProfilePopover {
await this.container.getByLabel('Close user profile popover').click(); await this.container.getByLabel('Close user profile popover').click();
} }
} }
export {UserProfilePopover};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class Footer { export default class Footer {
readonly container: Locator; readonly container: Locator;
@@ -26,5 +26,3 @@ export default class Footer {
await expect(this.copyright).toBeVisible(); await expect(this.copyright).toBeVisible();
} }
} }
export {Footer};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class GlobalHeader { export default class GlobalHeader {
readonly container: Locator; readonly container: Locator;
@@ -49,5 +49,3 @@ export default class GlobalHeader {
await this.searchBox.getByTestId('searchBoxClose').click(); await this.searchBox.getByTestId('searchBoxClose').click();
} }
} }
export {GlobalHeader};

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

@@ -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,
};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class MainHeader { export default class MainHeader {
readonly container: Locator; readonly container: Locator;
@@ -20,5 +20,3 @@ export default class MainHeader {
await expect(this.container).toBeVisible(); await expect(this.container).toBeVisible();
} }
} }
export {MainHeader};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class SystemConsoleNavbar { export default class SystemConsoleNavbar {
readonly container: Locator; readonly container: Locator;
@@ -14,5 +14,3 @@ export default class SystemConsoleNavbar {
await expect(this.container).toBeVisible(); await expect(this.container).toBeVisible();
} }
} }
export {SystemConsoleNavbar};

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

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

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

@@ -21,5 +21,3 @@ export default class FeatureDiscovery {
await expect(this.container.getByTestId('featureDiscovery_title')).toHaveText(title); await expect(this.container.getByTestId('featureDiscovery_title')).toHaveText(title);
} }
} }
export {FeatureDiscovery};

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

@@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
/** /**
* The dropdown menu which appears for both Role and Status filter. * The dropdown menu which appears for both Role and Status filter.
*/ */
class SystemUsersFilterMenu { export default class SystemUsersFilterMenu {
readonly container: Locator; readonly container: Locator;
constructor(container: Locator) { constructor(container: Locator) {
@@ -42,5 +42,3 @@ class SystemUsersFilterMenu {
await this.container.press('Escape'); await this.container.press('Escape');
} }
} }
export {SystemUsersFilterMenu};

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

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
class SystemUsersFilterPopover { export default class SystemUsersFilterPopover {
readonly container: Locator; readonly container: Locator;
readonly teamMenuInput: Locator; readonly teamMenuInput: Locator;
@@ -66,5 +66,3 @@ class SystemUsersFilterPopover {
await expect(this.container).not.toBeVisible(); await expect(this.container).not.toBeVisible();
} }
} }
export {SystemUsersFilterPopover};

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

@@ -77,5 +77,3 @@ export default class MobileSecurity {
await this.saveButton.click(); await this.saveButton.click();
} }
} }
export {MobileSecurity};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
/** /**
* System Console -> User Management -> Users * System Console -> User Management -> Users
@@ -130,5 +130,3 @@ export default class SystemUsers {
await expect(foundUser).not.toBeVisible(); await expect(foundUser).not.toBeVisible();
} }
} }
export {SystemUsers};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test'; import {Locator, expect} from '@playwright/test';
export default class SystemConsoleSidebar { export default class SystemConsoleSidebar {
readonly container: Locator; readonly container: Locator;
@@ -37,5 +37,3 @@ export default class SystemConsoleSidebar {
await this.searchInput.fill(itemName); await this.searchInput.fill(itemName);
} }
} }
export {SystemConsoleSidebar};

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

@@ -3,7 +3,7 @@
import {Page} from '@playwright/test'; import {Page} from '@playwright/test';
import {components} from '@e2e-support/ui/components'; import {components} from '@/ui/components';
export default class ChannelsPage { export default class ChannelsPage {
readonly channels = 'Channels'; readonly channels = 'Channels';
@@ -19,6 +19,7 @@ export default class ChannelsPage {
readonly sidebarRight; readonly sidebarRight;
readonly appBar; readonly appBar;
readonly userProfilePopover; readonly userProfilePopover;
readonly messagePriority;
readonly findChannelsModal; readonly findChannelsModal;
readonly deletePostModal; readonly deletePostModal;
@@ -40,6 +41,7 @@ export default class ChannelsPage {
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer')); this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
this.messagePriority = new components.MessagePriority(page.locator('body'));
// Modals // Modals
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'})); this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
@@ -88,5 +90,3 @@ export default class ChannelsPage {
await this.centerView.postCreate.postMessage(message); await this.centerView.postCreate.postMessage(message);
} }
} }
export {ChannelsPage};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
export default class DraftPage { export default class DraftPage {
readonly page: Page; readonly page: Page;
@@ -124,5 +124,3 @@ export default class DraftPage {
await this.confirmbutton.click(); await this.confirmbutton.click();
} }
} }
export {DraftPage};

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

@@ -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,
};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
export default class LandingLoginPage { export default class LandingLoginPage {
readonly page: Page; readonly page: Page;
@@ -37,5 +37,3 @@ export default class LandingLoginPage {
await this.page.goto('/landing#/login'); await this.page.goto('/landing#/login');
} }
} }
export {LandingLoginPage};

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

@@ -1,11 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
import {UserProfile} from '@mattermost/types/users'; import {UserProfile} from '@mattermost/types/users';
import {components} from '@e2e-support/ui/components'; import {components} from '@/ui/components';
export default class LoginPage { export default class LoginPage {
readonly page: Page; readonly page: Page;
@@ -67,5 +66,3 @@ export default class LoginPage {
await Promise.all([this.page.waitForNavigation(), this.signInButton.click()]); await Promise.all([this.page.waitForNavigation(), this.signInButton.click()]);
} }
} }
export {LoginPage};

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

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
import {components} from '@e2e-support/ui/components'; import {components} from '@/ui/components';
export default class ResetPasswordPage { export default class ResetPasswordPage {
readonly page: Page; readonly page: Page;
@@ -47,5 +47,3 @@ export default class ResetPasswordPage {
await Promise.all([this.page.waitForNavigation(), this.resetButton.click()]); await Promise.all([this.page.waitForNavigation(), this.resetButton.click()]);
} }
} }
export {ResetPasswordPage};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
export default class ScheduledDraftPage { export default class ScheduledDraftPage {
readonly page: Page; readonly page: Page;
@@ -157,5 +157,3 @@ export default class ScheduledDraftPage {
await this.copyIcon.click(); await this.copyIcon.click();
} }
} }
export {ScheduledDraftPage};

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

@@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test'; import {Page, expect} from '@playwright/test';
import {duration, wait} from '@e2e-support/util'; import {duration, wait} from '@/util';
import {components} from '@e2e-support/ui/components'; import {components} from '@/ui/components';
export default class SignupPage { export default class SignupPage {
readonly page: Page; readonly page: Page;
@@ -86,5 +86,3 @@ export default class SignupPage {
} }
} }
} }
export {SignupPage};

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

@@ -2,9 +2,10 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Page} from '@playwright/test'; import {Page} from '@playwright/test';
import {components} from '../components';
class SystemConsolePage { import {components} from '@/ui/components';
export default class SystemConsolePage {
readonly page: Page; readonly page: Page;
readonly sidebar; readonly sidebar;
@@ -39,10 +40,10 @@ class SystemConsolePage {
// Sections and sub-sections // Sections and sub-sections
this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection')); this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection'));
this.mobileSecurity = new components.MobileSecurity( this.mobileSecurity = new components.SystemConsoleMobileSecurity(
page.getByTestId('sysconsole_section_MobileSecuritySettings'), page.getByTestId('sysconsole_section_MobileSecuritySettings'),
); );
this.featureDiscovery = new components.FeatureDiscovery(page.getByTestId('featureDiscovery')); this.featureDiscovery = new components.SystemConsoleFeatureDiscovery(page.getByTestId('featureDiscovery'));
// Menus & Popovers // Menus & Popovers
this.systemUsersFilterPopover = new components.SystemUsersFilterPopover( this.systemUsersFilterPopover = new components.SystemUsersFilterPopover(
@@ -84,5 +85,3 @@ class SystemConsolePage {
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click(); await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click();
} }
} }
export {SystemConsolePage};

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

@@ -4,14 +4,14 @@
import os from 'node:os'; import os from 'node:os';
import chalk from 'chalk'; import chalk from 'chalk';
import {expect, TestInfo} from '@playwright/test'; import {TestInfo, expect} from '@playwright/test';
import {duration, illegalRe, wait} from '@e2e-support/util';
import testConfig from '@e2e-test.config';
import {ScreenshotOptions, TestArgs} from '@e2e-types';
import snapshotWithPercy from './percy'; 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 = {}) { export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs, options: ScreenshotOptions = {}) {
if (os.platform() !== 'linux') { if (os.platform() !== 'linux') {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

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

@@ -3,8 +3,8 @@
import percySnapshot from '@percy/playwright'; import percySnapshot from '@percy/playwright';
import testConfig from '@e2e-test.config'; import {testConfig} from '@/test_config';
import {TestArgs} from '@e2e-types'; import {TestArgs} from '@/types';
export default async function snapshotWithPercy(name: string, testArgs: TestArgs) { export default async function snapshotWithPercy(name: string, testArgs: TestArgs) {
if (testArgs.browserName === 'chromium' && testConfig.percyEnabled && testArgs.viewport) { if (testArgs.browserName === 'chromium' && testConfig.percyEnabled && testArgs.viewport) {

20
e2e-tests/playwright/lib/tsconfig.json Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2022",
"esModuleInterop": true,
"strict": true,
"declaration": true,
"moduleResolution": "node",
"module": "esnext",
"resolveJsonModule": true,
"skipLibCheck": true,
"downlevelIteration": true,
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}

10101
e2e-tests/playwright/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -1,42 +1,34 @@
{ {
"scripts": { "name": "mattermost-playwright",
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test", "private": true,
"test:update-snapshots": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots", "workspaces": [
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad", "lib",
"tsc": "tsc -b", "test"
"lint": "eslint .", ],
"prettier": "prettier . --check", "scripts": {
"prettier:fix": "prettier --write .", "postinstall": "npm run build",
"check": "npm run tsc && npm run lint && npm run prettier", "build": "npm run build --workspaces --if-present",
"codegen": "cross-env playwright codegen $PW_BASE_URL", "build:watch": "npm run build:watch --workspaces --if-present",
"playwright-ui": "cross-env playwright test --ui", "clean": "rm -rf node_modules logs results test-results && npm run clean --workspaces --if-present",
"test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test", "tsc": "npm run tsc --workspaces --if-present",
"show-report": "npx playwright show-report", "lint": "eslint .",
"postinstall": "script/post_install.sh" "prettier": "prettier . --check",
}, "prettier:fix": "prettier --write .",
"dependencies": { "check": "npm run lint && npm run prettier && npm run tsc",
"@axe-core/playwright": "4.10.1", "test": "npm run test --workspace=test",
"@percy/cli": "1.30.6", "test:ci": "npm run test:ci --workspace=test"
"@percy/playwright": "1.0.7", },
"@playwright/test": "1.49.1", "dependencies": {
"async-wait-until": "2.0.18", "dayjs": "1.11.13",
"axe-core": "4.10.2", "@playwright/test": "1.51.1"
"chalk": "4.1.2", },
"dayjs": "1.11.13", "devDependencies": {
"deepmerge": "4.3.1", "@typescript-eslint/eslint-plugin": "8.28.0",
"dotenv": "16.4.7", "eslint": "9.23.0",
"form-data-encoder": "4.0.2", "eslint-import-resolver-typescript": "4.2.7",
"formdata-node": "6.0.3", "eslint-plugin-header": "3.1.1",
"uuid": "11.0.5", "eslint-plugin-import": "2.31.0",
"zod": "3.24.1" "prettier": "3.5.3",
}, "typescript": "5.7.3"
"devDependencies": { }
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"cross-env": "7.0.3",
"eslint": "9.18.0",
"prettier": "3.4.2",
"typescript": "5.7.3"
}
} }

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

@@ -1,6 +1,10 @@
#!/usr/bin/env node // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs'); const fs = require('fs');
const dayjs = require('dayjs'); const dayjs = require('dayjs');
const duration = require('dayjs/plugin/duration'); const duration = require('dayjs/plugin/duration');
dayjs.extend(duration); dayjs.extend(duration);
@@ -90,5 +94,5 @@ function generateWebhookBody() {
}; };
} }
let webhookBody = generateWebhookBody(); const webhookBody = generateWebhookBody();
process.stdout.write(JSON.stringify(webhookBody)); process.stdout.write(JSON.stringify(webhookBody));

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

@@ -1,65 +0,0 @@
// 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';
const assetPath = path.resolve(__dirname, 'asset');
/**
* Reads file data and creates a File object.
* @param filePath - The path to the file.
* @param mimeType - The MIME type of the file.
* @returns A File object containing the file data.
* @throws If the file does not exist.
*/
export function getFileData(filePath: string, mimeType: string): File {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found at path: ${filePath}`);
}
const fileName = path.basename(filePath);
const fileBuffer = fs.readFileSync(filePath);
return new File([fileBuffer], fileName, {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.
* @param mimeType - The MIME type of the file.
* @returns An object containing a File object and the filename.
*/
export function getFileDataFromAsset(filename: string, mimeType: string) {
const filePath = path.join(assetPath, filename);
return {file: getFileData(filePath, mimeType), filename: path.basename(filePath)};
}
/**
* Reads file data and creates a Blob object.
* @param filePath - The path to the file.
* @param mimeType - The MIME type of the file.
* @returns A Blob object containing the file data.
* @throws If the file does not exist.
*/
export function getBlobData(filePath: string, mimeType: string): Blob {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found at path: ${filePath}`);
}
const fileBuffer = fs.readFileSync(filePath);
return new Blob([fileBuffer], {type: mimeType});
}
/**
* Reads file data from the "asset" directory and creates a Blob object.
* @param filename - The name of the file in the "asset" directory.
* @param mimeType - The MIME type of the file.
* @returns An object containing a Blob object and the filename.
*/
export function getBlobDataFromAsset(filename: string, mimeType: string) {
const filePath = path.join(assetPath, filename);
return {blob: getBlobData(filePath, mimeType), filename: path.basename(filePath)};
}

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

@@ -1,162 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This is based on "webapp/platform/client/src/client4.ts". Modified for node client.
// Update should be made in comparison with the base Client4.
import fs from 'node:fs';
import path from 'node:path';
import stream from 'stream';
import {FormData} from 'formdata-node';
import {FormDataEncoder} from 'form-data-encoder';
import testConfig from '@e2e-test.config';
import Client4 from '@mattermost/client/client4';
import {Options, StatusOK} from '@mattermost/types/client4';
import {License} from '@mattermost/types/config';
import {CustomEmoji} from '@mattermost/types/emojis';
import {PluginManifest} from '@mattermost/types/plugins';
import {UserProfile} from '@mattermost/types/users';
export default class Client extends Client4 {
getFormDataOptions = (formData: FormData): Options => {
const encoder = new FormDataEncoder(formData);
return {
method: 'post',
body: stream.Readable.from(encoder.encode()),
headers: encoder.headers,
duplex: 'half',
};
};
readFileAsFormData = (filePath: string, key: string, additionalFields: Record<string, any> = {}) => {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append(key, new Blob([fileData]), path.basename(filePath));
// Append additional fields if provided
for (const [field, value] of Object.entries(additionalFields)) {
formData.append(field, value);
}
return formData;
};
uploadProfileImageX = (userId: string, filePath: string) => {
const formData = this.readFileAsFormData(filePath, 'image');
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getUserRoute(userId)}/image`, options);
};
setTeamIconX = (teamId: string, filePath: string) => {
const formData = this.readFileAsFormData(filePath, 'image');
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getTeamRoute(teamId)}/image`, options);
};
createCustomEmojiX = (emoji: CustomEmoji, filePath: string) => {
const formData = this.readFileAsFormData(filePath, 'image', {emoji: JSON.stringify(emoji)});
const options = this.getFormDataOptions(formData);
return this.doFetch<CustomEmoji>(`${this.getEmojisRoute()}`, options);
};
uploadBrandImageX = (filePath: string) => {
const formData = this.readFileAsFormData(filePath, 'image');
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(`${this.getBrandRoute()}/image`, options);
};
uploadCertificateX = (filePath: string, route: string) => {
const formData = this.readFileAsFormData(filePath, 'certificate');
const options = this.getFormDataOptions(formData);
return this.doFetch<StatusOK>(route, options);
};
uploadPublicSamlCertificateX = (filePath: string) => {
return this.uploadCertificateX(filePath, `${this.getBaseRoute()}/saml/certificate/public`);
};
uploadPrivateSamlCertificateX = (filePath: string) => {
return this.uploadCertificateX(filePath, `${this.getBaseRoute()}/saml/certificate/private`);
};
uploadPublicLdapCertificateX = (filePath: string) => {
return this.uploadCertificateX(filePath, `${this.getBaseRoute()}/ldap/certificate/public`);
};
uploadPrivateLdapCertificateX = (filePath: string) => {
return this.uploadCertificateX(filePath, `${this.getBaseRoute()}/ldap/certificate/private`);
};
uploadIdpSamlCertificateX = (filePath: string) => {
return this.uploadCertificateX(filePath, `${this.getBaseRoute()}/saml/certificate/idp`);
};
uploadLicenseX = (filePath: string) => {
const formData = this.readFileAsFormData(filePath, 'license');
const options = this.getFormDataOptions(formData);
return this.doFetch<License>(`${this.getBaseRoute()}/license`, options);
};
uploadPluginX = async (filePath: string, force = false) => {
const additionalFields = force ? {force: 'true'} : {};
const formData = this.readFileAsFormData(filePath, 'plugin', additionalFields);
const options = this.getFormDataOptions(formData);
return this.doFetch<PluginManifest>(this.getPluginsRoute(), options);
};
}
// Variable to hold cache
const clients: Record<string, ClientCache> = {};
async function makeClient(
userRequest?: UserRequest,
opts: {useCache?: boolean; skipLog?: boolean} = {useCache: true, skipLog: false},
): Promise<ClientCache> {
const client = new Client();
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: Client;
user: UserProfile | null;
};
export {Client, makeClient};

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

@@ -1,98 +0,0 @@
// 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 '@e2e-support/ui/components/channels/post_edit';
import DeletePostConfirmationDialog from '@e2e-support/ui/components/channels/delete_post_confirmation_dialog';
import RestorePostConfirmationDialog from '@e2e-support/ui/components/channels/restore_post_confirmation_dialog';
import MobileSecurity from './system_console/sections/system_users/mobile_security';
import FeatureDiscovery from './system_console/sections/system_users/feature_discovery';
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,
MessagePriority,
UserProfilePopover,
DeletePostConfirmationDialog,
RestorePostConfirmationDialog,
MobileSecurity,
FeatureDiscovery,
};
export {
components,
GlobalHeader,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,
ChannelsAppBar,
ChannelsHeader,
ChannelsPostCreate,
ChannelsPostEdit,
ChannelsPost,
FindChannelsModal,
DeletePostModal,
PostDotMenu,
PostMenu,
ThreadFooter,
MessagePriority,
DeletePostConfirmationDialog,
};

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

@@ -1,25 +0,0 @@
// 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 {ChannelsPage, LandingLoginPage, LoginPage, SignupPage, ScheduledDraftPage, DraftPage};
export default pages;

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

@@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as dotenv from 'dotenv';
import {TestConfig} from '@e2e-types';
dotenv.config();
// All process.env should be defined here
const config: TestConfig = {
// Server
baseURL: process.env.PW_BASE_URL || 'http://localhost:8065',
adminUsername: process.env.PW_ADMIN_USERNAME || 'sysadmin',
adminPassword: process.env.PW_ADMIN_PASSWORD || 'Sys@dmin-sample1',
adminEmail: process.env.PW_ADMIN_EMAIL || 'sysadmin@sample.mattermost.com',
ensurePluginsInstalled:
typeof process.env?.PW_ENSURE_PLUGINS_INSTALLED === 'string'
? process.env.PW_ENSURE_PLUGINS_INSTALLED.split(',').filter((plugin) => Boolean(plugin))
: [],
haClusterEnabled: parseBool(process.env.PW_HA_CLUSTER_ENABLED, false),
haClusterNodeCount: parseNumber(process.env.PW_HA_CLUSTER_NODE_COUNT, 2),
haClusterName: process.env.PW_HA_CLUSTER_NAME || 'mm_dev_cluster',
pushNotificationServer: process.env.PW_PUSH_NOTIFICATION_SERVER || 'https://push-test.mattermost.com',
resetBeforeTest: parseBool(process.env.PW_RESET_BEFORE_TEST, false),
// CI
isCI: !!process.env.CI,
// Playwright
headless: parseBool(process.env.PW_HEADLESS, true),
slowMo: parseNumber(process.env.PW_SLOWMO, 0),
workers: parseNumber(process.env.PW_WORKERS, 1),
// Visual tests
snapshotEnabled: parseBool(process.env.PW_SNAPSHOT_ENABLE, false),
percyEnabled: parseBool(process.env.PW_PERCY_ENABLE, false),
};
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;
}
export default config;

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

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

@@ -40,10 +40,10 @@ npm run test
#### 1. Run docker container using latest focal version #### 1. Run docker container using latest focal version
Change to the root directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.) Change to the `e2e-tests/playwright` directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
``` ```
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.49.1-noble /bin/bash docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.51.1-noble /bin/bash
``` ```
#### 2. Inside the docker container #### 2. Inside the docker container
@@ -51,23 +51,26 @@ docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwr
``` ```
export PW_BASE_URL=http://host.docker.internal:8065 export PW_BASE_URL=http://host.docker.internal:8065
export PW_HEADLESS=true export PW_HEADLESS=true
cd mattermost/e2e-tests/playwright cd mattermost
# Install npm packages. Use "npm ci" to match the automated environment # Install npm packages. Use "npm ci" to match the automated environment
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci
# Run specific test. See https://playwright.dev/docs/test-cli. # Run specific test. See https://playwright.dev/docs/test-cli.
npm run test -- login --project=chrome npm run test -- -- login --project=chrome
# Or run all tests # Or run all tests
npm run test npm run test
# Update snapshots # Run visual tests
npm run test -- login --update-snapshots npm run test -- -- visual
# Update snapshots of visual tests
npm run test -- -- visual --update-snapshots
``` ```
## Page/Component Object Model ## Page/Component Object Model
See https://playwright.dev/docs/test-pom. See https://playwright.dev/docs/test-pom.
Page and component abstractions are located at `./support/ui`. They should be established before writing a spec file so that any future changes in the DOM structure will be made in one place only. No static UI text or fixed locator should be written in the spec file. Page and component abstractions are in shared library located at `./lib/src/ui`. They should be established before writing a spec file so that any future changes in the DOM structure will be made in one place only. No static UI text or fixed locator should be written in the spec file.

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

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

После

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

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

До

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

После

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

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

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {baseGlobalSetup, testConfig} from '@mattermost/playwright-lib';
async function globalSetup() {
try {
await baseGlobalSetup();
} catch (error: unknown) {
// eslint-disable-next-line no-console
console.error(error);
throw new Error(
`Global setup failed.\n\tEnsure the server at ${testConfig.baseURL} is running and accessible.\n\tPlease check the logs for more details.`,
);
}
return function () {
// placeholder for teardown setup
};
}
export default globalSetup;

26
e2e-tests/playwright/test/package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
{
"name": "@mattermost/playwright-test",
"version": "0.0.1",
"scripts": {
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test",
"test:ci": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome",
"test:update-snapshots": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots",
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad",
"tsc": "tsc -b",
"codegen": "cross-env playwright codegen $PW_BASE_URL",
"playwright-ui": "cross-env playwright test --ui",
"test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
"show-report": "npx playwright show-report",
"postinstall": "script/post_install.sh",
"clean": "rm -rf dist node_modules *.tsbuildinfo logs results storage_state test-results"
},
"dependencies": {
"@mattermost/client": "file:../../../webapp/platform/client",
"@mattermost/playwright-lib": "*",
"@mattermost/types": "file:../../../webapp/platform/types",
"zod": "3.24.2"
},
"devDependencies": {
"cross-env": "7.0.3"
}
}

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

@@ -2,16 +2,14 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {defineConfig, devices} from '@playwright/test'; import {defineConfig, devices} from '@playwright/test';
import {duration, testConfig} from '@mattermost/playwright-lib';
import {duration} from '@e2e-support/util';
import testConfig from '@e2e-test.config';
export default defineConfig({ export default defineConfig({
globalSetup: require.resolve('./global_setup'), globalSetup: './global_setup.ts',
forbidOnly: testConfig.isCI, forbidOnly: testConfig.isCI,
outputDir: './results/output', outputDir: './results/output',
retries: testConfig.isCI ? 2 : 0, retries: testConfig.isCI ? 2 : 0,
testDir: 'tests', testDir: 'specs',
timeout: duration.one_min, timeout: duration.one_min,
workers: testConfig.workers, workers: testConfig.workers,
expect: { expect: {

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

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {expect, test} from '@e2e-support/test_fixture'; import {expect, test} from '@mattermost/playwright-lib';
test.fixme('Base channel accessibility', async ({pw, axe}) => { test.fixme('Base channel accessibility', async ({pw, axe}) => {
// # Create and sign in a new user // # Create and sign in a new user

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше