mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
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
This commit is contained in:
parent
ce9632cca3
commit
a47269cfe2
163 changed files with 7203 additions and 5048 deletions
2
.github/workflows/e2e-fulltests-ci.yml
vendored
2
.github/workflows/e2e-fulltests-ci.yml
vendored
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
SERVER: "${{ steps.generate.outputs.SERVER }}"
|
||||
ENABLED_DOCKER_SERVICES: "${{ steps.generate.outputs.ENABLED_DOCKER_SERVICES }}"
|
||||
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 }}"
|
||||
TM4J_ENABLE: "${{ steps.generate.outputs.TM4J_ENABLE }}"
|
||||
REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}"
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ case "${TEST:-$TEST_DEFAULT}" in
|
|||
cypress )
|
||||
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;;
|
||||
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='' ;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ case "$TEST" in
|
|||
if [ -n "$WEBHOOK_URL" ]; then
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
|
||||
# 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
|
||||
;;
|
||||
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
|
||||
echo '
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.49.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.51.1-noble
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
|
||||
env_file:
|
||||
|
|
|
|||
|
|
@ -37,13 +37,18 @@ EOF
|
|||
|
||||
# Run Playwright test
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
{
|
||||
passed: .stats.expected,
|
||||
|
|
|
|||
16
e2e-tests/playwright/.gitignore
vendored
Normal file
16
e2e-tests/playwright/.gitignore
vendored
Normal file
|
|
@ -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 js from '@eslint/js';
|
||||
import {FlatCompat} from '@eslint/eslintrc';
|
||||
import eslintPluginHeader from 'eslint-plugin-header';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
|
@ -14,18 +15,23 @@ const compat = new FlatCompat({
|
|||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
eslintPluginHeader.rules.header.meta.schema = false;
|
||||
|
||||
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) => ({
|
||||
...config,
|
||||
files: ['**/*.ts', '**/*.s'],
|
||||
})),
|
||||
...compat
|
||||
.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:import/recommended')
|
||||
.map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.ts', '**/*.js'],
|
||||
})),
|
||||
{
|
||||
files: ['**/*.ts', '**/*.s'],
|
||||
files: ['**/*.ts', '**/*.js'],
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
header: eslintPluginHeader,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
|
@ -35,11 +41,31 @@ export default [
|
|||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'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
Normal file
214
e2e-tests/playwright/lib/README.md
Normal file
|
|
@ -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
Normal file
68
e2e-tests/playwright/lib/package.json
Normal file
|
|
@ -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
Normal file
41
e2e-tests/playwright/lib/rollup.config.js
Normal file
|
|
@ -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',
|
||||
],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
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 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 {
|
||||
readonly browser: Browser;
|
||||
93
e2e-tests/playwright/lib/src/file.ts
Normal file
93
e2e-tests/playwright/lib/src/file.ts
Normal file
|
|
@ -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 {expect} from '@playwright/test';
|
||||
import {expect, test} from '@playwright/test';
|
||||
|
||||
import {test} from './test_fixture';
|
||||
import {callsPluginId} from './constant';
|
||||
import {getAdminClient} from './server/init';
|
||||
|
||||
|
|
@ -56,12 +55,17 @@ export async function ensureLicense() {
|
|||
|
||||
export async function requestTrialLicense() {
|
||||
const {adminClient} = await getAdminClient();
|
||||
const admin = await adminClient.getMe();
|
||||
try {
|
||||
// @ts-expect-error This may fail requesting for trial license
|
||||
await adminClient.requestTrialLicense({
|
||||
receive_emails_accepted: true,
|
||||
terms_accepted: true,
|
||||
users: 100,
|
||||
contact_name: admin.first_name + ' ' + admin.last_name,
|
||||
contact_email: admin.email,
|
||||
company_name: 'Mattermost Playwright E2E Tests',
|
||||
company_size: '101-250',
|
||||
company_country: 'United States',
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error, 'Failed to request trial license').toBeFalsy();
|
||||
|
|
@ -2,21 +2,22 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {PluginManifest} from '@mattermost/types/plugins';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server';
|
||||
import {defaultTeam} from './support/util';
|
||||
import testConfig from './test.config';
|
||||
import {defaultTeam} from './util';
|
||||
import {createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server';
|
||||
import {testConfig} from './test_config';
|
||||
|
||||
async function globalSetup() {
|
||||
let adminClient: Client;
|
||||
export async function baseGlobalSetup() {
|
||||
let adminClient: Client4;
|
||||
let adminUser: UserProfile | null;
|
||||
({adminClient, adminUser} = await getAdminClient({skipLog: true}));
|
||||
|
||||
if (!adminUser) {
|
||||
const firstClient = new Client();
|
||||
const firstClient = new Client4();
|
||||
firstClient.setUrl(testConfig.baseURL);
|
||||
const defaultAdmin = getDefaultAdminUser();
|
||||
await firstClient.createUser(defaultAdmin, '', '');
|
||||
|
|
@ -25,13 +26,9 @@ async function globalSetup() {
|
|||
}
|
||||
|
||||
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.
|
||||
if (!user) {
|
||||
await client.verifyUserEmail(client.token);
|
||||
|
|
@ -79,7 +76,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) {
|
|||
await ensureServerDeployment(client);
|
||||
}
|
||||
|
||||
async function printLicenseInfo(client: Client) {
|
||||
async function printLicenseInfo(client: Client4) {
|
||||
const license = await client.getClientLicenseOld();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Server License:
|
||||
|
|
@ -91,7 +88,7 @@ async function printLicenseInfo(client: Client) {
|
|||
- Users = ${license.Users}`);
|
||||
}
|
||||
|
||||
async function printClientInfo(client: Client) {
|
||||
async function printClientInfo(client: Client4) {
|
||||
const config = await client.getClientConfigOld();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Build Info:
|
||||
|
|
@ -111,7 +108,7 @@ async function printClientInfo(client: Client) {
|
|||
- LogSettings.EnableDiagnostics = ${LogSettings?.EnableDiagnostics}`);
|
||||
}
|
||||
|
||||
export async function ensurePluginsLoaded(client: Client) {
|
||||
export async function ensurePluginsLoaded(client: Client4) {
|
||||
const pluginStatus = await client.getPluginStatuses();
|
||||
const plugins = await client.getPlugins();
|
||||
|
||||
|
|
@ -123,7 +120,7 @@ export async function ensurePluginsLoaded(client: Client) {
|
|||
return;
|
||||
}
|
||||
|
||||
const isActive = plugins.active.some((plugin) => plugin.id === pluginId);
|
||||
const isActive = plugins.active.some((plugin: PluginManifest) => plugin.id === pluginId);
|
||||
if (!isActive) {
|
||||
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();
|
||||
|
||||
if (plugins.active.length) {
|
||||
|
|
@ -144,7 +141,7 @@ async function printPluginDetails(client: Client) {
|
|||
console.log('Active plugins:');
|
||||
}
|
||||
|
||||
plugins.active.forEach((plugin) => {
|
||||
plugins.active.forEach((plugin: PluginManifest) => {
|
||||
// eslint-disable-next-line no-console
|
||||
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:');
|
||||
}
|
||||
|
||||
plugins.inactive.forEach((plugin) => {
|
||||
plugins.inactive.forEach((plugin: PluginManifest) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` - ${plugin.id}@${plugin.version} | min_server@${plugin.min_server_version}`);
|
||||
});
|
||||
|
|
@ -163,7 +160,7 @@ async function printPluginDetails(client: Client) {
|
|||
console.log('');
|
||||
}
|
||||
|
||||
async function ensureServerDeployment(client: Client) {
|
||||
async function ensureServerDeployment(client: Client4) {
|
||||
if (testConfig.haClusterEnabled) {
|
||||
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 {
|
||||
if (!userId) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
22
e2e-tests/playwright/lib/src/index.ts
Normal file
22
e2e-tests/playwright/lib/src/index.ts
Normal file
|
|
@ -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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getRandomId} from '@e2e-support/util';
|
||||
import {Channel, ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
|
||||
type ChannelInput = {
|
||||
teamId: string;
|
||||
name: string;
|
||||
58
e2e-tests/playwright/lib/src/server/client.ts
Normal file
58
e2e-tests/playwright/lib/src/server/client.ts
Normal file
|
|
@ -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.
|
||||
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import {
|
||||
AdminConfig,
|
||||
ClusterSettings,
|
||||
|
|
@ -15,7 +14,8 @@ import {
|
|||
ServiceSettings,
|
||||
TeamSettings,
|
||||
} from '@mattermost/types/config';
|
||||
import testConfig from '@e2e-test.config';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
export function getOnPremServerConfig(): AdminConfig {
|
||||
return merge<AdminConfig>(defaultServerConfig, onPremServerConfig() as AdminConfig);
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {Client, makeClient} from './client';
|
||||
export {makeClient} from './client';
|
||||
export {createRandomChannel} from './channel';
|
||||
export {getOnPremServerConfig} from './default_config';
|
||||
export {initSetup, getAdminClient} from './init';
|
||||
export {createRandomPost} from './post';
|
||||
export {createRandomTeam} from './team';
|
||||
export {createRandomUser, getDefaultAdminUser} from './user';
|
||||
|
|
@ -2,16 +2,16 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
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 {createRandomTeam} from './team';
|
||||
import {createRandomUser} from './user';
|
||||
|
||||
import {getFileFromCommonAsset} from '@/file';
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
export async function initSetup({
|
||||
userPrefix = 'user',
|
||||
teamPrefix = {name: 'team', displayName: 'Team'},
|
||||
|
|
@ -30,7 +30,7 @@ export async function initSetup({
|
|||
}
|
||||
|
||||
// Reset server config
|
||||
const adminConfig = await adminClient.updateConfig(getOnPremServerConfig());
|
||||
const adminConfig = await adminClient.updateConfig(getOnPremServerConfig() as any);
|
||||
|
||||
// Create new team
|
||||
const team = await adminClient.createTeam(createRandomTeam(teamPrefix.name, teamPrefix.displayName));
|
||||
|
|
@ -45,7 +45,7 @@ export async function initSetup({
|
|||
const {client: userClient} = await makeClient(user);
|
||||
|
||||
if (withDefaultProfileImage) {
|
||||
const {file} = getFileDataFromAsset('mattermost-icon_128x128.png', 'image/png');
|
||||
const file = getFileFromCommonAsset('mattermost-icon_128x128.png');
|
||||
await userClient.uploadProfileImage(user.id, file);
|
||||
}
|
||||
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Post, PostMetadata} from '@mattermost/types/posts';
|
||||
import {getRandomId} from '@e2e-support/util';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
|
||||
export function createRandomPost(post?: Partial<Post>): Post {
|
||||
if (post && post.channel_id && post.user_id) {
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
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 {
|
||||
const randomSuffix = getRandomId();
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
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') {
|
||||
const randomId = getRandomId();
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, Page} from '@playwright/test';
|
||||
|
||||
export {waitUntil} from 'async-wait-until';
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function moveMouseToCenter(page: Page) {
|
||||
await page.mouse.move(0, 0);
|
||||
}
|
||||
63
e2e-tests/playwright/lib/src/test_config.ts
Normal file
63
e2e-tests/playwright/lib/src/test_config.ts
Normal file
|
|
@ -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 AxeBuilder from '@axe-core/playwright';
|
||||
import {AxeBuilder} from '@axe-core/playwright';
|
||||
|
||||
import {TestBrowser} from './browser_context';
|
||||
import {
|
||||
ensureLicense,
|
||||
shouldHaveCallsEnabled,
|
||||
shouldHaveFeatureFlag,
|
||||
shouldRunInLinux,
|
||||
ensureLicense,
|
||||
skipIfNoLicense,
|
||||
skipIfFeatureFlagNotSet,
|
||||
skipIfNoLicense,
|
||||
} 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 pages from './ui/pages';
|
||||
import {pages} from './ui/pages';
|
||||
import {matchSnapshot} from './visual';
|
||||
import {stubNotification, waitForNotification} from './mock_browser_api';
|
||||
import {duration} from './util';
|
||||
import {duration, getRandomId, simpleEmailRe, wait} from './util';
|
||||
|
||||
export {expect} from '@playwright/test';
|
||||
|
||||
type ExtendedFixtures = {
|
||||
export type ExtendedFixtures = {
|
||||
axe: AxeBuilderExtended;
|
||||
pw: PlaywrightExtended;
|
||||
};
|
||||
|
|
@ -43,7 +54,7 @@ export const test = base.extend<ExtendedFixtures>({
|
|||
},
|
||||
});
|
||||
|
||||
class PlaywrightExtended {
|
||||
export class PlaywrightExtended {
|
||||
// ./browser_context
|
||||
readonly testBrowser;
|
||||
|
||||
|
|
@ -55,6 +66,10 @@ class PlaywrightExtended {
|
|||
readonly skipIfNoLicense;
|
||||
readonly skipIfFeatureFlagNotSet;
|
||||
|
||||
// ./file
|
||||
readonly getBlobFromAsset;
|
||||
readonly getFileFromAsset;
|
||||
|
||||
// ./server
|
||||
readonly getAdminClient;
|
||||
readonly initSetup;
|
||||
|
|
@ -64,19 +79,27 @@ class PlaywrightExtended {
|
|||
readonly waitForAnimationEnd;
|
||||
readonly waitUntil;
|
||||
|
||||
// ./mock_browser_api
|
||||
readonly stubNotification;
|
||||
readonly waitForNotification;
|
||||
|
||||
// ./visual
|
||||
readonly matchSnapshot;
|
||||
|
||||
// ./util
|
||||
readonly duration;
|
||||
readonly simpleEmailRe;
|
||||
readonly wait;
|
||||
|
||||
// random
|
||||
readonly random;
|
||||
|
||||
// unauthenticated page
|
||||
readonly loginPage;
|
||||
readonly landingLoginPage;
|
||||
readonly signupPage;
|
||||
readonly resetPasswordPage;
|
||||
|
||||
// ./visual
|
||||
readonly matchSnapshot;
|
||||
|
||||
// ./mock_browser_api
|
||||
readonly stubNotification;
|
||||
readonly waitForNotification;
|
||||
|
||||
readonly hasSeenLandingPage;
|
||||
|
||||
constructor(browser: Browser, page: Page, isMobile: boolean) {
|
||||
|
|
@ -91,6 +114,10 @@ class PlaywrightExtended {
|
|||
this.skipIfNoLicense = skipIfNoLicense;
|
||||
this.skipIfFeatureFlagNotSet = skipIfFeatureFlagNotSet;
|
||||
|
||||
// ./file
|
||||
this.getBlobFromAsset = getBlobFromAsset;
|
||||
this.getFileFromAsset = getFileFromAsset;
|
||||
|
||||
// ./server
|
||||
this.initSetup = initSetup;
|
||||
this.getAdminClient = getAdminClient;
|
||||
|
|
@ -106,13 +133,26 @@ class PlaywrightExtended {
|
|||
this.signupPage = new pages.SignupPage(page);
|
||||
this.resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
|
||||
// ./visual
|
||||
this.matchSnapshot = matchSnapshot;
|
||||
|
||||
// ./mock_browser_api
|
||||
this.stubNotification = stubNotification;
|
||||
this.waitForNotification = waitForNotification;
|
||||
|
||||
// ./visual
|
||||
this.matchSnapshot = matchSnapshot;
|
||||
|
||||
// ./util
|
||||
this.duration = duration;
|
||||
this.wait = wait;
|
||||
this.simpleEmailRe = simpleEmailRe;
|
||||
|
||||
this.random = {
|
||||
id: getRandomId,
|
||||
channel: createRandomChannel,
|
||||
post: createRandomPost,
|
||||
team: createRandomTeam,
|
||||
user: createRandomUser,
|
||||
};
|
||||
|
||||
this.hasSeenLandingPage = async () => {
|
||||
// Visit the base URL to be able to set the localStorage
|
||||
await page.goto('/');
|
||||
|
|
@ -121,7 +161,7 @@ class PlaywrightExtended {
|
|||
}
|
||||
}
|
||||
|
||||
class AxeBuilderExtended {
|
||||
export class AxeBuilderExtended {
|
||||
readonly builder: (page: Page, options?: AxeBuilderOptions) => AxeBuilder;
|
||||
|
||||
// See https://github.com/dequelabs/axe-core/blob/master/doc/API.md#axe-core-tags
|
||||
|
|
@ -9,29 +9,6 @@ export type TestArgs = {
|
|||
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
|
||||
export type ScreenshotOptions = {
|
||||
/**
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelsAppBar {
|
||||
readonly container: Locator;
|
||||
|
|
@ -18,5 +18,3 @@ export default class ChannelsAppBar {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsAppBar};
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {waitUntil} from '@e2e-support/test_action';
|
||||
import {duration} from '@e2e-support/util';
|
||||
import ChannelsHeader from './header';
|
||||
import ChannelsPostCreate from './post_create';
|
||||
import ChannelsPostEdit from './post_edit';
|
||||
import ChannelsPost from './post';
|
||||
|
||||
import {duration} from '@/util';
|
||||
import {waitUntil} from '@/test_action';
|
||||
|
||||
export default class ChannelsCenterView {
|
||||
readonly container: Locator;
|
||||
|
|
@ -25,12 +29,10 @@ export default class ChannelsCenterView {
|
|||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.scheduledDraftChannelInfoMessageLocator = 'span:has-text("Message scheduled for")';
|
||||
this.header = new components.ChannelsHeader(this.container.locator('.channel-header'));
|
||||
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('post-create'));
|
||||
this.scheduledDraftOptions = new components.ChannelsPostCreate(
|
||||
container.locator('#dropdown_send_post_options'),
|
||||
);
|
||||
this.postEdit = new components.ChannelsPostEdit(container.locator('.post-edit__container'));
|
||||
this.header = new ChannelsHeader(this.container.locator('.channel-header'));
|
||||
this.postCreate = new ChannelsPostCreate(container.getByTestId('post-create'));
|
||||
this.scheduledDraftOptions = new ChannelsPostCreate(container.locator('#dropdown_send_post_options'));
|
||||
this.postEdit = new ChannelsPostEdit(container.locator('.post-edit__container'));
|
||||
this.postBoxIndicator = container.locator('div.postBoxIndicator');
|
||||
this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator');
|
||||
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
|
||||
|
|
@ -58,7 +60,7 @@ export default class ChannelsCenterView {
|
|||
async getFirstPost() {
|
||||
const firstPost = this.container.getByTestId('postView').first();
|
||||
await firstPost.waitFor();
|
||||
return new components.ChannelsPost(firstPost);
|
||||
return new ChannelsPost(firstPost);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +69,7 @@ export default class ChannelsCenterView {
|
|||
async getLastPost() {
|
||||
const lastPost = this.container.getByTestId('postView').last();
|
||||
await lastPost.waitFor();
|
||||
return new components.ChannelsPost(lastPost);
|
||||
return new ChannelsPost(lastPost);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,7 +91,7 @@ export default class ChannelsCenterView {
|
|||
async getNthPost(index: number) {
|
||||
const nthPost = this.container.getByTestId('postView').nth(index);
|
||||
await nthPost.waitFor();
|
||||
return new components.ChannelsPost(nthPost);
|
||||
return new ChannelsPost(nthPost);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,7 +101,7 @@ export default class ChannelsCenterView {
|
|||
async getPostById(id: string) {
|
||||
const postById = this.container.locator(`[id="post_${id}"]`);
|
||||
await postById.waitFor();
|
||||
return new components.ChannelsPost(postById);
|
||||
return new ChannelsPost(postById);
|
||||
}
|
||||
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class DeletePostConfirmationDialog {
|
||||
readonly container: Locator;
|
||||
|
|
@ -36,5 +36,3 @@ export default class DeletePostConfirmationDialog {
|
|||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {DeletePostConfirmationDialog};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class DeletePostModal {
|
||||
readonly container: Locator;
|
||||
|
|
@ -24,5 +24,3 @@ export default class DeletePostModal {
|
|||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {DeletePostModal};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class EmojiGifPicker {
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class FindChannelsModal {
|
||||
readonly container: Locator;
|
||||
|
|
@ -19,5 +19,3 @@ export default class FindChannelsModal {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {FindChannelsModal};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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.
|
||||
|
|
@ -41,5 +41,3 @@ export default class GenericConfirmModal {
|
|||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {GenericConfirmModal};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelsHeader {
|
||||
readonly container: Locator;
|
||||
|
|
@ -14,5 +14,3 @@ export default class ChannelsHeader {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsHeader};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class MessagePriority {
|
||||
readonly container: Locator;
|
||||
|
|
@ -74,5 +74,3 @@ export default class MessagePriority {
|
|||
await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {MessagePriority};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {
|
||||
readonly container: Locator;
|
||||
|
|
@ -25,8 +26,8 @@ export default class ChannelsPost {
|
|||
|
||||
this.removePostButton = container.locator('.post__remove');
|
||||
|
||||
this.postMenu = new components.PostMenu(container.locator('.post-menu'));
|
||||
this.threadFooter = new components.ThreadFooter(container.locator('.ThreadFooter'));
|
||||
this.postMenu = new PostMenu(container.locator('.post-menu'));
|
||||
this.threadFooter = new ThreadFooter(container.locator('.ThreadFooter'));
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
|
@ -72,5 +73,3 @@ export default class ChannelsPost {
|
|||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsPost};
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
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 {
|
||||
readonly container: Locator;
|
||||
|
|
@ -15,6 +19,7 @@ export default class ChannelsPostCreate {
|
|||
readonly scheduleDraftMessageButton;
|
||||
readonly priorityButton;
|
||||
readonly suggestionList;
|
||||
readonly filePreview;
|
||||
|
||||
constructor(container: Locator, isRHS = false) {
|
||||
this.container = container;
|
||||
|
|
@ -31,6 +36,7 @@ export default class ChannelsPostCreate {
|
|||
this.scheduleDraftMessageButton = container.getByLabel('Schedule message');
|
||||
this.priorityButton = container.getByLabel('Message priority');
|
||||
this.suggestionList = container.getByTestId('suggestionList');
|
||||
this.filePreview = container.locator('.file-preview__container');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
|
@ -101,18 +107,16 @@ export default class ChannelsPostCreate {
|
|||
await this.writeMessage(message);
|
||||
|
||||
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) => {
|
||||
await fileChooser.setFiles(filePaths);
|
||||
});
|
||||
|
||||
// Click on the attachment button
|
||||
await this.attachmentButton.click();
|
||||
|
||||
// wait for all files to be uploaded
|
||||
await waitUntil(async () => {
|
||||
const attachment = await this.container.locator('.file-preview').count();
|
||||
return attachment === files.length;
|
||||
});
|
||||
// Wait until the file preview is displayed
|
||||
await this.waitUntilFilePreviewContains(files);
|
||||
}
|
||||
|
||||
await this.sendMessage();
|
||||
|
|
@ -122,6 +126,18 @@ export default class ChannelsPostCreate {
|
|||
await expect(this.emojiButton).toBeVisible();
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostDotMenu {
|
||||
readonly container: Locator;
|
||||
|
|
@ -46,5 +46,3 @@ export default class PostDotMenu {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {PostDotMenu};
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
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 {
|
||||
readonly container: Locator;
|
||||
|
|
@ -23,10 +28,8 @@ export default class ChannelsPostEdit {
|
|||
this.attachmentButton = container.locator('#fileUploadButton');
|
||||
this.emojiButton = container.getByLabel('select an emoji');
|
||||
this.sendMessageButton = container.locator('.save');
|
||||
this.deleteConfirmationDialog = new components.DeletePostConfirmationDialog(
|
||||
container.page().locator('#deletePostModal'),
|
||||
);
|
||||
this.restorePostConfirmationDialog = new components.RestorePostConfirmationDialog(
|
||||
this.deleteConfirmationDialog = new DeletePostConfirmationDialog(container.page().locator('#deletePostModal'));
|
||||
this.restorePostConfirmationDialog = new RestorePostConfirmationDialog(
|
||||
container.page().locator('#restorePostModal'),
|
||||
);
|
||||
}
|
||||
|
|
@ -51,7 +54,7 @@ export default class ChannelsPostEdit {
|
|||
}
|
||||
|
||||
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) => {
|
||||
await fileChooser.setFiles(filePaths);
|
||||
});
|
||||
|
|
@ -90,5 +93,3 @@ export default class ChannelsPostEdit {
|
|||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsPostEdit};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostMenu {
|
||||
readonly container: Locator;
|
||||
|
|
@ -55,5 +55,3 @@ export default class PostMenu {
|
|||
await this.dotMenuButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {PostMenu};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class PostReminderMenu {
|
||||
readonly container: Locator;
|
||||
|
|
@ -28,5 +28,3 @@ export default class PostReminderMenu {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {PostReminderMenu};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class RestorePostConfirmationDialog {
|
||||
readonly container: Locator;
|
||||
|
|
@ -30,5 +30,3 @@ export default class RestorePostConfirmationDialog {
|
|||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {RestorePostConfirmationDialog};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftMenu {
|
||||
readonly container: Locator;
|
||||
|
|
@ -22,5 +22,3 @@ export default class ScheduledDraftMenu {
|
|||
await this.scheduleDraftMessageCustomTimeOption.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {ScheduledDraftMenu};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftModal {
|
||||
readonly container: Locator;
|
||||
|
|
@ -93,5 +93,3 @@ export default class ScheduledDraftModal {
|
|||
return pacificTime;
|
||||
}
|
||||
}
|
||||
|
||||
export {ScheduledDraftModal};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SearchPopover {
|
||||
readonly container: Locator;
|
||||
|
|
@ -43,5 +43,3 @@ export default class SearchPopover {
|
|||
return this.searchHints.locator('.suggestion--selected');
|
||||
}
|
||||
}
|
||||
|
||||
export {SearchPopover};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification';
|
||||
|
||||
|
|
@ -52,5 +52,3 @@ export default class NotificationsSettings {
|
|||
await this.container.getByText('Save').click();
|
||||
}
|
||||
}
|
||||
|
||||
export {NotificationsSettings};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {
|
||||
readonly container: Locator;
|
||||
|
|
@ -35,5 +35,3 @@ export default class SettingsModal {
|
|||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {SettingsModal};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ChannelsSidebarLeft {
|
||||
readonly container: Locator;
|
||||
|
|
@ -56,5 +56,3 @@ export default class ChannelsSidebarLeft {
|
|||
await expect(channel).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsSidebarLeft};
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {
|
||||
readonly container: Locator;
|
||||
|
|
@ -28,11 +30,11 @@ export default class ChannelsSidebarRight {
|
|||
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all")');
|
||||
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
|
||||
this.rhsPostBody = container.locator('.post-message__text');
|
||||
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('comment-create'), true);
|
||||
this.postCreate = new ChannelsPostCreate(container.getByTestId('comment-create'), true);
|
||||
this.closeButton = container.locator('.sidebar--right__close');
|
||||
|
||||
this.editTextbox = container.locator('#edit_textbox');
|
||||
this.postEdit = new 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.restorePreviousPostVersionIcon = container.locator(
|
||||
'button[aria-label="Select to restore an old message."]',
|
||||
|
|
@ -50,7 +52,7 @@ export default class ChannelsSidebarRight {
|
|||
async getPostById(postId: string) {
|
||||
const post = this.container.locator(`[id="rhsPost_${postId}"]`);
|
||||
await post.waitFor();
|
||||
return new components.ChannelsPost(post);
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,13 +61,13 @@ export default class ChannelsSidebarRight {
|
|||
async getLastPost() {
|
||||
const post = this.container.getByTestId('rhsPostView').last();
|
||||
await post.waitFor();
|
||||
return new components.ChannelsPost(post);
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
async getFirstPost() {
|
||||
const post = this.container.getByTestId('rhsPostView').first();
|
||||
await post.waitFor();
|
||||
return new components.ChannelsPost(post);
|
||||
return new ChannelsPost(post);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -96,5 +98,3 @@ export default class ChannelsSidebarRight {
|
|||
await this.restorePreviousPostVersionIcon.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsSidebarRight};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class ThreadFooter {
|
||||
readonly container: Locator;
|
||||
|
|
@ -26,5 +26,3 @@ export default class ThreadFooter {
|
|||
await this.replyButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {ThreadFooter};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class UserProfilePopover {
|
||||
readonly container: Locator;
|
||||
|
|
@ -18,5 +18,3 @@ export default class UserProfilePopover {
|
|||
await this.container.getByLabel('Close user profile popover').click();
|
||||
}
|
||||
}
|
||||
|
||||
export {UserProfilePopover};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class Footer {
|
||||
readonly container: Locator;
|
||||
|
|
@ -26,5 +26,3 @@ export default class Footer {
|
|||
await expect(this.copyright).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {Footer};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class GlobalHeader {
|
||||
readonly container: Locator;
|
||||
|
|
@ -49,5 +49,3 @@ export default class GlobalHeader {
|
|||
await this.searchBox.getByTestId('searchBoxClose').click();
|
||||
}
|
||||
}
|
||||
|
||||
export {GlobalHeader};
|
||||
96
e2e-tests/playwright/lib/src/ui/components/index.ts
Normal file
96
e2e-tests/playwright/lib/src/ui/components/index.ts
Normal file
|
|
@ -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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class MainHeader {
|
||||
readonly container: Locator;
|
||||
|
|
@ -20,5 +20,3 @@ export default class MainHeader {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {MainHeader};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemConsoleNavbar {
|
||||
readonly container: Locator;
|
||||
|
|
@ -14,5 +14,3 @@ export default class SystemConsoleNavbar {
|
|||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemConsoleNavbar};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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;
|
||||
|
||||
constructor(container: Locator) {
|
||||
|
|
@ -48,5 +48,3 @@ class SystemUsersColumnToggleMenu {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export {FeatureDiscovery};
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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.
|
||||
*/
|
||||
class SystemUsersFilterMenu {
|
||||
export default class SystemUsersFilterMenu {
|
||||
readonly container: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
|
|
@ -42,5 +42,3 @@ class SystemUsersFilterMenu {
|
|||
await this.container.press('Escape');
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemUsersFilterMenu};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 teamMenuInput: Locator;
|
||||
|
|
@ -66,5 +66,3 @@ class SystemUsersFilterPopover {
|
|||
await expect(this.container).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemUsersFilterPopover};
|
||||
|
|
@ -77,5 +77,3 @@ export default class MobileSecurity {
|
|||
await this.saveButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {MobileSecurity};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> User Management -> Users
|
||||
|
|
@ -130,5 +130,3 @@ export default class SystemUsers {
|
|||
await expect(foundUser).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemUsers};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class SystemConsoleSidebar {
|
||||
readonly container: Locator;
|
||||
|
|
@ -37,5 +37,3 @@ export default class SystemConsoleSidebar {
|
|||
await this.searchInput.fill(itemName);
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemConsoleSidebar};
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {Page} from '@playwright/test';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class ChannelsPage {
|
||||
readonly channels = 'Channels';
|
||||
|
|
@ -19,6 +19,7 @@ export default class ChannelsPage {
|
|||
readonly sidebarRight;
|
||||
readonly appBar;
|
||||
readonly userProfilePopover;
|
||||
readonly messagePriority;
|
||||
|
||||
readonly findChannelsModal;
|
||||
readonly deletePostModal;
|
||||
|
|
@ -40,6 +41,7 @@ export default class ChannelsPage {
|
|||
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
|
||||
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
|
||||
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
|
||||
this.messagePriority = new components.MessagePriority(page.locator('body'));
|
||||
|
||||
// Modals
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
|
|
@ -88,5 +90,3 @@ export default class ChannelsPage {
|
|||
await this.centerView.postCreate.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
export {ChannelsPage};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Page} from '@playwright/test';
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
export default class DraftPage {
|
||||
readonly page: Page;
|
||||
|
|
@ -124,5 +124,3 @@ export default class DraftPage {
|
|||
await this.confirmbutton.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {DraftPage};
|
||||
34
e2e-tests/playwright/lib/src/ui/pages/index.ts
Normal file
34
e2e-tests/playwright/lib/src/ui/pages/index.ts
Normal file
|
|
@ -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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Page} from '@playwright/test';
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
export default class LandingLoginPage {
|
||||
readonly page: Page;
|
||||
|
|
@ -37,5 +37,3 @@ export default class LandingLoginPage {
|
|||
await this.page.goto('/landing#/login');
|
||||
}
|
||||
}
|
||||
|
||||
export {LandingLoginPage};
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {components} from '@e2e-support/ui/components';
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class LoginPage {
|
||||
readonly page: Page;
|
||||
|
|
@ -67,5 +66,3 @@ export default class LoginPage {
|
|||
await Promise.all([this.page.waitForNavigation(), this.signInButton.click()]);
|
||||
}
|
||||
}
|
||||
|
||||
export {LoginPage};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {
|
||||
readonly page: Page;
|
||||
|
|
@ -47,5 +47,3 @@ export default class ResetPasswordPage {
|
|||
await Promise.all([this.page.waitForNavigation(), this.resetButton.click()]);
|
||||
}
|
||||
}
|
||||
|
||||
export {ResetPasswordPage};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Page} from '@playwright/test';
|
||||
import {Page, expect} from '@playwright/test';
|
||||
|
||||
export default class ScheduledDraftPage {
|
||||
readonly page: Page;
|
||||
|
|
@ -157,5 +157,3 @@ export default class ScheduledDraftPage {
|
|||
await this.copyIcon.click();
|
||||
}
|
||||
}
|
||||
|
||||
export {ScheduledDraftPage};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {components} from '@e2e-support/ui/components';
|
||||
import {duration, wait} from '@/util';
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class SignupPage {
|
||||
readonly page: Page;
|
||||
|
|
@ -86,5 +86,3 @@ export default class SignupPage {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {SignupPage};
|
||||
|
|
@ -2,9 +2,10 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Page} from '@playwright/test';
|
||||
import {components} from '../components';
|
||||
|
||||
class SystemConsolePage {
|
||||
import {components} from '@/ui/components';
|
||||
|
||||
export default class SystemConsolePage {
|
||||
readonly page: Page;
|
||||
|
||||
readonly sidebar;
|
||||
|
|
@ -39,10 +40,10 @@ class SystemConsolePage {
|
|||
|
||||
// Sections and sub-sections
|
||||
this.systemUsers = new components.SystemUsers(page.getByTestId('systemUsersSection'));
|
||||
this.mobileSecurity = new components.MobileSecurity(
|
||||
this.mobileSecurity = new components.SystemConsoleMobileSecurity(
|
||||
page.getByTestId('sysconsole_section_MobileSecuritySettings'),
|
||||
);
|
||||
this.featureDiscovery = new components.FeatureDiscovery(page.getByTestId('featureDiscovery'));
|
||||
this.featureDiscovery = new components.SystemConsoleFeatureDiscovery(page.getByTestId('featureDiscovery'));
|
||||
|
||||
// Menus & Popovers
|
||||
this.systemUsersFilterPopover = new components.SystemUsersFilterPopover(
|
||||
|
|
@ -84,5 +85,3 @@ class SystemConsolePage {
|
|||
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click();
|
||||
}
|
||||
}
|
||||
|
||||
export {SystemConsolePage};
|
||||
|
|
@ -4,14 +4,14 @@
|
|||
import os from 'node:os';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {expect, TestInfo} from '@playwright/test';
|
||||
|
||||
import {duration, illegalRe, wait} from '@e2e-support/util';
|
||||
import testConfig from '@e2e-test.config';
|
||||
import {ScreenshotOptions, TestArgs} from '@e2e-types';
|
||||
import {TestInfo, expect} from '@playwright/test';
|
||||
|
||||
import snapshotWithPercy from './percy';
|
||||
|
||||
import {duration, illegalRe, wait} from '@/util';
|
||||
import {testConfig} from '@/test_config';
|
||||
import {ScreenshotOptions, TestArgs} from '@/types';
|
||||
|
||||
export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs, options: ScreenshotOptions = {}) {
|
||||
if (os.platform() !== 'linux') {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import testConfig from '@e2e-test.config';
|
||||
import {TestArgs} from '@e2e-types';
|
||||
import {testConfig} from '@/test_config';
|
||||
import {TestArgs} from '@/types';
|
||||
|
||||
export default async function snapshotWithPercy(name: string, testArgs: TestArgs) {
|
||||
if (testArgs.browserName === 'chromium' && testConfig.percyEnabled && testArgs.viewport) {
|
||||
20
e2e-tests/playwright/lib/tsconfig.json
Normal file
20
e2e-tests/playwright/lib/tsconfig.json
Normal file
|
|
@ -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
generated
10101
e2e-tests/playwright/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,42 +1,34 @@
|
|||
{
|
||||
"scripts": {
|
||||
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
||||
"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",
|
||||
"lint": "eslint .",
|
||||
"prettier": "prettier . --check",
|
||||
"prettier:fix": "prettier --write .",
|
||||
"check": "npm run tsc && npm run lint && npm run prettier",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axe-core/playwright": "4.10.1",
|
||||
"@percy/cli": "1.30.6",
|
||||
"@percy/playwright": "1.0.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"async-wait-until": "2.0.18",
|
||||
"axe-core": "4.10.2",
|
||||
"chalk": "4.1.2",
|
||||
"dayjs": "1.11.13",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "16.4.7",
|
||||
"form-data-encoder": "4.0.2",
|
||||
"formdata-node": "6.0.3",
|
||||
"uuid": "11.0.5",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
"name": "mattermost-playwright",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"lib",
|
||||
"test"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "npm run build",
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"build:watch": "npm run build:watch --workspaces --if-present",
|
||||
"clean": "rm -rf node_modules logs results test-results && npm run clean --workspaces --if-present",
|
||||
"tsc": "npm run tsc --workspaces --if-present",
|
||||
"lint": "eslint .",
|
||||
"prettier": "prettier . --check",
|
||||
"prettier:fix": "prettier --write .",
|
||||
"check": "npm run lint && npm run prettier && npm run tsc",
|
||||
"test": "npm run test --workspace=test",
|
||||
"test:ci": "npm run test:ci --workspace=test"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "1.11.13",
|
||||
"@playwright/test": "1.51.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.28.0",
|
||||
"eslint": "9.23.0",
|
||||
"eslint-import-resolver-typescript": "4.2.7",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"prettier": "3.5.3",
|
||||
"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 dayjs = require('dayjs');
|
||||
const duration = require('dayjs/plugin/duration');
|
||||
dayjs.extend(duration);
|
||||
|
|
@ -90,5 +94,5 @@ function generateWebhookBody() {
|
|||
};
|
||||
}
|
||||
|
||||
let webhookBody = generateWebhookBody();
|
||||
const webhookBody = generateWebhookBody();
|
||||
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
|
||||
|
||||
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
|
||||
|
|
@ -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_HEADLESS=true
|
||||
cd mattermost/e2e-tests/playwright
|
||||
cd mattermost
|
||||
|
||||
# Install npm packages. Use "npm ci" to match the automated environment
|
||||
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci
|
||||
|
||||
# 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
|
||||
npm run test
|
||||
|
||||
# Update snapshots
|
||||
npm run test -- login --update-snapshots
|
||||
# Run visual tests
|
||||
npm run test -- -- visual
|
||||
|
||||
# Update snapshots of visual tests
|
||||
npm run test -- -- visual --update-snapshots
|
||||
```
|
||||
|
||||
## Page/Component Object Model
|
||||
|
||||
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.
|
||||
BIN
e2e-tests/playwright/test/asset/mattermost-icon_128x128.png
Normal file
BIN
e2e-tests/playwright/test/asset/mattermost-icon_128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
22
e2e-tests/playwright/test/global_setup.ts
Normal file
22
e2e-tests/playwright/test/global_setup.ts
Normal file
|
|
@ -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
Normal file
26
e2e-tests/playwright/test/package.json
Normal file
|
|
@ -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.
|
||||
|
||||
import {defineConfig, devices} from '@playwright/test';
|
||||
|
||||
import {duration} from '@e2e-support/util';
|
||||
import testConfig from '@e2e-test.config';
|
||||
import {duration, testConfig} from '@mattermost/playwright-lib';
|
||||
|
||||
export default defineConfig({
|
||||
globalSetup: require.resolve('./global_setup'),
|
||||
globalSetup: './global_setup.ts',
|
||||
forbidOnly: testConfig.isCI,
|
||||
outputDir: './results/output',
|
||||
retries: testConfig.isCI ? 2 : 0,
|
||||
testDir: 'tests',
|
||||
testDir: 'specs',
|
||||
timeout: duration.one_min,
|
||||
workers: testConfig.workers,
|
||||
expect: {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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}) => {
|
||||
// # Create and sign in a new user
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue