MM-64282 E2E/Playwright: Test documentation format (#31050)

* initial implementation of test documentation in spec file with AI-assisted prompt from Claude and linter script

* update snapshots

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
sabril
2025-05-20 01:07:47 +08:00
коммит произвёл GitHub
родитель a358401772
Коммит 2116a6d94a
18 изменённых файлов: 1721 добавлений и 487 удалений

2
.gitignore поставляемый
Просмотреть файл

@@ -162,5 +162,5 @@ docker-compose.override.yaml
**/CLAUDE.local.md **/CLAUDE.local.md
CLAUDE.md ./CLAUDE.md

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

@@ -0,0 +1,242 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Purpose
This is the Playwright E2E testing suite for Mattermost. It contains end-to-end tests for validating the Mattermost web application functionality using the Playwright testing framework.
## Key Commands
### Installation
```bash
# Install npm packages
npm i
# Install browser binaries (if prompted)
npx playwright install
```
### Running Tests
```bash
# Run a specific test across all browsers (Chrome, Firefox, iPad)
npm run test -- <test-name>
# Run a specific test for a specific browser
npm run test -- <test-name> --project=chrome
npm run test -- <test-name> --project=firefox
npm run test -- <test-name> --project=ipad
# Run all tests
npm run test
# Run tests with UI mode
npm run playwright-ui
# Run tests with slow-motion to debug
npm run test:slomo
# Run visual tests
npm run test -- visual
# Update visual test snapshots
npm run test:update-snapshots
# Visual testing with Percy
npm run percy
```
### Development Commands
```bash
# Build the project
npm run build
# Watch mode for development
npm run build:watch
# Type checking
npm run tsc
# Linting
npm run lint
# Format code
npm run prettier:fix
# Verify test documentation format
npm run lint:test-docs
# Run all checks (lint, prettier, typescript, test docs)
npm run check
# Clean the project
npm run clean
# Show test report
npm run show-report
```
## Architecture Overview
### Key Components
1. **`lib/` Directory**: Contains the shared library (`@mattermost/playwright-lib`) that provides:
- Page objects for Mattermost UI pages
- Component abstractions for UI elements
- Test utilities and fixtures
- Server setup and management functions
- Visual testing support
2. **`specs/` Directory**: Contains the actual test files organized by type:
- `functional/` - Functional tests for various features
- `visual/` - Visual regression tests
- `accessibility/` - Accessibility tests
- `client/` - Client API tests
3. **Test Fixtures**: The main test fixture (`pw`) provides:
- Browser context management
- Page actions and utilities
- Server API helpers
- Random data generators
- Visual testing helpers
4. **Page Object Model**: UI abstractions are organized in:
- `lib/src/ui/pages/` - Page objects (Login, Channels, etc.)
- `lib/src/ui/components/` - Component objects (Posts, Menus, etc.)
### Test Flow
1. Tests typically follow this pattern:
- Initialize test setup with `pw.initSetup()`
- Login to a test account with `pw.testBrowser.login()`
- Navigate to the relevant page
- Perform actions and assertions
- Optionally take visual snapshots
2. Visual tests also:
- Hide dynamic content with `pw.hideDynamicChannelsContent()`
- Take snapshots with `pw.matchSnapshot()`
## Environment Configuration
Tests can be configured through environment variables:
- `PW_BASE_URL` - Mattermost server URL (default: http://localhost:8065)
- `PW_ADMIN_USERNAME` - Admin username (default: sysadmin)
- `PW_ADMIN_PASSWORD` - Admin password (default: Sys@dmin-sample1)
- `PW_HEADLESS` - Run tests headless (default: true)
- `PW_SNAPSHOT_ENABLE` - Enable snapshot testing (default: false)
- `PW_SLOWMO` - Add delay between actions in ms (default: 0)
- `PW_WORKERS` - Number of parallel workers (default: 1)
## Server Setup
Before running tests, a Mattermost server must be available. Two options:
1. **Run from source**:
```bash
cd server && make run
```
2. **Run using Docker** (recommended for testing):
```bash
# Configure environment in e2e-tests/.ci/env
cd e2e-tests && TEST=playwright make
```
## Best Practices
1. **Page Object Pattern**: Always use page/component objects from the library. No static UI selectors should be in test files.
2. **Visual Testing**: For visual tests:
- Run via Docker container for consistency
- Use `pw.hideDynamicChannelsContent()` to hide dynamic elements
- Update snapshots with `npm run test:update-snapshots`
3. **Test Title Validation with Claude Code**: When using Claude:
- Run `claude spec/path/to/file.spec.ts` to check your test file
- Ask: "Check if test titles follow the format in CLAUDE.md"
- Claude will analyze each test title and suggest improvements
- Format should be action-oriented, feature-specific, context-aware, and outcome-focused
- Example: `creates scheduled message from channel and posts at scheduled time`
4. **Test Structure**:
- Use descriptive test titles that follow this format:
- **Action-oriented**: Start with a verb that describes the main action
- **Feature-specific**: Include the feature or component being tested
- **Context-aware**: Include relevant context (where/how it's being performed)
- **Outcome-focused**: Specify the expected outcome or behavior
- Examples of well-formatted test titles:
- `"creates scheduled message from channel and posts at scheduled time"`
- `"edits scheduled message content while preserving send date"`
- `"reschedules message to a future date from scheduled posts page"`
- `"deletes scheduled message from scheduled posts page"`
- `"converts draft message to scheduled message"`
- Test keys (`MM-T\d+`) in test titles are optional for new tests
- New tests without keys will automatically be registered in the test management system after merge
- Test keys will be assigned later through a separate automated process
- Follow the `# Action` and `* Verification` comment pattern
- Group related tests in the same spec file
- Keep tests independent and isolated
- Use tags to categorize tests with `{tag: '@feature_name'}`
5. **Test Documentation Format**:
- Include JSDoc-style documentation before each test:
```typescript
/**
* @objective Clear description of what the test verifies
*
* @precondition
* Special setup or conditions required for the test
* Note: Only include preconditions that are not part of the default setup.
* Standard conditions like "a test server is running" should be omitted.
*/
test('MM-T1234 descriptive test title', {tag: '@feature_tag'}, async ({pw}) => {
// Test implementation
});
```
- If no special preconditions are needed, omit the `@precondition` tag entirely:
```typescript
/**
* @objective Clear description of what the test verifies
*/
test('descriptive test title', {tag: '@feature_tag'}, async ({pw}) => {
// Test implementation
});
```
- For new tests, the MM-T ID is optional and will be assigned later:
```typescript
/**
* @objective Clear description of what the test verifies
*/
test('descriptive test title', {tag: '@feature_tag'}, async ({pw}) => {
// Test implementation
});
```
- Use comment prefixes to clearly indicate actions and verifications:
- `// # descriptive action` - Comments that describe steps being taken (e.g., `// # Initialize user and login`)
- `// * descriptive verification` - Comments that describe assertions/checks (e.g., `// * Verify message appears in channel`)
6. **Browser Compatibility**:
- Tests run on Chrome, Firefox, and iPad by default
- Consider browser-specific behaviors for certain features
- Use `test.skip()` for browser-specific limitations
7. **Test Documentation Linting**:
- Run `npm run lint:test-docs` to verify all spec files follow the documentation format
- The linter checks for proper JSDoc tags, test titles, feature tags, and action/verification comments
- This is also included in the standard `npm run check` command
- See the example in `specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts`

483
e2e-tests/playwright/package-lock.json сгенерированный
Просмотреть файл

@@ -26,6 +26,7 @@
"eslint-import-resolver-typescript": "4.3.4", "eslint-import-resolver-typescript": "4.3.4",
"eslint-plugin-header": "3.1.1", "eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"glob": "11.0.2",
"prettier": "3.5.3", "prettier": "3.5.3",
"typescript": "5.8.3" "typescript": "5.8.3"
} }
@@ -402,6 +403,24 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@mattermost/client": { "node_modules/@mattermost/client": {
"resolved": "../../webapp/platform/client", "resolved": "../../webapp/platform/client",
"link": true "link": true
@@ -1772,6 +1791,19 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2508,6 +2540,13 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2515,6 +2554,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -3418,6 +3464,23 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3614,21 +3677,24 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "11.0.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
"deprecated": "Glob versions prior to v9 are no longer supported", "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^4.0.1",
"inherits": "2", "minimatch": "^10.0.0",
"minimatch": "^3.1.1", "minipass": "^7.1.2",
"once": "^1.3.0", "package-json-from-dist": "^1.0.0",
"path-is-absolute": "^1.0.0" "path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
}, },
"engines": { "engines": {
"node": "*" "node": "20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -3647,26 +3713,20 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/glob/node_modules/minimatch": { "node_modules/glob/node_modules/minimatch": {
"version": "3.1.2", "version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^2.0.1"
}, },
"engines": { "engines": {
"node": "*" "node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/globals": { "node_modules/globals": {
@@ -3719,6 +3779,52 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/globby/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/globby/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globby/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -4178,6 +4284,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-generator-function": { "node_modules/is-generator-function": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
@@ -4423,6 +4539,22 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/jackspeak": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4550,6 +4682,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"dev": true,
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.6.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
@@ -4661,6 +4803,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4938,6 +5090,13 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": { "node_modules/pako": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
@@ -5019,6 +5178,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
@@ -5362,6 +5538,49 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.40.2", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
@@ -5746,6 +5965,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5827,6 +6059,70 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string.prototype.trim": { "node_modules/string.prototype.trim": {
"version": "1.2.10", "version": "1.2.10",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
@@ -5886,6 +6182,46 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": { "node_modules/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -6406,6 +6742,101 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

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

@@ -9,9 +9,10 @@
"build:watch": "npm run build:watch --workspaces", "build:watch": "npm run build:watch --workspaces",
"tsc": "tsc -b && npm run tsc --workspaces", "tsc": "tsc -b && npm run tsc --workspaces",
"lint": "eslint .", "lint": "eslint .",
"lint:test-docs": "node script/lint-test-docs.js",
"prettier": "prettier . --check", "prettier": "prettier . --check",
"prettier:fix": "prettier --write .", "prettier:fix": "prettier --write .",
"check": "npm run lint && npm run prettier && npm run tsc", "check": "npm run lint && npm run prettier && npm run tsc && npm run lint:test-docs",
"test": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test", "test": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test",
"test:ci": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome", "test:ci": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --project=chrome",
"test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots", "test:update-snapshots": "npm run build && cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots",
@@ -39,6 +40,7 @@
"eslint-import-resolver-typescript": "4.3.4", "eslint-import-resolver-typescript": "4.3.4",
"eslint-plugin-header": "3.1.1", "eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"glob": "11.0.2",
"prettier": "3.5.3", "prettier": "3.5.3",
"typescript": "5.8.3" "typescript": "5.8.3"
} }

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

@@ -0,0 +1,62 @@
# Playwright E2E Test Scripts
This directory contains utility scripts for the Playwright E2E test suite.
## Test Documentation Format Linter
The `lint-test-docs.js` script verifies that all spec files follow the required documentation format:
- JSDoc with `@objective` and `@precondition` tags
- Proper test title with MM-T ID (e.g., MM-T1234)
- Tag for feature categorization (e.g., `{tag: '@feature_name'}`)
- Action comments (e.g., `// # Action`)
- Verification comments (e.g., `// * Verification`)
### Usage
```bash
# Run the linter directly
node script/lint-test-docs.js
# Or use the npm script
npm run lint:test-docs
```
### Integration with CI
The linter is also integrated with the main `check` command, which is typically run before committing changes:
```bash
npm run check
```
This will run ESLint, Prettier, TypeScript type checking, and the test documentation format linter.
### Requirements
All spec files should follow this format:
```typescript
/**
* @objective Clear description of what the test verifies
*
* @precondition
* Special setup or conditions required for the test
* Note: Only include for non-default requirements
*/
test('descriptive test title', {tag: '@feature_tag'}, async ({pw}) => {
// # Initialize setup and login
const {user} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
// # Navigate to channel and post a message
await channelsPage.goto();
await channelsPage.postMessage('Test message');
// * Verify message appears in the channel
const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toContainText('Test message');
});
```
This ensures consistency across all test files and makes it easier to understand the purpose and requirements of each test.

386
e2e-tests/playwright/script/lint-test-docs.js Исполняемый файл
Просмотреть файл

@@ -0,0 +1,386 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* Test Documentation Format Linter
*
* This script verifies that all spec files follow the required documentation format:
* - JSDoc with @objective and @precondition
* - Proper test title with MM-T ID
* - Tag for feature categorization
* - Action/Verification comments
*/
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable import/order */
const fs = require('fs');
const path = require('path');
const glob = require('glob');
/* eslint-disable no-console */
// Colors for terminal output
const colors = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
reset: '\x1b[0m',
cyan: '\x1b[36m',
magenta: '\x1b[35m',
white: '\x1b[37m',
};
// Regex patterns
const patterns = {
// Support both single-line and multi-line test declarations
testDeclaration:
/test\(\s*['"]([^'"]+)['"]\s*,(?:\s*|\n\s*){.*?tag:\s*['"]@.*?['"].*?}(?:\s*|\n\s*),(?:\s*|\n\s*)async/g,
jsdocBlock: /\/\*\*\s*\n(?:.*\n)*?\s*\*\//g,
objective: /@objective\s+.*?(?=\n|\*\/)/g,
precondition: /@precondition\s+.*?(?=\n|\*\/)/g,
tagDeclaration: /{tag:\s*['"]@[\w_]+['"]}|{\s*tag:\s*['"]@[\w_]+['"]\s*}/g,
// Match action comments - note the difference between # as a comment marker vs # in strings
actionComment: /(?:^|\n)\s*\/\/\s*#\s*.+/g,
// Match verification comments - note the difference between * as a comment marker vs * in strings
verificationComment: /(?:^|\n)\s*\/\/\s*\*\s*.+/g,
};
// Get all spec files
const specFiles = glob.sync(path.join(process.cwd(), 'specs/**/*.spec.ts'));
// Results
const results = {
passed: 0,
failed: 0,
warnings: 0,
errors: [],
};
// Process each file
specFiles.forEach((filePath) => {
const relativeFilePath = path.relative(process.cwd(), filePath);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const fileErrors = [];
// Extract all test declarations
const testDeclarations = Array.from(fileContent.matchAll(patterns.testDeclaration));
const jsdocBlocks = Array.from(fileContent.matchAll(patterns.jsdocBlock));
// If no test declarations found, warn but don't fail
if (testDeclarations.length === 0) {
results.warnings++;
console.log(
`${colors.yellow}WARNING${colors.reset}: No test declarations found in ${colors.cyan}${relativeFilePath}${colors.reset}`,
);
return;
}
// Check each test for proper format
testDeclarations.forEach((testMatch) => {
const testDeclaration = testMatch[0];
const testName = testMatch[1];
// Get test function position to find corresponding JSDoc
const testPosition = testMatch.index;
// Get the JSDoc block that immediately precedes this test
const precedingJSDoc = jsdocBlocks.find((jsdoc) => {
const jsdocEnd = jsdoc.index + jsdoc[0].length;
// JSDoc should end right before the test with only whitespace in between
const textBetween = fileContent.substring(jsdocEnd, testPosition).trim();
return textBetween === '';
});
// Check if test has a test key (MM-T####)
if (!testName.match(/^MM-T\d+/)) {
// This is a new test without a test key
console.log(
`${colors.cyan}NEW TEST FOUND: "${testName}"${colors.reset} - Will be registered in the test management system after merge`,
);
results.newTests = results.newTests || [];
results.newTests.push({file: relativeFilePath, testName});
} else {
// This is an existing test with a test key
const baseTestKey = testName.match(/^(MM-T\d+)/)[1];
// Check if this is a step of a multi-step test case
const stepMatch = testName.match(/^MM-T\d+(_\d+)/);
const isStep = stepMatch !== null;
const stepSuffix = isStep ? stepMatch[1] : '';
const testKey = baseTestKey + (stepSuffix || '');
// Log different message for test steps
if (isStep) {
// Extract the step number without the underscore
const stepNumber = stepSuffix.substring(1);
console.log(
`${colors.magenta}DOCUMENTATION UPDATE: "${testKey}"${colors.reset} - step ${stepNumber} of ${baseTestKey} - Changes will be mapped to test management system after merge`,
);
} else {
console.log(
`${colors.magenta}DOCUMENTATION UPDATE: "${testKey}"${colors.reset} - Changes will be saved to test management system after merge`,
);
}
// Check if documentation is present and mark for update
if (precedingJSDoc) {
results.updatedTests = results.updatedTests || [];
results.updatedTests.push({
file: relativeFilePath,
testKey,
baseTestKey,
isStep,
stepSuffix,
testName,
});
}
}
// JSDoc was already retrieved above, no need to get it again
if (!precedingJSDoc) {
fileErrors.push(`Missing JSDoc documentation at "${testName}"`);
} else {
const jsdocContent = precedingJSDoc[0];
// Check for @objective
if (!jsdocContent.match(patterns.objective)) {
fileErrors.push(`Missing @objective in JSDoc at "${testName}"`);
}
// Note: @precondition is optional and should only be included when there are
// non-default requirements for the test
}
// Check for tag declaration
if (!testDeclaration.match(patterns.tagDeclaration)) {
fileErrors.push(`Missing feature tag at "${testName}"`);
}
// Simpler approach to extract test body by using string search and tracking braces to find function boundaries
const testNameEscaped = testName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const testDeclarationPattern = new RegExp(`test\\s*\\([\\s\\n]*['"]${testNameEscaped}['"]`);
// Find the position of the test declaration
const testDeclarationMatch = testDeclarationPattern.exec(fileContent);
let testFnMatch = null;
if (testDeclarationMatch) {
// Find the position of "async" after the test declaration
const startPos = testDeclarationMatch.index;
const asyncPattern = /async\s*\([^)]*\)\s*=>\s*{/g;
asyncPattern.lastIndex = startPos;
const asyncMatch = asyncPattern.exec(fileContent);
if (asyncMatch) {
// Find the position of the opening brace of the function body
const openBracePos = asyncMatch.index + asyncMatch[0].length - 1;
// Find the matching closing brace
let braceCount = 1;
let currentPos = openBracePos + 1;
while (braceCount > 0 && currentPos < fileContent.length) {
const char = fileContent[currentPos];
if (char === '{') {
braceCount++;
} else if (char === '}') {
braceCount--;
}
currentPos++;
}
// If we found the matching closing brace, extract everything between them
if (braceCount === 0) {
const testBody = fileContent.substring(openBracePos + 1, currentPos - 1);
testFnMatch = [null, testBody];
}
}
// If we couldn't extract using the above method, try a more direct string search approach
if (!testFnMatch) {
// Find the test function by searching for the test name and extracting all content until the end of the function
const testIndex =
fileContent.indexOf(`test('${testName}'`) ||
fileContent.indexOf(`test("${testName}"`) ||
fileContent.indexOf(`test(\n '${testName}'`) ||
fileContent.indexOf(`test(\n "${testName}"`);
if (testIndex !== -1) {
// Find the position of "async" which indicates the start of the function body
const asyncIndex = fileContent.indexOf('async', testIndex);
if (asyncIndex !== -1) {
// Find the opening brace after "async"
const openBraceIndex = fileContent.indexOf('{', asyncIndex);
if (openBraceIndex !== -1) {
// Count braces to find the matching closing brace
let braceCount = 1;
let closePos = openBraceIndex + 1;
while (braceCount > 0 && closePos < fileContent.length) {
const char = fileContent[closePos];
if (char === '{') {
braceCount++;
} else if (char === '}') {
braceCount--;
}
closePos++;
}
if (braceCount === 0) {
const testBody = fileContent.substring(openBraceIndex + 1, closePos - 1);
testFnMatch = [null, testBody];
}
}
}
}
}
}
if (testFnMatch) {
const testBody = testFnMatch[1];
// Check for action comments (// #) - we need to be more flexible in our detection
let hasActionComments = false;
const actionPattern = /\/\/\s*#/;
if (actionPattern.test(testBody)) {
hasActionComments = true;
}
if (!hasActionComments) {
fileErrors.push(`Missing action comments at "${testName}" (format: "// # Some descriptive action")`);
}
// Check for verification comments (// *) - we need to be more flexible in our detection
let hasVerificationComments = false;
const verificationPattern = /\/\/\s*\*/;
if (verificationPattern.test(testBody)) {
hasVerificationComments = true;
}
if (!hasVerificationComments) {
fileErrors.push(
`Missing verification comments at "${testName}" (format: "// * Some descriptive verification")`,
);
}
} else {
fileErrors.push(`Could not extract test body for "${testName}"`);
}
});
if (fileErrors.length > 0) {
results.failed++;
results.errors.push({
file: relativeFilePath,
errors: fileErrors,
});
} else {
results.passed++;
}
});
// Print results
console.log('\n' + '-'.repeat(80));
console.log(`${colors.cyan}Test Documentation Format Linter Results${colors.reset}`);
console.log(`Files checked: ${specFiles.length}`);
console.log(`${colors.green}Passed: ${results.passed}${colors.reset}`);
console.log(`${colors.red}Failed: ${results.failed}${colors.reset}`);
console.log(`${colors.yellow}Warnings: ${results.warnings}${colors.reset}`);
console.log(`${colors.cyan}New tests: ${results.newTests ? results.newTests.length : 0}${colors.reset}`);
console.log(`${colors.magenta}Updated tests: ${results.updatedTests ? results.updatedTests.length : 0}${colors.reset}`);
if (results.errors.length > 0) {
console.log('\n' + '-'.repeat(80));
console.log(`${colors.red}Errors:${colors.reset}`);
results.errors.forEach((fileResult) => {
console.log(`\n${colors.cyan}${fileResult.file}${colors.reset}:`);
fileResult.errors.forEach((error) => {
console.log(` ${colors.red}${colors.reset} ${error}`);
});
});
}
// Display new tests summary if any were found
if (results.newTests && results.newTests.length > 0) {
console.log('\n' + '-'.repeat(80));
console.log(`${colors.cyan}New Tests to be Registered:${colors.reset}`);
results.newTests.forEach((newTest) => {
console.log(` ${colors.cyan}${colors.reset} ${newTest.file}: "${newTest.testName}"`);
});
}
// Display updated tests summary if any were found
if (results.updatedTests && results.updatedTests.length > 0) {
console.log('\n' + '-'.repeat(80));
console.log(`${colors.magenta}Tests with Documentation Updates:${colors.reset}`);
// Group the updated tests by base test key
const groupedTests = {};
results.updatedTests.forEach((test) => {
if (!groupedTests[test.baseTestKey]) {
groupedTests[test.baseTestKey] = [];
}
groupedTests[test.baseTestKey].push(test);
});
// Display the tests grouped by base test key
Object.keys(groupedTests).forEach((baseTestKey) => {
const tests = groupedTests[baseTestKey];
// If there's only one test with this base key and it's not a step
if (tests.length === 1 && !tests[0].isStep) {
const test = tests[0];
console.log(
` ${colors.magenta}${colors.reset} ${test.file}: ${test.testKey} "${test.testName.substring(test.testKey.length).trim()}"`,
);
}
// If there are multiple steps of the same test
else {
console.log(
` ${colors.magenta}${colors.reset} ${tests[0].file}: ${colors.cyan}${baseTestKey}${colors.reset} (with ${tests.length} steps):`,
);
// Sort steps by their step number
tests.sort((a, b) => {
if (!a.isStep) return -1;
if (!b.isStep) return 1;
const aNum = parseInt(a.stepSuffix.substring(1), 10);
const bNum = parseInt(b.stepSuffix.substring(1), 10);
return aNum - bNum;
});
// Display each step
tests.forEach((test) => {
const namePart = test.testName.substring(test.baseTestKey.length).trim();
if (test.isStep) {
// Extract the step number without the underscore
const stepNumber = test.stepSuffix.substring(1);
console.log(
` ${colors.magenta}${colors.reset} step ${stepNumber}: "${namePart.substring(test.stepSuffix.length).trim()}"`,
);
} else {
console.log(` ${colors.magenta}${colors.reset} Base test: "${namePart}"`);
}
});
}
});
}
console.log('\n' + '-'.repeat(80));
if (results.failed > 0) {
console.log(`${colors.red}Linter failed!${colors.reset} Please fix the test documentation issues.`);
process.exit(1);
} else {
console.log(`${colors.green}Linter passed!${colors.reset} All spec files follow the required format.`);
process.exit(0);
}

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

@@ -3,91 +3,106 @@
import {expect, test} from '@mattermost/playwright-lib'; import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that users can search for GIFs, select them, and post them correctly when using the center textbox.
*/
test.fixme( test.fixme(
'MM-T5445 Should search, select and post correct Gif when Gif picker is opened from center textbox', 'MM-T5445 searches for GIF from center textbox and posts selected GIF correctly',
{tag: '@gif_picker'},
async ({pw}) => { async ({pw}) => {
// # Initialize a test user
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
// # Log in as a user in new browser context // # Log in as a user in new browser context
const {channelsPage} = await pw.testBrowser.login(user); const {channelsPage} = await pw.testBrowser.login(user);
// # Visit default channel page // # Navigate to default channel page
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// # Open emoji/gif picker // # Open emoji/gif picker from center textbox
await channelsPage.centerView.postCreate.openEmojiPicker(); await channelsPage.centerView.postCreate.openEmojiPicker();
// * Verify emoji/gif picker popup appears
await channelsPage.emojiGifPickerPopup.toBeVisible(); await channelsPage.emojiGifPickerPopup.toBeVisible();
// # Open gif tab // # Switch to GIF tab in the picker
await channelsPage.emojiGifPickerPopup.openGifTab(); await channelsPage.emojiGifPickerPopup.openGifTab();
// # Search for gif // # Search for GIFs using the term "hello"
await channelsPage.emojiGifPickerPopup.searchGif('hello'); await channelsPage.emojiGifPickerPopup.searchGif('hello');
// # Select the first gif // # Select the first GIF from search results
const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} = const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} =
await channelsPage.emojiGifPickerPopup.getNthGif(0); await channelsPage.emojiGifPickerPopup.getNthGif(0);
await firstSearchGifResult.click(); await firstSearchGifResult.click();
// # Send the selected gif as a message // # Send the selected GIF as a message
await channelsPage.centerView.postCreate.sendMessage(); await channelsPage.centerView.postCreate.sendMessage();
// * Verify that last message has the gif // * Verify the posted message contains the selected GIF
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible(); await lastPost.toBeVisible();
await expect(lastPost.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult); await expect(lastPost.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult);
}, },
); );
/**
* @objective Verify that users can search for GIFs, select them, and post them correctly when using the right-hand sidebar.
*/
test.fixme( test.fixme(
'MM-T5446 Should search, select and post correct Gif when Gif picker is opened from RHS textbox', 'MM-T5446 searches for GIF from RHS textbox and posts selected GIF correctly in thread',
{tag: '@gif_picker'},
async ({pw}) => { async ({pw}) => {
// # Initialize a test user
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
// # Log in as a user in new browser context // # Log in as a user in new browser context
const {channelsPage} = await pw.testBrowser.login(user); const {channelsPage} = await pw.testBrowser.login(user);
// # Visit default channel page // # Navigate to default channel page
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// # Send a message // # Post a message to create a thread
await channelsPage.postMessage('Message to open RHS'); await channelsPage.postMessage('Message to open RHS');
// # Open the last post sent in RHS // # Open the message in right-hand sidebar to start a thread
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await lastPost.hover(); await lastPost.hover();
await lastPost.postMenu.toBeVisible(); await lastPost.postMenu.toBeVisible();
await lastPost.postMenu.reply(); await lastPost.postMenu.reply();
// * Verify right sidebar opens and is visible
const sidebarRight = channelsPage.sidebarRight; const sidebarRight = channelsPage.sidebarRight;
await sidebarRight.toBeVisible(); await sidebarRight.toBeVisible();
// # Send a message in the thread // # Post an initial reply in the thread
await sidebarRight.postCreate.toBeVisible(); await sidebarRight.postCreate.toBeVisible();
await sidebarRight.postCreate.writeMessage('Replying to a thread'); await sidebarRight.postCreate.writeMessage('Replying to a thread');
await sidebarRight.postCreate.sendMessage(); await sidebarRight.postCreate.sendMessage();
// # Open emoji/gif picker // # Open emoji/gif picker from the RHS textbox
await sidebarRight.postCreate.openEmojiPicker(); await sidebarRight.postCreate.openEmojiPicker();
// * Verify emoji/gif picker popup appears
await channelsPage.emojiGifPickerPopup.toBeVisible(); await channelsPage.emojiGifPickerPopup.toBeVisible();
// # Open gif tab // # Switch to GIF tab in the picker
await channelsPage.emojiGifPickerPopup.openGifTab(); await channelsPage.emojiGifPickerPopup.openGifTab();
// # Search for gif // # Search for GIFs using the term "hello"
await channelsPage.emojiGifPickerPopup.searchGif('hello'); await channelsPage.emojiGifPickerPopup.searchGif('hello');
// # Select the first gif // # Select the first GIF from search results
const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} = const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} =
await channelsPage.emojiGifPickerPopup.getNthGif(0); await channelsPage.emojiGifPickerPopup.getNthGif(0);
await firstSearchGifResult.click(); await firstSearchGifResult.click();
// # Send the selected gif as a message in the thread // # Send the selected GIF as a message in the thread
await sidebarRight.postCreate.sendMessage(); await sidebarRight.postCreate.sendMessage();
// * Verify that last message has the gif // * Verify the posted message in the thread contains the selected GIF
const lastPostInRHS = await sidebarRight.getLastPost(); const lastPostInRHS = await sidebarRight.getLastPost();
await lastPostInRHS.toBeVisible(); await lastPostInRHS.toBeVisible();
await expect(lastPostInRHS.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult); await expect(lastPostInRHS.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult);

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

@@ -3,9 +3,17 @@
import {expect, test} from '@mattermost/playwright-lib'; import {expect, test} from '@mattermost/playwright-lib';
test('Multiple user mentions test', async ({pw}) => { /**
* @objective Verify that multiple user mentions are displayed properly in the Recent Mentions section.
*
* @precondition
* Two users must be members of the same team
*/
test('displays multiple mentions correctly in Recent Mentions panel', {tag: '@mentions'}, async ({pw}) => {
// # Define the number of mentions to create
const MENTION_COUNT = 20; const MENTION_COUNT = 20;
// # Initialize the first user who will create the mentions
const { const {
team, team,
user: mentioningUser, user: mentioningUser,
@@ -19,9 +27,10 @@ test('Multiple user mentions test', async ({pw}) => {
const mentionedUser = pw.random.user('mentioned'); const mentionedUser = pw.random.user('mentioned');
const {id: mentionedUserID} = await adminClient.createUser(mentionedUser, '', ''); const {id: mentionedUserID} = await adminClient.createUser(mentionedUser, '', '');
// # Add the mentioned user to the team
await adminClient.addToTeam(team.id, mentionedUserID); await adminClient.addToTeam(team.id, mentionedUserID);
// Get the town-square channel data // # Get the town-square channel data
const channels = await userClient.getMyChannels(team.id); const channels = await userClient.getMyChannels(team.id);
const townSquare = channels.find((channel) => channel.name === 'town-square'); const townSquare = channels.find((channel) => channel.name === 'town-square');
@@ -29,7 +38,7 @@ test('Multiple user mentions test', async ({pw}) => {
throw new Error('Town Square channel not found'); throw new Error('Town Square channel not found');
} }
// Use API to create all the mention posts // # Create multiple posts that mention the second user
for (let i = 0; i < MENTION_COUNT; i++) { for (let i = 0; i < MENTION_COUNT; i++) {
const message = `Hey @${mentionedUser.username}, this is mention #${i + 1}`; const message = `Hey @${mentionedUser.username}, this is mention #${i + 1}`;
await userClient.createPost({ await userClient.createPost({
@@ -39,26 +48,24 @@ test('Multiple user mentions test', async ({pw}) => {
}); });
} }
// Login as the mentioned user to check mentions in the UI // # Login as the mentioned user to check mentions in the UI
const {page: mentionedPage, channelsPage: mentionedChannelsPage} = await pw.testBrowser.login(mentionedUser); const {page: mentionedPage, channelsPage: mentionedChannelsPage} = await pw.testBrowser.login(mentionedUser);
await mentionedChannelsPage.goto(team.name, 'town-square'); await mentionedChannelsPage.goto(team.name, 'town-square');
await mentionedChannelsPage.toBeVisible(); await mentionedChannelsPage.toBeVisible();
// Click on the Recent Mentions button in the channel header // # Click on the Recent Mentions button in the channel header
await mentionedPage.getByRole('button', {name: 'Recent mentions'}).click(); await mentionedPage.getByRole('button', {name: 'Recent mentions'}).click();
// Wait for the RHS panel to be visible first // * Verify the right sidebar opens and is visible
await mentionedChannelsPage.sidebarRight.toBeVisible(); await mentionedChannelsPage.sidebarRight.toBeVisible();
// Get all the mention posts in the RHS // # Get all the mention posts in the right sidebar
const mentionPosts = mentionedChannelsPage.sidebarRight.container.locator('.post'); const mentionPosts = mentionedChannelsPage.sidebarRight.container.locator('.post');
// Verify we have the expected number of mention posts // * Verify the correct number of mention posts are displayed
// Note: RHS might not load all 100 at once due to pagination, so we'll check
// a sufficient number is loaded (at least the first page)
await expect(mentionPosts).toHaveCount(MENTION_COUNT); await expect(mentionPosts).toHaveCount(MENTION_COUNT);
// Verify the content of the first few mentions (most recent first) // * Verify the content of each mention displays correctly with the right mention text
for (let i = 0; i < MENTION_COUNT; i++) { for (let i = 0; i < MENTION_COUNT; i++) {
const mentionNumber = MENTION_COUNT - i; const mentionNumber = MENTION_COUNT - i;
const expectedText = `Hey @${mentionedUser.username}, this is mention #${mentionNumber}`; const expectedText = `Hey @${mentionedUser.username}, this is mention #${mentionNumber}`;

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

@@ -3,7 +3,13 @@
import {expect, test} from '@mattermost/playwright-lib'; import {expect, test} from '@mattermost/playwright-lib';
test('MM-T5139: Message Priority - Standard message priority and system setting', async ({pw}) => { /**
* @objective Verify that standard message priority posts correctly without priority labels and functions as expected.
*/
test(
'MM-T5139 posts message with standard priority and verifies no priority labels appear',
{tag: '@message_priority'},
async ({pw}) => {
// # Setup test environment // # Setup test environment
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
@@ -14,35 +20,41 @@ test('MM-T5139: Message Priority - Standard message priority and system setting'
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// Open menu // # Open priority menu
await channelsPage.centerView.postCreate.openPriorityMenu(); await channelsPage.centerView.postCreate.openPriorityMenu();
// Use messagePriority for dialog interactions // * Verify priority dialog appears with standard option selected
await channelsPage.messagePriority.verifyPriorityDialog(); await channelsPage.messagePriority.verifyPriorityDialog();
await channelsPage.messagePriority.verifyStandardOptionSelected(); await channelsPage.messagePriority.verifyStandardOptionSelected();
// # Close menu and post message // # Close priority menu
await channelsPage.messagePriority.closePriorityMenu(); await channelsPage.messagePriority.closePriorityMenu();
// # Post a message with standard priority
const testMessage = 'This is just a test message'; const testMessage = 'This is just a test message';
await channelsPage.postMessage(testMessage); await channelsPage.postMessage(testMessage);
// # Verify message posts without priority label // * Verify message posts correctly with the expected text
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible(); await lastPost.toBeVisible();
await lastPost.toContainText(testMessage); await lastPost.toContainText(testMessage);
// * Verify no priority label appears on the post
await expect(lastPost.container.locator('.post-priority')).not.toBeVisible(); await expect(lastPost.container.locator('.post-priority')).not.toBeVisible();
// # Open post in RHS and verify // # Open post in right-hand sidebar
await lastPost.container.click(); await lastPost.container.click();
await channelsPage.sidebarRight.toBeVisible(); await channelsPage.sidebarRight.toBeVisible();
// # Get RHS post and verify content // * Verify post content appears correctly in RHS
const rhsPost = await channelsPage.sidebarRight.getLastPost(); const rhsPost = await channelsPage.sidebarRight.getLastPost();
await rhsPost.toBeVisible(); await rhsPost.toBeVisible();
await rhsPost.toContainText(testMessage); await rhsPost.toContainText(testMessage);
// * Verify no priority label appears in RHS
await expect(rhsPost.container.locator('.post-priority')).not.toBeVisible(); await expect(rhsPost.container.locator('.post-priority')).not.toBeVisible();
// # Verify RHS formatting bar doesn't have priority button // * Verify RHS formatting bar doesn't include priority button
await expect(channelsPage.sidebarRight.postCreate.priorityButton).not.toBeVisible(); await expect(channelsPage.sidebarRight.postCreate.priorityButton).not.toBeVisible();
}); },
);

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

@@ -3,33 +3,43 @@
import {expect, test} from '@mattermost/playwright-lib'; import {expect, test} from '@mattermost/playwright-lib';
test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, headless, browserName}) => { /**
* @objective Verify that channel-wide mentions with uppercase letters trigger notifications and are properly highlighted.
*
* @precondition
* - Two users are members of the same team
* - Notification permissions are granted in the browser
*/
test(
'MM-T483 triggers notification with uppercase channel-wide mention and highlights message for all users',
{tag: '@notifications'},
async ({pw, headless, browserName}) => {
test.skip( test.skip(
headless && browserName !== 'firefox', headless && browserName !== 'firefox',
'Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.', 'Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.',
); );
// Initialize setup and get the required users and team // # Initialize setup and get the required users and team
const {team, adminUser, user} = await pw.initSetup(); const {team, adminUser, user} = await pw.initSetup();
// Log in as the admin in one browser session and navigate to the "town-square" channel // # Log in as the admin in one browser session and navigate to the "town-square" channel
const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser); const {page: adminPage, channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
await adminChannelsPage.goto(team.name, 'town-square'); await adminChannelsPage.goto(team.name, 'town-square');
await adminChannelsPage.toBeVisible(); await adminChannelsPage.toBeVisible();
// Stub the Notification in the admin's browser to capture notifications // # Stub the Notification in the admin's browser to capture notifications
await pw.stubNotification(adminPage, 'granted'); await pw.stubNotification(adminPage, 'granted');
// Log in as the regular user in a separate browser and navigate to the "off-topic" channel // # Log in as the regular user in a separate browser and navigate to the "off-topic" channel
const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(user); const {channelsPage: otherChannelsPage} = await pw.testBrowser.login(user);
await otherChannelsPage.goto(team.name, 'off-topic'); await otherChannelsPage.goto(team.name, 'off-topic');
await otherChannelsPage.toBeVisible(); await otherChannelsPage.toBeVisible();
// Post a channel-wide mention message "@ALL" in uppercase from the user's browser // # Post a channel-wide mention message "@ALL" in uppercase from the user's browser
const message = `@ALL good morning, ${team.name}!`; const message = `@ALL good morning, ${team.name}!`;
await otherChannelsPage.postMessage(message); await otherChannelsPage.postMessage(message);
// Wait for a notification to be received in the admin's browser and verify its content // * Verify notification is received in the admin's browser with correct content
const notifications = await pw.waitForNotification(adminPage); const notifications = await pw.waitForNotification(adminPage);
expect(notifications.length).toBe(1); expect(notifications.length).toBe(1);
@@ -41,16 +51,19 @@ test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, headles
expect(notification.requireInteraction).toBe(false); expect(notification.requireInteraction).toBe(false);
expect(notification.silent).toBe(false); expect(notification.silent).toBe(false);
// Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted // * Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
const otherLastPost = await otherChannelsPage.getLastPost(); const otherLastPost = await otherChannelsPage.getLastPost();
await otherLastPost.toContainText(message); await otherLastPost.toContainText(message);
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible(); await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible(); await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
// Admin navigates to the "off-topic" channel and verifies the message is posted and highlighted correctly // # Navigate admin to the "off-topic" channel
await adminChannelsPage.goto(team.name, 'off-topic'); await adminChannelsPage.goto(team.name, 'off-topic');
// * Verify the message is posted and highlighted correctly for the admin user
const adminLastPost = await adminChannelsPage.getLastPost(); const adminLastPost = await adminChannelsPage.getLastPost();
await adminLastPost.toContainText(message); await adminLastPost.toContainText(message);
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible(); await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible(); await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
}); },
);

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

@@ -16,40 +16,43 @@ test.beforeEach(async ({pw}) => {
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5643_1 should create a scheduled message from a channel', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5643_1 creates scheduled message from channel and posts at scheduled time',
{tag: '@scheduled_messages'},
async ({pw}) => {
// Set test timeout to 4 mins to wait for the scheduled message to be sent // Set test timeout to 4 mins to wait for the scheduled message to be sent
// which is expected within 2 mins. // which is expected within 2 mins.
test.setTimeout(pw.duration.four_min); test.setTimeout(pw.duration.four_min);
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message // # Create a scheduled message with short delay
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
// * Verify scheduled post indicator with correct date/time // * Verify scheduled post indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count // * Verify scheduled post badge in left sidebar shows count of 1
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1); await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Click "See all link" to navigate to scheduled posts page // # Navigate to scheduled posts page via "See all" link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`;
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1}); await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
// 4. Go back to the channels page // # Return to the channels page
await page.goBack(); await page.goBack();
// * Verify the message has been posted and there's no more scheduled messages // * Verify scheduled message was posted successfully
await pw.waitUntil( await pw.waitUntil(
async () => { async () => {
const post = await channelsPage.getLastPost(); const post = await channelsPage.getLastPost();
@@ -59,10 +62,13 @@ test('MM-T5643_1 should create a scheduled message from a channel', {tag: '@sche
}, },
{timeout: pw.duration.two_min}, {timeout: pw.duration.two_min},
); );
// * Verify scheduled indicators are removed after posting
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible(); await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible(); await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
}); },
);
/** /**
* @objective Verify the ability to create a scheduled message in a thread. * @objective Verify the ability to create a scheduled message in a thread.
@@ -70,32 +76,35 @@ test('MM-T5643_1 should create a scheduled message from a channel', {tag: '@sche
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5643_6 should create a scheduled message under a thread post', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5643_6 creates scheduled message in thread and posts in thread conversation',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Threaded Message ${pw.random.id()}`; const draftMessage = `Scheduled Threaded Message ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Post a message // # Create a root message in the channel
await channelsPage.postMessage('Root Message'); await channelsPage.postMessage('Root Message');
// 3. Reply to a message // # Start a thread by replying to the message
const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread'); const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread');
// 4. Create a scheduled message from the thread // # Create a scheduled message within the thread
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
// * Verify scheduled post indicator with correct date/time // * Verify scheduled post indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage);
// 5. Navigate to scheduled posts page // # Navigate to scheduled posts page using indicator link
await sidebarRight.scheduledPostIndicator.seeAllLink.click(); await sidebarRight.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -103,21 +112,24 @@ test('MM-T5643_6 should create a scheduled message under a thread post', {tag: '
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 6. Hover over the scheduled post and send now // # Send the scheduled message immediately
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.sendNowButton.click(); await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible(); await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click(); await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify the message has been posted and there's no more scheduled messages // * Verify message is posted in the thread
await sidebarRight.toBeVisible(); await sidebarRight.toBeVisible();
const lastPost = await sidebarRight.getLastPost(); const lastPost = await sidebarRight.getLastPost();
await expect(lastPost.body).toContainText(draftMessage); await expect(lastPost.body).toContainText(draftMessage);
// * Verify all scheduled message indicators are removed
await sidebarRight.scheduledPostIndicator.toBeNotVisible(); await sidebarRight.scheduledPostIndicator.toBeNotVisible();
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible(); await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible(); await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
}); },
);
/** /**
* @objective Verify the ability to reschedule a scheduled message. * @objective Verify the ability to reschedule a scheduled message.
@@ -125,29 +137,32 @@ test('MM-T5643_6 should create a scheduled message under a thread post', {tag: '
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5644_2 reschedules message to a future date from scheduled posts page',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset // # Create a scheduled message for tomorrow
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time // * Verify scheduled message indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count // * Verify scheduled post badge appears with count of 1
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1); await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page // # Navigate to scheduled posts page via indicator link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -155,19 +170,18 @@ test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_message
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 4. Reschedule message to 2 days from today // # Reschedule the message to a different date (2 days from now)
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} = await scheduledPostsPage.rescheduleMessage( const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} =
scheduledPost, await scheduledPostsPage.rescheduleMessage(scheduledPost, 2);
2,
);
// 5. Return to channel page // # Return to channel page
await channelsPage.goto(); await channelsPage.goto();
// * Verify the message shows updated scheduled time // * Verify indicator shows the updated scheduled time
const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`; const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage);
}); },
);
/** /**
* @objective Verify the ability to delete a scheduled message. * @objective Verify the ability to delete a scheduled message.
@@ -175,26 +189,29 @@ test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_message
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5645 deletes scheduled message from scheduled posts page and removes all indicators',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset // # Create a scheduled message for tomorrow
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time // * Verify scheduled message indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// 3. Navigate to scheduled posts page // # Navigate to scheduled posts page via indicator link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -202,18 +219,20 @@ test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'},
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 4. Delete the scheduled message // # Delete the scheduled message
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.deleteButton.click(); await scheduledPost.deleteButton.click();
// # Confirm deletion in the modal
await scheduledPostsPage.deleteScheduledPostModal.toBeVisible(); await scheduledPostsPage.deleteScheduledPostModal.toBeVisible();
await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click(); await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click();
// * Verify the scheduled message is removed from the scheduled posts page // * Verify the scheduled message is removed and no longer appears
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible(); await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
await expect(scheduledPostsPage.badge).not.toBeVisible(); await expect(scheduledPostsPage.badge).not.toBeVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
}); },
);
/** /**
* @objective Verify the ability to send a scheduled message immediately. * @objective Verify the ability to send a scheduled message immediately.
@@ -221,26 +240,29 @@ test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'},
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5643_9 should send a scheduled message immediately', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5643_9 sends scheduled message immediately from scheduled posts page',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user, townSquareUrl} = await pw.initSetup(); const {user, townSquareUrl} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset // # Create a scheduled message for tomorrow
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time // * Verify scheduled message indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// 3. Navigate to scheduled posts page // # Navigate to scheduled posts page via indicator link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -248,19 +270,24 @@ test('MM-T5643_9 should send a scheduled message immediately', {tag: '@scheduled
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 4. Send the scheduled message immediately // # Send the scheduled message immediately instead of waiting
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.sendNowButton.click(); await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible(); await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click(); await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages // * Verify page redirects to the channel
await expect(channelsPage.page).toHaveURL(townSquareUrl); await expect(channelsPage.page).toHaveURL(townSquareUrl);
// * Verify scheduled indicators are removed
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible(); await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
// * Verify message was posted in the channel
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toContainText(draftMessage); await expect(lastPost.body).toContainText(draftMessage);
}); },
);
/** /**
* @objective Verify the ability to create a scheduled message from a direct message (DM). * @objective Verify the ability to create a scheduled message from a direct message (DM).
@@ -268,22 +295,25 @@ test('MM-T5643_9 should send a scheduled message immediately', {tag: '@scheduled
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5643_3 should create a scheduled message from a DM', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5643_3 creates scheduled message from DM channel and posts at scheduled time',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user and another user // # Initialize test setup with main user and create a second user
const {user, team, adminClient} = await pw.initSetup(); const {user, team, adminClient} = await pw.initSetup();
const otherUser = await adminClient.createUser(pw.random.user(), '', ''); const otherUser = await adminClient.createUser(pw.random.user(), '', '');
// 2. Login the first user and navigate to a DM channel with the other user // # Login as first user and navigate to DM channel with second user
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, `@${otherUser.username}`); await channelsPage.goto(team.name, `@${otherUser.username}`);
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 3. Create a scheduled message with 1 day offset // # Create a scheduled message for tomorrow in the DM
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled message indicator appears with correct date/time // * Verify appropriate scheduled message indicator appears
let indicatorMessage; let indicatorMessage;
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) { if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
indicatorMessage = 'You have one scheduled message.'; indicatorMessage = 'You have one scheduled message.';
@@ -293,14 +323,14 @@ test('MM-T5643_3 should create a scheduled message from a DM', {tag: '@scheduled
await channelsPage.centerView.scheduledPostIndicator.toBeVisible(); await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage); await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage);
// 4. Navigate to scheduled posts page // # Navigate to scheduled posts page using appropriate link
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) { if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click(); await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click();
} else { } else {
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
} }
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -308,19 +338,24 @@ test('MM-T5643_3 should create a scheduled message from a DM', {tag: '@scheduled
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 5. Send the scheduled message immediately // # Send the scheduled message immediately instead of waiting
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.sendNowButton.click(); await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible(); await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click(); await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the DM channel, message is posted and there's no more scheduled messages // * Verify page redirects to the DM channel
await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`); await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
// * Verify scheduled indicators are removed
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible(); await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
// * Verify message was posted in the DM channel
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toContainText(draftMessage); await expect(lastPost.body).toContainText(draftMessage);
}); },
);
/** /**
* @objective Verify the ability to convert a draft message to a scheduled message. * @objective Verify the ability to convert a draft message to a scheduled message.
@@ -328,40 +363,46 @@ test('MM-T5643_3 should create a scheduled message from a DM', {tag: '@scheduled
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5648 should create a draft and then schedule it', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5648 converts draft message to scheduled message from drafts page',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user, team} = await pw.initSetup(); const {user, team} = await pw.initSetup();
const {channelsPage, draftsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, draftsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a draft message // # Create a draft message without sending it
await channelsPage.centerView.postCreate.input.fill(draftMessage); await channelsPage.centerView.postCreate.input.fill(draftMessage);
// 3. Go to drafts page // # Navigate to the drafts page
await draftsPage.goto(team.name); await draftsPage.goto(team.name);
await draftsPage.toBeVisible(); await draftsPage.toBeVisible();
// * Verify draft count badge shows one draft
expect(await draftsPage.getBadgeCountOnTab()).toBe('1'); expect(await draftsPage.getBadgeCountOnTab()).toBe('1');
// * Verify draft message exists // * Verify draft message content appears correctly
const draftedPost = await draftsPage.getLastPost(); const draftedPost = await draftsPage.getLastPost();
await expect(draftedPost.panelBody).toContainText(draftMessage); await expect(draftedPost.panelBody).toContainText(draftMessage);
// 4. Open schedule modal from draft and schedule it to the next 2 days // # Schedule the draft for 2 days in the future
await draftedPost.hover(); await draftedPost.hover();
await draftedPost.scheduleButton.click(); await draftedPost.scheduleButton.click();
await draftsPage.scheduleMessageModal.toBeVisible(); await draftsPage.scheduleMessageModal.toBeVisible();
const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2); const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2);
// 5. Navigate to scheduled posts page // # Navigate to scheduled posts page
await scheduledPostsPage.goto(team.name); await scheduledPostsPage.goto(team.name);
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1}); await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
}); },
);
/** /**
* @objective Verify the ability to edit a scheduled message before it's sent. * @objective Verify the ability to edit a scheduled message before it's sent.
@@ -369,29 +410,32 @@ test('MM-T5648 should create a draft and then schedule it', {tag: '@scheduled_me
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => { test(
'MM-T5644_1 edits scheduled message content while preserving scheduled time',
{tag: '@scheduled_messages'},
async ({pw}) => {
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user, townSquareUrl} = await pw.initSetup(); const {user, townSquareUrl} = await pw.initSetup();
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message with 2 days offset // # Create a scheduled message for 2 days in the future
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
// * Verify scheduled message indicator appears with correct date/time // * Verify scheduled message indicator shows correct date and time
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`; const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage); await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
// * Verify scheduled post badge in left sidebar shows correct count // * Verify scheduled post badge shows count of 1
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1); await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page // # Navigate to scheduled posts page via indicator link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -399,32 +443,37 @@ test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, asy
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 4. Hover and click edit button // # Edit the scheduled message content
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.editButton.click(); await scheduledPost.editButton.click();
// 5. Edit the scheduled message
const updatedText = 'updated text'; const updatedText = 'updated text';
await scheduledPost.editTextBox.fill(updatedText); await scheduledPost.editTextBox.fill(updatedText);
await scheduledPost.saveButton.click(); await scheduledPost.saveButton.click();
// 6. Verify the edited message appears in the channel // * Verify the edited message content is updated
await expect(scheduledPost.panelBody).toContainText(updatedText); await expect(scheduledPost.panelBody).toContainText(updatedText);
// * Verify scheduled date/time remains unchanged
await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`); await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`);
// 7. Send the message immediately // # Send the edited message immediately
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.sendNowButton.click(); await scheduledPost.sendNowButton.click();
await scheduledPostsPage.sendMessageNowModal.toBeVisible(); await scheduledPostsPage.sendMessageNowModal.toBeVisible();
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click(); await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages // * Verify page redirects to the channel
await expect(channelsPage.page).toHaveURL(townSquareUrl); await expect(channelsPage.page).toHaveURL(townSquareUrl);
// * Verify scheduled indicators are removed
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible(); await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible(); await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
// * Verify edited message was posted in the channel
const lastPost = await channelsPage.getLastPost(); const lastPost = await channelsPage.getLastPost();
await expect(lastPost.body).toHaveText(updatedText); await expect(lastPost.body).toHaveText(updatedText);
}); },
);
/** /**
* @objective Verify the ability to copy a scheduled message to clipboard. * @objective Verify the ability to copy a scheduled message to clipboard.
@@ -432,28 +481,31 @@ test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, asy
* @precondition * @precondition
* A test server with valid license to support scheduled message features * A test server with valid license to support scheduled message features
*/ */
test('MM-T5650 should copy scheduled message', {tag: '@scheduled_messages'}, async ({pw, browserName}) => { test(
// Skip this test in Firefox clipboard permissions are not supported 'MM-T5650 copies scheduled message text to clipboard for reuse',
{tag: '@scheduled_messages'},
async ({pw, browserName}) => {
// # Skip this test in Firefox since clipboard permissions are not supported
test.skip(browserName === 'firefox', 'Test not supported in Firefox'); test.skip(browserName === 'firefox', 'Test not supported in Firefox');
const draftMessage = `Scheduled Draft ${pw.random.id()}`; const draftMessage = `Scheduled Draft ${pw.random.id()}`;
// 1. Setup test user, login and navigate to a channel // # Initialize test user, login and navigate to a channel
const {user} = await pw.initSetup(); const {user} = await pw.initSetup();
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user); const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(); await channelsPage.goto();
await channelsPage.toBeVisible(); await channelsPage.toBeVisible();
// 2. Create a scheduled message with 1 day offset // # Create a scheduled message for tomorrow
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1); const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
// * Verify scheduled post badge in left sidebar shows correct count // * Verify scheduled post badge shows count of 1
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1); await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
// 3. Navigate to scheduled posts page // # Navigate to scheduled posts page via indicator link
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click(); await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
// * Verify scheduled posts page displays correct information // * Verify scheduled post appears with correct information
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`; const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, { const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
draftMessage, draftMessage,
@@ -461,22 +513,23 @@ test('MM-T5650 should copy scheduled message', {tag: '@scheduled_messages'}, asy
badgeCountOnTab: 1, badgeCountOnTab: 1,
}); });
// 4. Copy the scheduled message // # Copy the scheduled message text to clipboard
await scheduledPost.hover(); await scheduledPost.hover();
await scheduledPost.copyTextButton.click(); await scheduledPost.copyTextButton.click();
// 5. Return to channel page // # Return to the channel page
await page.goBack(); await page.goBack();
// 6. Paste the copied message in post creator // # Paste the copied message into the post input box
await channelsPage.centerView.postCreate.input.focus(); await channelsPage.centerView.postCreate.input.focus();
await page.keyboard.down('ControlOrMeta'); await page.keyboard.down('ControlOrMeta');
await page.keyboard.press('V'); await page.keyboard.press('V');
await page.keyboard.up('ControlOrMeta'); await page.keyboard.up('ControlOrMeta');
// * Verify the copied message is pasted in the post input box // * Verify the clipboard content was pasted correctly
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage); await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
}); },
);
/** /**
* Verifies that the scheduled post indicator is visible and displays the correct date and time. * Verifies that the scheduled post indicator is visible and displays the correct date and time.

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

@@ -3,38 +3,49 @@
import {test} from '@mattermost/playwright-lib'; import {test} from '@mattermost/playwright-lib';
test('/signup_email', async ({pw, page, browserName, viewport}, testInfo) => { /**
// Set up the page not to redirect to the landing page * @objective Verify the appearance of the signup email page in normal and error states
*/
test(
'signup_email visual verification',
{tag: '@visual_signup'},
async ({pw, page, browserName, viewport}, testInfo) => {
// # Set up the page not to redirect to the landing page
await pw.hasSeenLandingPage(); await pw.hasSeenLandingPage();
// Go to login page // # Navigate to login page
const {adminClient} = await pw.getAdminClient(); const {adminClient} = await pw.getAdminClient();
await pw.loginPage.goto(); await pw.loginPage.goto();
await pw.loginPage.toBeVisible(); await pw.loginPage.toBeVisible();
// Create an account // # Click on create account link
await pw.loginPage.createAccountLink.click(); await pw.loginPage.createAccountLink.click();
// Should have redirected to signup page // * Verify redirection to signup page
await pw.signupPage.toBeVisible(); await pw.signupPage.toBeVisible();
// Click to other element to remove focus from email input // # Remove focus from email input by clicking elsewhere
await pw.signupPage.title.click(); await pw.signupPage.title.click();
// Match snapshot of signup_email page // # Get license information to determine snapshot suffix
const testArgs = {page, browserName, viewport};
const license = await adminClient.getClientLicenseOld(); const license = await adminClient.getClientLicenseOld();
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition'; const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
const testArgs = {page, browserName, viewport};
// * Verify visual appearance of signup page
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs); await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
// Click sign in button without entering user credential // # Attempt to create account with invalid credentials
const invalidUser = {email: 'invalid', username: 'a', password: 'b'}; const invalidUser = {email: 'invalid', username: 'a', password: 'b'};
await pw.signupPage.create(invalidUser, false); await pw.signupPage.create(invalidUser, false);
// * Verify error messages appear for each field
await pw.signupPage.emailError.waitFor(); await pw.signupPage.emailError.waitFor();
await pw.signupPage.usernameError.waitFor(); await pw.signupPage.usernameError.waitFor();
await pw.signupPage.passwordError.waitFor(); await pw.signupPage.passwordError.waitFor();
await pw.waitForAnimationEnd(pw.signupPage.bodyCard); await pw.waitForAnimationEnd(pw.signupPage.bodyCard);
// Match snapshot of signup_email page // * Verify visual appearance of signup page with errors
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs); await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs);
}); },
);

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

После

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

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

После

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

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

После

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

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

После

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

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

После

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

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

После

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