diff --git a/.gitignore b/.gitignore index 78131290bb3..87cc7c3055d 100644 --- a/.gitignore +++ b/.gitignore @@ -162,5 +162,5 @@ docker-compose.override.yaml **/CLAUDE.local.md -CLAUDE.md +./CLAUDE.md diff --git a/e2e-tests/playwright/CLAUDE.md b/e2e-tests/playwright/CLAUDE.md new file mode 100644 index 00000000000..00466635bc2 --- /dev/null +++ b/e2e-tests/playwright/CLAUDE.md @@ -0,0 +1,242 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Purpose + +This is the Playwright E2E testing suite for Mattermost. It contains end-to-end tests for validating the Mattermost web application functionality using the Playwright testing framework. + +## Key Commands + +### Installation + +```bash +# Install npm packages +npm i + +# Install browser binaries (if prompted) +npx playwright install +``` + +### Running Tests + +```bash +# Run a specific test across all browsers (Chrome, Firefox, iPad) +npm run test -- + +# Run a specific test for a specific browser +npm run test -- --project=chrome +npm run test -- --project=firefox +npm run test -- --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` diff --git a/e2e-tests/playwright/package-lock.json b/e2e-tests/playwright/package-lock.json index fb14bcaff57..0ac9b5a5b12 100644 --- a/e2e-tests/playwright/package-lock.json +++ b/e2e-tests/playwright/package-lock.json @@ -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", diff --git a/e2e-tests/playwright/package.json b/e2e-tests/playwright/package.json index 711d76afb9a..27f40e902a6 100644 --- a/e2e-tests/playwright/package.json +++ b/e2e-tests/playwright/package.json @@ -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" } diff --git a/e2e-tests/playwright/script/README.md b/e2e-tests/playwright/script/README.md new file mode 100644 index 00000000000..347498e3f24 --- /dev/null +++ b/e2e-tests/playwright/script/README.md @@ -0,0 +1,62 @@ +# Playwright E2E Test Scripts + +This directory contains utility scripts for the Playwright E2E test suite. + +## Test Documentation Format Linter + +The `lint-test-docs.js` script verifies that all spec files follow the required documentation format: + +- JSDoc with `@objective` and `@precondition` tags +- Proper test title with MM-T ID (e.g., MM-T1234) +- Tag for feature categorization (e.g., `{tag: '@feature_name'}`) +- Action comments (e.g., `// # Action`) +- Verification comments (e.g., `// * Verification`) + +### Usage + +```bash +# Run the linter directly +node script/lint-test-docs.js + +# Or use the npm script +npm run lint:test-docs +``` + +### Integration with CI + +The linter is also integrated with the main `check` command, which is typically run before committing changes: + +```bash +npm run check +``` + +This will run ESLint, Prettier, TypeScript type checking, and the test documentation format linter. + +### Requirements + +All spec files should follow this format: + +```typescript +/** + * @objective Clear description of what the test verifies + * + * @precondition + * Special setup or conditions required for the test + * Note: Only include for non-default requirements + */ +test('descriptive test title', {tag: '@feature_tag'}, async ({pw}) => { + // # Initialize setup and login + const {user} = await pw.initSetup(); + const {channelsPage} = await pw.testBrowser.login(user); + + // # Navigate to channel and post a message + await channelsPage.goto(); + await channelsPage.postMessage('Test message'); + + // * Verify message appears in the channel + const lastPost = await channelsPage.getLastPost(); + await expect(lastPost.body).toContainText('Test message'); +}); +``` + +This ensures consistency across all test files and makes it easier to understand the purpose and requirements of each test. diff --git a/e2e-tests/playwright/script/lint-test-docs.js b/e2e-tests/playwright/script/lint-test-docs.js new file mode 100755 index 00000000000..ed4c95ee526 --- /dev/null +++ b/e2e-tests/playwright/script/lint-test-docs.js @@ -0,0 +1,386 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/** + * Test Documentation Format Linter + * + * This script verifies that all spec files follow the required documentation format: + * - JSDoc with @objective and @precondition + * - Proper test title with MM-T ID + * - Tag for feature categorization + * - Action/Verification comments + */ + +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable import/order */ + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +/* eslint-disable no-console */ + +// Colors for terminal output +const colors = { + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + reset: '\x1b[0m', + cyan: '\x1b[36m', + magenta: '\x1b[35m', + white: '\x1b[37m', +}; + +// Regex patterns +const patterns = { + // Support both single-line and multi-line test declarations + testDeclaration: + /test\(\s*['"]([^'"]+)['"]\s*,(?:\s*|\n\s*){.*?tag:\s*['"]@.*?['"].*?}(?:\s*|\n\s*),(?:\s*|\n\s*)async/g, + jsdocBlock: /\/\*\*\s*\n(?:.*\n)*?\s*\*\//g, + objective: /@objective\s+.*?(?=\n|\*\/)/g, + precondition: /@precondition\s+.*?(?=\n|\*\/)/g, + tagDeclaration: /{tag:\s*['"]@[\w_]+['"]}|{\s*tag:\s*['"]@[\w_]+['"]\s*}/g, + // Match action comments - note the difference between # as a comment marker vs # in strings + actionComment: /(?:^|\n)\s*\/\/\s*#\s*.+/g, + // Match verification comments - note the difference between * as a comment marker vs * in strings + verificationComment: /(?:^|\n)\s*\/\/\s*\*\s*.+/g, +}; + +// Get all spec files +const specFiles = glob.sync(path.join(process.cwd(), 'specs/**/*.spec.ts')); + +// Results +const results = { + passed: 0, + failed: 0, + warnings: 0, + errors: [], +}; + +// Process each file +specFiles.forEach((filePath) => { + const relativeFilePath = path.relative(process.cwd(), filePath); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const fileErrors = []; + + // Extract all test declarations + const testDeclarations = Array.from(fileContent.matchAll(patterns.testDeclaration)); + const jsdocBlocks = Array.from(fileContent.matchAll(patterns.jsdocBlock)); + + // If no test declarations found, warn but don't fail + if (testDeclarations.length === 0) { + results.warnings++; + console.log( + `${colors.yellow}WARNING${colors.reset}: No test declarations found in ${colors.cyan}${relativeFilePath}${colors.reset}`, + ); + return; + } + + // Check each test for proper format + testDeclarations.forEach((testMatch) => { + const testDeclaration = testMatch[0]; + const testName = testMatch[1]; + + // Get test function position to find corresponding JSDoc + const testPosition = testMatch.index; + + // Get the JSDoc block that immediately precedes this test + const precedingJSDoc = jsdocBlocks.find((jsdoc) => { + const jsdocEnd = jsdoc.index + jsdoc[0].length; + // JSDoc should end right before the test with only whitespace in between + const textBetween = fileContent.substring(jsdocEnd, testPosition).trim(); + return textBetween === ''; + }); + + // Check if test has a test key (MM-T####) + if (!testName.match(/^MM-T\d+/)) { + // This is a new test without a test key + console.log( + `${colors.cyan}NEW TEST FOUND: "${testName}"${colors.reset} - Will be registered in the test management system after merge`, + ); + results.newTests = results.newTests || []; + results.newTests.push({file: relativeFilePath, testName}); + } else { + // This is an existing test with a test key + const baseTestKey = testName.match(/^(MM-T\d+)/)[1]; + + // Check if this is a step of a multi-step test case + const stepMatch = testName.match(/^MM-T\d+(_\d+)/); + const isStep = stepMatch !== null; + const stepSuffix = isStep ? stepMatch[1] : ''; + const testKey = baseTestKey + (stepSuffix || ''); + + // Log different message for test steps + if (isStep) { + // Extract the step number without the underscore + const stepNumber = stepSuffix.substring(1); + console.log( + `${colors.magenta}DOCUMENTATION UPDATE: "${testKey}"${colors.reset} - step ${stepNumber} of ${baseTestKey} - Changes will be mapped to test management system after merge`, + ); + } else { + console.log( + `${colors.magenta}DOCUMENTATION UPDATE: "${testKey}"${colors.reset} - Changes will be saved to test management system after merge`, + ); + } + + // Check if documentation is present and mark for update + if (precedingJSDoc) { + results.updatedTests = results.updatedTests || []; + results.updatedTests.push({ + file: relativeFilePath, + testKey, + baseTestKey, + isStep, + stepSuffix, + testName, + }); + } + } + + // JSDoc was already retrieved above, no need to get it again + + if (!precedingJSDoc) { + fileErrors.push(`Missing JSDoc documentation at "${testName}"`); + } else { + const jsdocContent = precedingJSDoc[0]; + + // Check for @objective + if (!jsdocContent.match(patterns.objective)) { + fileErrors.push(`Missing @objective in JSDoc at "${testName}"`); + } + + // Note: @precondition is optional and should only be included when there are + // non-default requirements for the test + } + + // Check for tag declaration + if (!testDeclaration.match(patterns.tagDeclaration)) { + fileErrors.push(`Missing feature tag at "${testName}"`); + } + + // Simpler approach to extract test body by using string search and tracking braces to find function boundaries + const testNameEscaped = testName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const testDeclarationPattern = new RegExp(`test\\s*\\([\\s\\n]*['"]${testNameEscaped}['"]`); + + // Find the position of the test declaration + const testDeclarationMatch = testDeclarationPattern.exec(fileContent); + let testFnMatch = null; + + if (testDeclarationMatch) { + // Find the position of "async" after the test declaration + const startPos = testDeclarationMatch.index; + const asyncPattern = /async\s*\([^)]*\)\s*=>\s*{/g; + asyncPattern.lastIndex = startPos; + + const asyncMatch = asyncPattern.exec(fileContent); + if (asyncMatch) { + // Find the position of the opening brace of the function body + const openBracePos = asyncMatch.index + asyncMatch[0].length - 1; + + // Find the matching closing brace + let braceCount = 1; + let currentPos = openBracePos + 1; + + while (braceCount > 0 && currentPos < fileContent.length) { + const char = fileContent[currentPos]; + + if (char === '{') { + braceCount++; + } else if (char === '}') { + braceCount--; + } + + currentPos++; + } + + // If we found the matching closing brace, extract everything between them + if (braceCount === 0) { + const testBody = fileContent.substring(openBracePos + 1, currentPos - 1); + testFnMatch = [null, testBody]; + } + } + + // If we couldn't extract using the above method, try a more direct string search approach + if (!testFnMatch) { + // Find the test function by searching for the test name and extracting all content until the end of the function + const testIndex = + fileContent.indexOf(`test('${testName}'`) || + fileContent.indexOf(`test("${testName}"`) || + fileContent.indexOf(`test(\n '${testName}'`) || + fileContent.indexOf(`test(\n "${testName}"`); + + if (testIndex !== -1) { + // Find the position of "async" which indicates the start of the function body + const asyncIndex = fileContent.indexOf('async', testIndex); + if (asyncIndex !== -1) { + // Find the opening brace after "async" + const openBraceIndex = fileContent.indexOf('{', asyncIndex); + if (openBraceIndex !== -1) { + // Count braces to find the matching closing brace + let braceCount = 1; + let closePos = openBraceIndex + 1; + + while (braceCount > 0 && closePos < fileContent.length) { + const char = fileContent[closePos]; + + if (char === '{') { + braceCount++; + } else if (char === '}') { + braceCount--; + } + + closePos++; + } + + if (braceCount === 0) { + const testBody = fileContent.substring(openBraceIndex + 1, closePos - 1); + testFnMatch = [null, testBody]; + } + } + } + } + } + } + + if (testFnMatch) { + const testBody = testFnMatch[1]; + + // Check for action comments (// #) - we need to be more flexible in our detection + let hasActionComments = false; + const actionPattern = /\/\/\s*#/; + + if (actionPattern.test(testBody)) { + hasActionComments = true; + } + + if (!hasActionComments) { + fileErrors.push(`Missing action comments at "${testName}" (format: "// # Some descriptive action")`); + } + + // Check for verification comments (// *) - we need to be more flexible in our detection + let hasVerificationComments = false; + const verificationPattern = /\/\/\s*\*/; + + if (verificationPattern.test(testBody)) { + hasVerificationComments = true; + } + + if (!hasVerificationComments) { + fileErrors.push( + `Missing verification comments at "${testName}" (format: "// * Some descriptive verification")`, + ); + } + } else { + fileErrors.push(`Could not extract test body for "${testName}"`); + } + }); + + if (fileErrors.length > 0) { + results.failed++; + results.errors.push({ + file: relativeFilePath, + errors: fileErrors, + }); + } else { + results.passed++; + } +}); + +// Print results +console.log('\n' + '-'.repeat(80)); +console.log(`${colors.cyan}Test Documentation Format Linter Results${colors.reset}`); +console.log(`Files checked: ${specFiles.length}`); +console.log(`${colors.green}Passed: ${results.passed}${colors.reset}`); +console.log(`${colors.red}Failed: ${results.failed}${colors.reset}`); +console.log(`${colors.yellow}Warnings: ${results.warnings}${colors.reset}`); +console.log(`${colors.cyan}New tests: ${results.newTests ? results.newTests.length : 0}${colors.reset}`); +console.log(`${colors.magenta}Updated tests: ${results.updatedTests ? results.updatedTests.length : 0}${colors.reset}`); + +if (results.errors.length > 0) { + console.log('\n' + '-'.repeat(80)); + console.log(`${colors.red}Errors:${colors.reset}`); + + results.errors.forEach((fileResult) => { + console.log(`\n${colors.cyan}${fileResult.file}${colors.reset}:`); + fileResult.errors.forEach((error) => { + console.log(` ${colors.red}•${colors.reset} ${error}`); + }); + }); +} + +// Display new tests summary if any were found +if (results.newTests && results.newTests.length > 0) { + console.log('\n' + '-'.repeat(80)); + console.log(`${colors.cyan}New Tests to be Registered:${colors.reset}`); + + results.newTests.forEach((newTest) => { + console.log(` ${colors.cyan}•${colors.reset} ${newTest.file}: "${newTest.testName}"`); + }); +} + +// Display updated tests summary if any were found +if (results.updatedTests && results.updatedTests.length > 0) { + console.log('\n' + '-'.repeat(80)); + console.log(`${colors.magenta}Tests with Documentation Updates:${colors.reset}`); + + // Group the updated tests by base test key + const groupedTests = {}; + results.updatedTests.forEach((test) => { + if (!groupedTests[test.baseTestKey]) { + groupedTests[test.baseTestKey] = []; + } + groupedTests[test.baseTestKey].push(test); + }); + + // Display the tests grouped by base test key + Object.keys(groupedTests).forEach((baseTestKey) => { + const tests = groupedTests[baseTestKey]; + + // If there's only one test with this base key and it's not a step + if (tests.length === 1 && !tests[0].isStep) { + const test = tests[0]; + console.log( + ` ${colors.magenta}•${colors.reset} ${test.file}: ${test.testKey} "${test.testName.substring(test.testKey.length).trim()}"`, + ); + } + // If there are multiple steps of the same test + else { + console.log( + ` ${colors.magenta}•${colors.reset} ${tests[0].file}: ${colors.cyan}${baseTestKey}${colors.reset} (with ${tests.length} steps):`, + ); + + // Sort steps by their step number + tests.sort((a, b) => { + if (!a.isStep) return -1; + if (!b.isStep) return 1; + + const aNum = parseInt(a.stepSuffix.substring(1), 10); + const bNum = parseInt(b.stepSuffix.substring(1), 10); + return aNum - bNum; + }); + + // Display each step + tests.forEach((test) => { + const namePart = test.testName.substring(test.baseTestKey.length).trim(); + if (test.isStep) { + // Extract the step number without the underscore + const stepNumber = test.stepSuffix.substring(1); + console.log( + ` ${colors.magenta}◦${colors.reset} step ${stepNumber}: "${namePart.substring(test.stepSuffix.length).trim()}"`, + ); + } else { + console.log(` ${colors.magenta}◦${colors.reset} Base test: "${namePart}"`); + } + }); + } + }); +} + +console.log('\n' + '-'.repeat(80)); +if (results.failed > 0) { + console.log(`${colors.red}Linter failed!${colors.reset} Please fix the test documentation issues.`); + process.exit(1); +} else { + console.log(`${colors.green}Linter passed!${colors.reset} All spec files follow the required format.`); + process.exit(0); +} diff --git a/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts b/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts index 2dfd2bbd82e..9483d8d8935 100644 --- a/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/gif_picker/gif_picker_search.spec.ts @@ -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); diff --git a/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts b/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts index 9e4d58ae8bf..8b98b939751 100644 --- a/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/mentions/multiple_mentions.spec.ts @@ -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}`; diff --git a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts index a2c6e768175..bf8c5c1803b 100644 --- a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts @@ -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(); + }, +); diff --git a/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts b/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts index e7e7492c0ca..989c537f561 100644 --- a/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/notifications/notification.spec.ts @@ -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(); + }, +); diff --git a/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts b/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts index 18faec86927..7a6595e9618 100644 --- a/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/scheduled_messages/scheduled_messages.spec.ts @@ -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. diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts index 066b1050933..d88aadc3e1e 100644 --- a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts +++ b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts @@ -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); + }, +); diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-chrome-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-chrome-linux.png new file mode 100644 index 00000000000..1ca838edae8 Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-chrome-linux.png differ diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-chrome-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-chrome-linux.png new file mode 100644 index 00000000000..871eb111b34 Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-chrome-linux.png differ diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-firefox-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-firefox-linux.png new file mode 100644 index 00000000000..25c1c7b1306 Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-firefox-linux.png differ diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-ipad-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-ipad-linux.png new file mode 100644 index 00000000000..4e9684537a5 Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-error-ipad-linux.png differ diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-firefox-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-firefox-linux.png new file mode 100644 index 00000000000..ef845a5961d Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-firefox-linux.png differ diff --git a/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-ipad-linux.png b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-ipad-linux.png new file mode 100644 index 00000000000..bded51722b7 Binary files /dev/null and b/e2e-tests/playwright/specs/visual/common/signup_email.spec.ts-snapshots/signup-email-visual-verification-ipad-linux.png differ