mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
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>
This commit is contained in:
parent
a358401772
commit
2116a6d94a
18 changed files with 1721 additions and 487 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -162,5 +162,5 @@ docker-compose.override.yaml
|
|||
|
||||
|
||||
**/CLAUDE.local.md
|
||||
CLAUDE.md
|
||||
./CLAUDE.md
|
||||
|
||||
|
|
|
|||
242
e2e-tests/playwright/CLAUDE.md
Normal file
242
e2e-tests/playwright/CLAUDE.md
Normal file
|
|
@ -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
generated
483
e2e-tests/playwright/package-lock.json
generated
|
|
@ -26,6 +26,7 @@
|
|||
"eslint-import-resolver-typescript": "4.3.4",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"glob": "11.0.2",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
|
@ -402,6 +403,24 @@
|
|||
"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": {
|
||||
"resolved": "../../webapp/platform/client",
|
||||
"link": true
|
||||
|
|
@ -1772,6 +1791,19 @@
|
|||
"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": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
|
|
@ -2508,6 +2540,13 @@
|
|||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
|
@ -2515,6 +2554,13 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
|
|
@ -3418,6 +3464,23 @@
|
|||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
|
|
@ -3614,21 +3677,24 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
|
||||
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
|
||||
"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"
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^4.0.1",
|
||||
"minimatch": "^10.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
|
|
@ -3647,26 +3713,20 @@
|
|||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
|
|
@ -3719,6 +3779,52 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -4178,6 +4284,16 @@
|
|||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
||||
|
|
@ -4423,6 +4539,22 @@
|
|||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -4550,6 +4682,16 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||
|
|
@ -4661,6 +4803,16 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -4938,6 +5090,13 @@
|
|||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
|
|
@ -5019,6 +5178,23 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "8.2.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
||||
|
|
@ -5746,6 +5965,19 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
|
|
@ -5827,6 +6059,70 @@
|
|||
"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": {
|
||||
"version": "1.2.10",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
|
|
@ -6406,6 +6742,101 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
"build:watch": "npm run build:watch --workspaces",
|
||||
"tsc": "tsc -b && npm run tsc --workspaces",
|
||||
"lint": "eslint .",
|
||||
"lint:test-docs": "node script/lint-test-docs.js",
|
||||
"prettier": "prettier . --check",
|
||||
"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: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",
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
"eslint-import-resolver-typescript": "4.3.4",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"glob": "11.0.2",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
|
|
|||
62
e2e-tests/playwright/script/README.md
Normal file
62
e2e-tests/playwright/script/README.md
Normal file
|
|
@ -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
Executable file
386
e2e-tests/playwright/script/lint-test-docs.js
Executable file
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* @objective Verify that users can search for GIFs, select them, and post them correctly when using the center textbox.
|
||||
*/
|
||||
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}) => {
|
||||
// # Initialize a test user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in as a user in new browser context
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit default channel page
|
||||
// # Navigate to default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open emoji/gif picker
|
||||
// # Open emoji/gif picker from center textbox
|
||||
await channelsPage.centerView.postCreate.openEmojiPicker();
|
||||
|
||||
// * Verify emoji/gif picker popup appears
|
||||
await channelsPage.emojiGifPickerPopup.toBeVisible();
|
||||
|
||||
// # Open gif tab
|
||||
// # Switch to GIF tab in the picker
|
||||
await channelsPage.emojiGifPickerPopup.openGifTab();
|
||||
|
||||
// # Search for gif
|
||||
// # Search for GIFs using the term "hello"
|
||||
await channelsPage.emojiGifPickerPopup.searchGif('hello');
|
||||
|
||||
// # Select the first gif
|
||||
// # Select the first GIF from search results
|
||||
const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} =
|
||||
await channelsPage.emojiGifPickerPopup.getNthGif(0);
|
||||
await firstSearchGifResult.click();
|
||||
|
||||
// # Send the selected gif as a message
|
||||
// # Send the selected GIF as a message
|
||||
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();
|
||||
await lastPost.toBeVisible();
|
||||
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(
|
||||
'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}) => {
|
||||
// # Initialize a test user
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in as a user in new browser context
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit default channel page
|
||||
// # Navigate to default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Send a message
|
||||
// # Post a message to create a thread
|
||||
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();
|
||||
await lastPost.hover();
|
||||
await lastPost.postMenu.toBeVisible();
|
||||
await lastPost.postMenu.reply();
|
||||
|
||||
// * Verify right sidebar opens and is visible
|
||||
const sidebarRight = channelsPage.sidebarRight;
|
||||
await sidebarRight.toBeVisible();
|
||||
|
||||
// # Send a message in the thread
|
||||
// # Post an initial reply in the thread
|
||||
await sidebarRight.postCreate.toBeVisible();
|
||||
await sidebarRight.postCreate.writeMessage('Replying to a thread');
|
||||
await sidebarRight.postCreate.sendMessage();
|
||||
|
||||
// # Open emoji/gif picker
|
||||
// # Open emoji/gif picker from the RHS textbox
|
||||
await sidebarRight.postCreate.openEmojiPicker();
|
||||
|
||||
// * Verify emoji/gif picker popup appears
|
||||
await channelsPage.emojiGifPickerPopup.toBeVisible();
|
||||
|
||||
// # Open gif tab
|
||||
// # Switch to GIF tab in the picker
|
||||
await channelsPage.emojiGifPickerPopup.openGifTab();
|
||||
|
||||
// # Search for gif
|
||||
// # Search for GIFs using the term "hello"
|
||||
await channelsPage.emojiGifPickerPopup.searchGif('hello');
|
||||
|
||||
// # Select the first gif
|
||||
// # Select the first GIF from search results
|
||||
const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} =
|
||||
await channelsPage.emojiGifPickerPopup.getNthGif(0);
|
||||
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();
|
||||
|
||||
// * Verify that last message has the gif
|
||||
// * Verify the posted message in the thread contains the selected GIF
|
||||
const lastPostInRHS = await sidebarRight.getLastPost();
|
||||
await lastPostInRHS.toBeVisible();
|
||||
await expect(lastPostInRHS.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,17 @@
|
|||
|
||||
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;
|
||||
|
||||
// # Initialize the first user who will create the mentions
|
||||
const {
|
||||
team,
|
||||
user: mentioningUser,
|
||||
|
|
@ -19,9 +27,10 @@ test('Multiple user mentions test', async ({pw}) => {
|
|||
const mentionedUser = pw.random.user('mentioned');
|
||||
const {id: mentionedUserID} = await adminClient.createUser(mentionedUser, '', '');
|
||||
|
||||
// # Add the mentioned user to the team
|
||||
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 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');
|
||||
}
|
||||
|
||||
// Use API to create all the mention posts
|
||||
// # Create multiple posts that mention the second user
|
||||
for (let i = 0; i < MENTION_COUNT; i++) {
|
||||
const message = `Hey @${mentionedUser.username}, this is mention #${i + 1}`;
|
||||
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);
|
||||
await mentionedChannelsPage.goto(team.name, 'town-square');
|
||||
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();
|
||||
|
||||
// Wait for the RHS panel to be visible first
|
||||
// * Verify the right sidebar opens and is visible
|
||||
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');
|
||||
|
||||
// Verify we have the expected number of mention posts
|
||||
// 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)
|
||||
// * Verify the correct number of mention posts are displayed
|
||||
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++) {
|
||||
const mentionNumber = MENTION_COUNT - i;
|
||||
const expectedText = `Hey @${mentionedUser.username}, this is mention #${mentionNumber}`;
|
||||
|
|
|
|||
|
|
@ -3,46 +3,58 @@
|
|||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test('MM-T5139: Message Priority - Standard message priority and system setting', async ({pw}) => {
|
||||
// # Setup test environment
|
||||
const {user} = await pw.initSetup();
|
||||
/**
|
||||
* @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
|
||||
const {user} = await pw.initSetup();
|
||||
|
||||
// # Log in as a user in new browser context
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
// # Log in as a user in new browser context
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Visit default channel page
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// Open menu
|
||||
await channelsPage.centerView.postCreate.openPriorityMenu();
|
||||
// # Open priority menu
|
||||
await channelsPage.centerView.postCreate.openPriorityMenu();
|
||||
|
||||
// Use messagePriority for dialog interactions
|
||||
await channelsPage.messagePriority.verifyPriorityDialog();
|
||||
await channelsPage.messagePriority.verifyStandardOptionSelected();
|
||||
// * Verify priority dialog appears with standard option selected
|
||||
await channelsPage.messagePriority.verifyPriorityDialog();
|
||||
await channelsPage.messagePriority.verifyStandardOptionSelected();
|
||||
|
||||
// # Close menu and post message
|
||||
await channelsPage.messagePriority.closePriorityMenu();
|
||||
// # Close priority menu
|
||||
await channelsPage.messagePriority.closePriorityMenu();
|
||||
|
||||
const testMessage = 'This is just a test message';
|
||||
await channelsPage.postMessage(testMessage);
|
||||
// # Post a message with standard priority
|
||||
const testMessage = 'This is just a test message';
|
||||
await channelsPage.postMessage(testMessage);
|
||||
|
||||
// # Verify message posts without priority label
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toBeVisible();
|
||||
await lastPost.toContainText(testMessage);
|
||||
await expect(lastPost.container.locator('.post-priority')).not.toBeVisible();
|
||||
// * Verify message posts correctly with the expected text
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await lastPost.toBeVisible();
|
||||
await lastPost.toContainText(testMessage);
|
||||
|
||||
// # Open post in RHS and verify
|
||||
await lastPost.container.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
// * Verify no priority label appears on the post
|
||||
await expect(lastPost.container.locator('.post-priority')).not.toBeVisible();
|
||||
|
||||
// # Get RHS post and verify content
|
||||
const rhsPost = await channelsPage.sidebarRight.getLastPost();
|
||||
await rhsPost.toBeVisible();
|
||||
await rhsPost.toContainText(testMessage);
|
||||
await expect(rhsPost.container.locator('.post-priority')).not.toBeVisible();
|
||||
// # Open post in right-hand sidebar
|
||||
await lastPost.container.click();
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// # Verify RHS formatting bar doesn't have priority button
|
||||
await expect(channelsPage.sidebarRight.postCreate.priorityButton).not.toBeVisible();
|
||||
});
|
||||
// * Verify post content appears correctly in RHS
|
||||
const rhsPost = await channelsPage.sidebarRight.getLastPost();
|
||||
await rhsPost.toBeVisible();
|
||||
await rhsPost.toContainText(testMessage);
|
||||
|
||||
// * Verify no priority label appears in RHS
|
||||
await expect(rhsPost.container.locator('.post-priority')).not.toBeVisible();
|
||||
|
||||
// * Verify RHS formatting bar doesn't include priority button
|
||||
await expect(channelsPage.sidebarRight.postCreate.priorityButton).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,54 +3,67 @@
|
|||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, headless, browserName}) => {
|
||||
test.skip(
|
||||
headless && browserName !== 'firefox',
|
||||
'Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.',
|
||||
);
|
||||
/**
|
||||
* @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(
|
||||
headless && browserName !== 'firefox',
|
||||
'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
|
||||
const {team, adminUser, user} = await pw.initSetup();
|
||||
// # Initialize setup and get the required users and team
|
||||
const {team, adminUser, user} = await pw.initSetup();
|
||||
|
||||
// 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);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
// # 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);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
|
||||
// Stub the Notification in the admin's browser to capture notifications
|
||||
await pw.stubNotification(adminPage, 'granted');
|
||||
// # Stub the Notification in the admin's browser to capture notifications
|
||||
await pw.stubNotification(adminPage, 'granted');
|
||||
|
||||
// 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);
|
||||
await otherChannelsPage.goto(team.name, 'off-topic');
|
||||
await otherChannelsPage.toBeVisible();
|
||||
// # 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);
|
||||
await otherChannelsPage.goto(team.name, 'off-topic');
|
||||
await otherChannelsPage.toBeVisible();
|
||||
|
||||
// Post a channel-wide mention message "@ALL" in uppercase from the user's browser
|
||||
const message = `@ALL good morning, ${team.name}!`;
|
||||
await otherChannelsPage.postMessage(message);
|
||||
// # Post a channel-wide mention message "@ALL" in uppercase from the user's browser
|
||||
const message = `@ALL good morning, ${team.name}!`;
|
||||
await otherChannelsPage.postMessage(message);
|
||||
|
||||
// Wait for a notification to be received in the admin's browser and verify its content
|
||||
const notifications = await pw.waitForNotification(adminPage);
|
||||
expect(notifications.length).toBe(1);
|
||||
// * Verify notification is received in the admin's browser with correct content
|
||||
const notifications = await pw.waitForNotification(adminPage);
|
||||
expect(notifications.length).toBe(1);
|
||||
|
||||
const notification = notifications[0];
|
||||
expect(notification.title).toBe('Off-Topic');
|
||||
expect(notification.body).toBe(`@${user.username}: ${message}`);
|
||||
expect(notification.tag).toBe(`@${user.username}: ${message}`);
|
||||
expect(notification.icon).toContain('.png');
|
||||
expect(notification.requireInteraction).toBe(false);
|
||||
expect(notification.silent).toBe(false);
|
||||
const notification = notifications[0];
|
||||
expect(notification.title).toBe('Off-Topic');
|
||||
expect(notification.body).toBe(`@${user.username}: ${message}`);
|
||||
expect(notification.tag).toBe(`@${user.username}: ${message}`);
|
||||
expect(notification.icon).toContain('.png');
|
||||
expect(notification.requireInteraction).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
|
||||
const otherLastPost = await otherChannelsPage.getLastPost();
|
||||
await otherLastPost.toContainText(message);
|
||||
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
// * 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();
|
||||
await otherLastPost.toContainText(message);
|
||||
await expect(otherLastPost.container.locator('.mention--highlight')).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
|
||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||
await adminLastPost.toContainText(message);
|
||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
});
|
||||
// # Navigate admin to the "off-topic" channel
|
||||
await adminChannelsPage.goto(team.name, 'off-topic');
|
||||
|
||||
// * Verify the message is posted and highlighted correctly for the admin user
|
||||
const adminLastPost = await adminChannelsPage.getLastPost();
|
||||
await adminLastPost.toContainText(message);
|
||||
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
|
||||
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,53 +16,59 @@ test.beforeEach(async ({pw}) => {
|
|||
* @precondition
|
||||
* 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}) => {
|
||||
// Set test timeout to 4 mins to wait for the scheduled message to be sent
|
||||
// which is expected within 2 mins.
|
||||
test.setTimeout(pw.duration.four_min);
|
||||
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
|
||||
// which is expected within 2 mins.
|
||||
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
|
||||
const {user} = await pw.initSetup();
|
||||
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
|
||||
// # Create a scheduled message with short delay
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
|
||||
|
||||
// * Verify scheduled post indicator with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled post indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// * Verify scheduled post badge in left sidebar shows correct count
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
// * Verify scheduled post badge in left sidebar shows count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
|
||||
// 3. Click "See all link" to navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via "See all" link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
|
||||
// 4. Go back to the channels page
|
||||
await page.goBack();
|
||||
// # Return to the channels page
|
||||
await page.goBack();
|
||||
|
||||
// * Verify the message has been posted and there's no more scheduled messages
|
||||
await pw.waitUntil(
|
||||
async () => {
|
||||
const post = await channelsPage.getLastPost();
|
||||
const content = await post.container.textContent();
|
||||
// * Verify scheduled message was posted successfully
|
||||
await pw.waitUntil(
|
||||
async () => {
|
||||
const post = await channelsPage.getLastPost();
|
||||
const content = await post.container.textContent();
|
||||
|
||||
return content?.includes(draftMessage);
|
||||
},
|
||||
{timeout: pw.duration.two_min},
|
||||
);
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
});
|
||||
return content?.includes(draftMessage);
|
||||
},
|
||||
{timeout: pw.duration.two_min},
|
||||
);
|
||||
|
||||
// * Verify scheduled indicators are removed after posting
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to create a scheduled message in a thread.
|
||||
|
|
@ -70,54 +76,60 @@ test('MM-T5643_1 should create a scheduled message from a channel', {tag: '@sche
|
|||
* @precondition
|
||||
* 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}) => {
|
||||
const draftMessage = `Scheduled Threaded Message ${pw.random.id()}`;
|
||||
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()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Post a message
|
||||
await channelsPage.postMessage('Root Message');
|
||||
// # Create a root message in the channel
|
||||
await channelsPage.postMessage('Root Message');
|
||||
|
||||
// 3. Reply to a message
|
||||
const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread');
|
||||
// # Start a thread by replying to the message
|
||||
const {sidebarRight} = await channelsPage.replyToLastPost('Replying to a thread');
|
||||
|
||||
// 4. Create a scheduled message from the thread
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
|
||||
// # Create a scheduled message within the thread
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled post indicator with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled post indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// 5. Navigate to scheduled posts page
|
||||
await sidebarRight.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page using indicator link
|
||||
await sidebarRight.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 6. Hover over the scheduled post and send now
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
// # Send the scheduled message immediately
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
|
||||
// * Verify the message has been posted and there's no more scheduled messages
|
||||
await sidebarRight.toBeVisible();
|
||||
const lastPost = await sidebarRight.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
await sidebarRight.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
});
|
||||
// * Verify message is posted in the thread
|
||||
await sidebarRight.toBeVisible();
|
||||
const lastPost = await sidebarRight.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
|
||||
// * Verify all scheduled message indicators are removed
|
||||
await sidebarRight.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to reschedule a scheduled message.
|
||||
|
|
@ -125,49 +137,51 @@ test('MM-T5643_6 should create a scheduled message under a thread post', {tag: '
|
|||
* @precondition
|
||||
* A test server with valid license to support scheduled message features
|
||||
*/
|
||||
test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
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()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message with 1 day offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
// # Create a scheduled message for tomorrow
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator appears with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// * Verify scheduled post badge in left sidebar shows correct count
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
// * Verify scheduled post badge appears with count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
|
||||
// 3. Navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 4. Reschedule message to 2 days from today
|
||||
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} = await scheduledPostsPage.rescheduleMessage(
|
||||
scheduledPost,
|
||||
2,
|
||||
);
|
||||
// # Reschedule the message to a different date (2 days from now)
|
||||
const {selectedDate: newSelectedDate, selectedTime: newSelectedTime} =
|
||||
await scheduledPostsPage.rescheduleMessage(scheduledPost, 2);
|
||||
|
||||
// 5. Return to channel page
|
||||
await channelsPage.goto();
|
||||
// # Return to channel page
|
||||
await channelsPage.goto();
|
||||
|
||||
// * Verify the message shows updated scheduled time
|
||||
const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage);
|
||||
});
|
||||
// * Verify indicator shows the updated scheduled time
|
||||
const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to delete a scheduled message.
|
||||
|
|
@ -175,45 +189,50 @@ test('MM-T5644 should reschedule a scheduled message', {tag: '@scheduled_message
|
|||
* @precondition
|
||||
* A test server with valid license to support scheduled message features
|
||||
*/
|
||||
test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
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()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message with 1 day offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
// # Create a scheduled message for tomorrow
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator appears with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// 3. Navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 4. Delete the scheduled message
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.deleteButton.click();
|
||||
// # Delete the scheduled message
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.deleteButton.click();
|
||||
|
||||
await scheduledPostsPage.deleteScheduledPostModal.toBeVisible();
|
||||
await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click();
|
||||
// # Confirm deletion in the modal
|
||||
await scheduledPostsPage.deleteScheduledPostModal.toBeVisible();
|
||||
await scheduledPostsPage.deleteScheduledPostModal.deleteButton.click();
|
||||
|
||||
// * Verify the scheduled message is removed from the scheduled posts page
|
||||
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
});
|
||||
// * Verify the scheduled message is removed and no longer appears
|
||||
await expect(scheduledPostsPage.noScheduledDrafts).toBeVisible();
|
||||
await expect(scheduledPostsPage.badge).not.toBeVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to send a scheduled message immediately.
|
||||
|
|
@ -221,46 +240,54 @@ test('MM-T5645 should delete a scheduled message', {tag: '@scheduled_messages'},
|
|||
* @precondition
|
||||
* 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}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
test(
|
||||
'MM-T5643_9 sends scheduled message immediately from scheduled posts page',
|
||||
{tag: '@scheduled_messages'},
|
||||
async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user, townSquareUrl} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user, townSquareUrl} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message with 1 day offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
// # Create a scheduled message for tomorrow
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator appears with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// 3. Navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 4. Send the scheduled message immediately
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
// # Send the scheduled message immediately instead of waiting
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
|
||||
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages
|
||||
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
});
|
||||
// * Verify page redirects to the channel
|
||||
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
||||
|
||||
// * Verify scheduled indicators are removed
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
|
||||
// * Verify message was posted in the channel
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to create a scheduled message from a direct message (DM).
|
||||
|
|
@ -268,59 +295,67 @@ test('MM-T5643_9 should send a scheduled message immediately', {tag: '@scheduled
|
|||
* @precondition
|
||||
* 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}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
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()}`;
|
||||
|
||||
// 1. Setup test user and another user
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const otherUser = await adminClient.createUser(pw.random.user(), '', '');
|
||||
// # Initialize test setup with main user and create a second user
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
const otherUser = await adminClient.createUser(pw.random.user(), '', '');
|
||||
|
||||
// 2. Login the first user and navigate to a DM channel with the other user
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
||||
await channelsPage.toBeVisible();
|
||||
// # Login as first user and navigate to DM channel with second user
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, `@${otherUser.username}`);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 3. Create a scheduled message with 1 day offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
// # Create a scheduled message for tomorrow in the DM
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator appears with correct date/time
|
||||
let indicatorMessage;
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
indicatorMessage = 'You have one scheduled message.';
|
||||
} else {
|
||||
indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
}
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
|
||||
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage);
|
||||
// * Verify appropriate scheduled message indicator appears
|
||||
let indicatorMessage;
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
indicatorMessage = 'You have one scheduled message.';
|
||||
} else {
|
||||
indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
}
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
|
||||
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage);
|
||||
|
||||
// 4. Navigate to scheduled posts page
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click();
|
||||
} else {
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
}
|
||||
// # Navigate to scheduled posts page using appropriate link
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
await channelsPage.centerView.scheduledPostIndicator.scheduledMessageLink.click();
|
||||
} else {
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
}
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 5. Send the scheduled message immediately
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
// # Send the scheduled message immediately instead of waiting
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
|
||||
// * Verify it redirects to the DM channel, message is posted and there's no more scheduled messages
|
||||
await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
});
|
||||
// * Verify page redirects to the DM channel
|
||||
await expect(channelsPage.page).toHaveURL(`/${team.name}/messages/@${otherUser.username}`);
|
||||
|
||||
// * Verify scheduled indicators are removed
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
|
||||
// * Verify message was posted in the DM channel
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toContainText(draftMessage);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
test(
|
||||
'MM-T5648 converts draft message to scheduled message from drafts page',
|
||||
{tag: '@scheduled_messages'},
|
||||
async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, draftsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user, team} = await pw.initSetup();
|
||||
const {channelsPage, draftsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a draft message
|
||||
await channelsPage.centerView.postCreate.input.fill(draftMessage);
|
||||
// # Create a draft message without sending it
|
||||
await channelsPage.centerView.postCreate.input.fill(draftMessage);
|
||||
|
||||
// 3. Go to drafts page
|
||||
await draftsPage.goto(team.name);
|
||||
await draftsPage.toBeVisible();
|
||||
expect(await draftsPage.getBadgeCountOnTab()).toBe('1');
|
||||
// # Navigate to the drafts page
|
||||
await draftsPage.goto(team.name);
|
||||
await draftsPage.toBeVisible();
|
||||
|
||||
// * Verify draft message exists
|
||||
const draftedPost = await draftsPage.getLastPost();
|
||||
await expect(draftedPost.panelBody).toContainText(draftMessage);
|
||||
// * Verify draft count badge shows one draft
|
||||
expect(await draftsPage.getBadgeCountOnTab()).toBe('1');
|
||||
|
||||
// 4. Open schedule modal from draft and schedule it to the next 2 days
|
||||
await draftedPost.hover();
|
||||
await draftedPost.scheduleButton.click();
|
||||
await draftsPage.scheduleMessageModal.toBeVisible();
|
||||
const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2);
|
||||
// * Verify draft message content appears correctly
|
||||
const draftedPost = await draftsPage.getLastPost();
|
||||
await expect(draftedPost.panelBody).toContainText(draftMessage);
|
||||
|
||||
// 5. Navigate to scheduled posts page
|
||||
await scheduledPostsPage.goto(team.name);
|
||||
// # Schedule the draft for 2 days in the future
|
||||
await draftedPost.hover();
|
||||
await draftedPost.scheduleButton.click();
|
||||
await draftsPage.scheduleMessageModal.toBeVisible();
|
||||
const {selectedDate, selectedTime} = await draftsPage.scheduleMessageModal.scheduleMessage(2);
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
});
|
||||
// # Navigate to scheduled posts page
|
||||
await scheduledPostsPage.goto(team.name);
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to edit a scheduled message before it's sent.
|
||||
|
|
@ -369,62 +410,70 @@ test('MM-T5648 should create a draft and then schedule it', {tag: '@scheduled_me
|
|||
* @precondition
|
||||
* A test server with valid license to support scheduled message features
|
||||
*/
|
||||
test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
test(
|
||||
'MM-T5644_1 edits scheduled message content while preserving scheduled time',
|
||||
{tag: '@scheduled_messages'},
|
||||
async ({pw}) => {
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user, townSquareUrl} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user, townSquareUrl} = await pw.initSetup();
|
||||
const {channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message with 2 days offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
|
||||
// # Create a scheduled message for 2 days in the future
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
|
||||
|
||||
// * Verify scheduled message indicator appears with correct date/time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
|
||||
// * Verify scheduled post badge in left sidebar shows correct count
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
// * Verify scheduled post badge shows count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
|
||||
// 3. Navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 4. Hover and click edit button
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.editButton.click();
|
||||
// # Edit the scheduled message content
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.editButton.click();
|
||||
const updatedText = 'updated text';
|
||||
await scheduledPost.editTextBox.fill(updatedText);
|
||||
await scheduledPost.saveButton.click();
|
||||
|
||||
// 5. Edit the scheduled message
|
||||
const updatedText = 'updated text';
|
||||
await scheduledPost.editTextBox.fill(updatedText);
|
||||
await scheduledPost.saveButton.click();
|
||||
// * Verify the edited message content is updated
|
||||
await expect(scheduledPost.panelBody).toContainText(updatedText);
|
||||
|
||||
// 6. Verify the edited message appears in the channel
|
||||
await expect(scheduledPost.panelBody).toContainText(updatedText);
|
||||
await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`);
|
||||
// * Verify scheduled date/time remains unchanged
|
||||
await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`);
|
||||
|
||||
// 7. Send the message immediately
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
// # Send the edited message immediately
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.sendNowButton.click();
|
||||
await scheduledPostsPage.sendMessageNowModal.toBeVisible();
|
||||
await scheduledPostsPage.sendMessageNowModal.sendNowButton.click();
|
||||
|
||||
// * Verify it redirects to the channels page, the message has been posted and there's no more scheduled messages
|
||||
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toHaveText(updatedText);
|
||||
});
|
||||
// * Verify page redirects to the channel
|
||||
await expect(channelsPage.page).toHaveURL(townSquareUrl);
|
||||
|
||||
// * Verify scheduled indicators are removed
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeNotVisible();
|
||||
await expect(channelsPage.sidebarLeft.scheduledPostBadge).not.toBeVisible();
|
||||
|
||||
// * Verify edited message was posted in the channel
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.body).toHaveText(updatedText);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify the ability to copy a scheduled message to clipboard.
|
||||
|
|
@ -432,51 +481,55 @@ test('MM-T5644 should edit scheduled message', {tag: '@scheduled_messages'}, asy
|
|||
* @precondition
|
||||
* A test server with valid license to support scheduled message features
|
||||
*/
|
||||
test('MM-T5650 should copy scheduled message', {tag: '@scheduled_messages'}, async ({pw, browserName}) => {
|
||||
// Skip this test in Firefox clipboard permissions are not supported
|
||||
test.skip(browserName === 'firefox', 'Test not supported in Firefox');
|
||||
test(
|
||||
'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');
|
||||
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
const draftMessage = `Scheduled Draft ${pw.random.id()}`;
|
||||
|
||||
// 1. Setup test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
// # Initialize test user, login and navigate to a channel
|
||||
const {user} = await pw.initSetup();
|
||||
const {page, channelsPage, scheduledPostsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Create a scheduled message with 1 day offset
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
// # Create a scheduled message for tomorrow
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled post badge in left sidebar shows correct count
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
// * Verify scheduled post badge shows count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
|
||||
// 3. Navigate to scheduled posts page
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled posts page displays correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
// 4. Copy the scheduled message
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.copyTextButton.click();
|
||||
// # Copy the scheduled message text to clipboard
|
||||
await scheduledPost.hover();
|
||||
await scheduledPost.copyTextButton.click();
|
||||
|
||||
// 5. Return to channel page
|
||||
await page.goBack();
|
||||
// # Return to the channel page
|
||||
await page.goBack();
|
||||
|
||||
// 6. Paste the copied message in post creator
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.down('ControlOrMeta');
|
||||
await page.keyboard.press('V');
|
||||
await page.keyboard.up('ControlOrMeta');
|
||||
// # Paste the copied message into the post input box
|
||||
await channelsPage.centerView.postCreate.input.focus();
|
||||
await page.keyboard.down('ControlOrMeta');
|
||||
await page.keyboard.press('V');
|
||||
await page.keyboard.up('ControlOrMeta');
|
||||
|
||||
// * Verify the copied message is pasted in the post input box
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
|
||||
});
|
||||
// * Verify the clipboard content was pasted correctly
|
||||
await expect(channelsPage.centerView.postCreate.input).toHaveText(draftMessage);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
test('/signup_email', async ({pw, page, browserName, viewport}, testInfo) => {
|
||||
// Set up the page not to redirect to the landing page
|
||||
await pw.hasSeenLandingPage();
|
||||
/**
|
||||
* @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();
|
||||
|
||||
// Go to login page
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
// # Navigate to login page
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
|
||||
// Create an account
|
||||
await pw.loginPage.createAccountLink.click();
|
||||
// # Click on create account link
|
||||
await pw.loginPage.createAccountLink.click();
|
||||
|
||||
// Should have redirected to signup page
|
||||
await pw.signupPage.toBeVisible();
|
||||
// * Verify redirection to signup page
|
||||
await pw.signupPage.toBeVisible();
|
||||
|
||||
// Click to other element to remove focus from email input
|
||||
await pw.signupPage.title.click();
|
||||
// # Remove focus from email input by clicking elsewhere
|
||||
await pw.signupPage.title.click();
|
||||
|
||||
// Match snapshot of signup_email page
|
||||
const testArgs = {page, browserName, viewport};
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
|
||||
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
|
||||
// # Get license information to determine snapshot suffix
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
|
||||
const testArgs = {page, browserName, viewport};
|
||||
|
||||
// Click sign in button without entering user credential
|
||||
const invalidUser = {email: 'invalid', username: 'a', password: 'b'};
|
||||
await pw.signupPage.create(invalidUser, false);
|
||||
await pw.signupPage.emailError.waitFor();
|
||||
await pw.signupPage.usernameError.waitFor();
|
||||
await pw.signupPage.passwordError.waitFor();
|
||||
await pw.waitForAnimationEnd(pw.signupPage.bodyCard);
|
||||
// * Verify visual appearance of signup page
|
||||
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
|
||||
|
||||
// Match snapshot of signup_email page
|
||||
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs);
|
||||
});
|
||||
// # Attempt to create account with invalid credentials
|
||||
const invalidUser = {email: 'invalid', username: 'a', password: 'b'};
|
||||
await pw.signupPage.create(invalidUser, false);
|
||||
|
||||
// * Verify error messages appear for each field
|
||||
await pw.signupPage.emailError.waitFor();
|
||||
await pw.signupPage.usernameError.waitFor();
|
||||
await pw.signupPage.passwordError.waitFor();
|
||||
await pw.waitForAnimationEnd(pw.signupPage.bodyCard);
|
||||
|
||||
// * Verify visual appearance of signup page with errors
|
||||
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 508 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 507 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
Loading…
Reference in a new issue