Files
mattermost/e2e-tests
Nick Misasi 9dfbaeca99 Add weekly recurring scheduled posts (#37746)
* Add weekly recurring scheduled posts.

Extend scheduled posts so users can schedule weekly repeats and keep the series healthy across sends, reschedules, and UI updates instead of falling back to one-shot behavior.

Made-with: Cursor

* Add Playwright coverage for recurring scheduled posts.

Cover weekly recurring scheduled messages in the scheduled-messages spec so the recurring UI and reschedule flow stay protected without adding a separate test surface.

Made-with: Cursor

* Fix recurring scheduled post CI failures.

Resolve the initial lint and formatting issues and renumber the new scheduled-post migration so it no longer collides with master during Postgres-backed test setup.

Made-with: Cursor

* Sync recurring scheduled post translation files.

Regenerate the affected English translation catalogs so the recurring scheduled post strings match the source extraction order expected by CI.

Made-with: Cursor

* Fix recurring scheduled post review follow-ups.

Preserve overdue cleanup behavior during weekly catch-up, defer delete websocket events until deletion succeeds, and address the remaining migration and UI review nits.

Made-with: Cursor

* Address recurring scheduled post review feedback

Made-with: Cursor

* Fix recurring scheduled post CI checks

Made-with: Cursor

* Make pending scheduled post keyset cursor index-scannable

EXPLAIN ANALYZE on a 5M-row ScheduledPosts table showed the pure OR-form
cursor forced Postgres to scan idx_scheduledposts_pending_scheduled_at_id
from the top on every page (~383ms/page, ~2M rows filtered). Keeping the
ScheduledAt <= beforeTime bound outside the tie-break restores the index
boundary (~0.12ms/page). Adds storetest coverage for cursor pagination.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Address code quality review findings for recurring scheduled posts

Server:
- Replace silent-fallback AdvanceWeeklyScheduledNextOccurrence with
  error-returning ScheduledPost.ComputeNextScheduledAt; a recurring post
  whose timezone fails to load is now routed through the failed-post path
  instead of being reposted every job run or silently deleted
- Add ScheduledPost.IsRecurring and partition batches in
  processScheduledPostBatch; advance/delete now run independently so a
  store failure in one path can't cause reposts in the other
- Drop dead generality in UpdateRecurringScheduledPosts (only ScheduledAt
  varies per row; ErrorCode/ProcessedAt are constants)
- Simplify redundant repeat-type condition in GetPendingScheduledPosts

Webapp:
- Add shared isRecurringScheduledPost helper, replacing six scattered
  repeat_type === 'weekly' literals
- Recurrence timezone is now simply the scheduler's current timezone;
  removes initialRepeatTimezone/effectiveTimezone plumbing and the
  modal-label/picker timezone mismatch
- Single enforcement point for hiding send-now on recurring posts
- Use canonical getTeamIdByChannelId at both SCHEDULED_POST_UPDATED
  dispatch sites; rewrite errorsByTeamId update case as remove-then-add
  and add reducer tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Simplify recurring scheduled post code per review

- End a recurring series when its channel no longer exists instead of
  advancing it forever (matches the one-shot channel-not-found handling)
- Collapse the errorsByTeamId SCHEDULED_POST_UPDATED case into the
  identical SINGLE_SCHEDULED_POST_RECEIVED case (a scheduled post's team
  can't change) and combine duplicate byId cases; preserves state
  references on no-op updates
- Drop no-op timezone conversions in ComputeNextScheduledAt
- Remove redundant checkbox aria-label (label htmlFor already names it)
- Schedule new job tests in the past instead of sleeping a real second
- Remove redundant test assignment and e2e positional boolean

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Remove slop from recurring scheduled post changes

- Drop the business-rule CHECK constraint from the recurrence migration;
  no other migration enforces model-layer validation in the database,
  and BaseIsValid plus the job's failure handling already own it
- Fold the standalone repeat-validation test file into the existing
  TestScheduledPostBaseIsValid, matching its conventions
- Revert unrelated benchmark modernization in utils_test.go
- Drop an unneeded cast, unused fixture fields, and naming/assertion
  inconsistencies in tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Regenerate ScheduledPostStore mock with mockery ordering

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Address CodeRabbit review feedback

- Reject the host-dependent 'Local' value for RepeatTimezone; recurring
  schedules need a fixed zone (UTC or IANA name)
- Hide reschedule for deactivated DMs, matching send-now eligibility

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Resolve scheduled post team bucket from existing state when channel is unloaded

An admin can reschedule before fetchMissingChannels resolves, in which case
deriving the team from the channel returns undefined and the update was
misfiled under directChannels. getScheduledPostTeamId falls back to the
byTeamId bucket that already holds the post.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Retrigger CI after transient enterprise npm network failure

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Clear hover/focus before asserting scheduled post header details in e2e

The drafts panel hides its timestamp/tag info section while hovered or
focus-within; after the reschedule modal closes, focus returns to the row
and the 'Repeats weekly' tag assertion saw a hidden element.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Move recurrence columns into baseColumns and dispatch ComputeNextScheduledAt on repeat type

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Preserve recurrence when scheduled post updates omit repeat fields

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Disallow file attachments on recurring scheduled posts

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Stop pinning the channel indicator for recurring-only scheduled posts

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Re-home nullable-Type comment onto baseColumns

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Gate recurring scheduled posts behind a default-off feature flag

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Move presence-preservation rationale to the copy site

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Extract draftHasAttachments helper and require allowRecurring at single-caller layers

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Return null from the indicator selector when nothing should show

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Gate only recurrence transitions and preserve existing series in the modal

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Avoid err shadowing in scheduled post update handler

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Disable the repeat weekly checkbox with a tooltip when the message has attachments

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Return an explicit disposition from postScheduledPost

The batch loop inferred 'channel permanently gone' from the one return
path with a nil error and an error code set - an invariant a future
change could silently break, deleting recurring series by accident.
postScheduledPost now returns posted/failed/unsendable explicitly, the
switch refuses to delete on an unhandled disposition, and a test pins
that both recurring and one-shot posts in a nonexistent channel are
permanently deleted.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Fix Repeat weekly attachments tooltip centering on the modal row (#37927)

Shrink-wrap the repeat checkbox row so WithTooltip anchors to the
control/label instead of the full modal body width.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-13 07:27:23 -04:00
..

E2E testing for the Mattermost web client

This directory contains the E2E testing code for the Mattermost web client.

How to run locally

For test case development

Please refer to the dedicated developer documentation for instructions.

Playwright note: the instructions below describe the Docker Compose flow Cypress uses (and that Playwright previously used too). Playwright's CI and recommended local setup have since moved to Testcontainers — see playwright/README.md's Server Setup section for details. This Compose flow remains the way to run Cypress, and to run a plain server instance (TEST=none make).

For pipeline debugging

The E2E testing pipeline's scripts depend on the following tools being installed on your system: docker, docker-compose, make, git, jq, node, and some common utilities (coreutils, findutils, bash, awk, sed, grep)

Instructions, tl;dr: create a local branch with your E2E test changes, then open a PR to the mattermost-server repo targeting the master branch (so that CI will produce the image that docker-compose needs), then run make in this directory.

Instructions, detailed:

  1. (optional, undefined variables are set to sane defaults) Create the .ci/env file, and populate it with the variables you need out of the following list:
  • SERVER: either onprem (default) or cloud.
  • CWS_URL (mandatory when SERVER=cloud, only used in such case): when spinning up a cloud-like test server that communicates with a test instance of a customer web server.
  • TEST: either cypress (default), playwright, or none (to avoid creating the cypress/playwright sidecar containers, e.g. if you only want to launch a server instance)
  • ENABLED_DOCKER_SERVICES: a space-separated list of services to start alongside the server. Default to postgres inbucket, for smoke test purposes and for lightweight and faster start-up time. Depending on the test requirement being worked on, you may want to override as needed, as such:
    • Cypress full tests require all services to be running: postgres inbucket minio openldap elasticsearch keycloak.
    • Cypress smoke tests require only the following: postgres inbucket.
    • Playwright no longer runs against this Compose flow in CI (see the note above) — this only matters if you're using it to spin up a plain server instance (TEST=none make) for Playwright's external mode.
  • The following variables, will be passed over to the server container: MM_LICENSE (no enterprise features will be available if this is unset; required when SERVER=cloud), and the exploded MM_ENV (a comma-separated list of env var specifications)
  • The following variables, which will be passed over to the cypress container: BRANCH, BUILD_ID, CI_BASE_URL, BROWSER, AUTOMATION_DASHBOARD_URL and AUTOMATION_DASHBOARD_TOKEN
  • The SERVER_IMAGE variable can also be set if you want to select a custom mattermost-server image. If not specified, the value of the SERVER_IMAGE_DEFAULT variable defined in file .ci/.e2erc is used.
  • The TEST_FILTER variable can also be set, to customize which tests you want Cypress/Playwright to run. If not specified, only the smoke tests will run
    • Its format depends on which tool is used: for Cypress, please check the e2e-tests/cypress/run_tests.js file for details. For Playwright, it can simply be populated with arguments you want to give to the playwright test command.
  • More variables may be required to configure reporting and cloud interactions. Check the content of the .ci/report.*.sh and .ci/server.cloud_*.sh scripts for reference.
  1. (optional) make start-dashboard && make generate-test-cycle: start the automation dashboard in the background, and initiate a test cycle on it, for the given BUILD_ID
  • NB: the BUILD_ID value should stay the same across the make generate-test-cycle command, and the subsequent make (see next step). If you need to initiate a new test cycle on the same dashboard, you'll need to change the BUILD_ID value and rerun both make generate-test-cycle and make.
  • Note that part of the dashboard functionality assumes the BUILD_ID to have a certain format (see here for details). This is not relevant for local running, but it's important to note in the testing pipelines.
  • This also automatically sets the AUTOMATION_DASHBOARD_URL and AUTOMATION_DASHBOARD_TOKEN variables for the cypress container
  • Note that if you run the dashboard locally, but also specify other AUTOMATION_DASHBOARD_* variables in your .ci/env file, the latter variables will take precedence.
  • The dashboard is used for orchestrating specs with parallel test runs and is typically used in CI.
  • Only Cypress is currently using the dashboard; Playwright is not.
  1. make: start and prepare the server, then run the Cypress smoke tests
  • You can track the progress of the run in the http://localhost:4000/cycles dashboard if you launched it locally
  • For SERVER=cloud runs, you'll need to first create a cloud customer against the specified CWS_URL service by running make cloud-init. The user isn't automatically removed, and may be reused across multiple runs until you run make cloud-teardown to delete it.
  • If you want to run the Playwright tests instead of the Cypress ones, you can run TEST=playwright make — though playwright/README.md's Testcontainers option is now the recommended way to run Playwright locally/in CI
  • If you just want to run a local server instance, without any further testing, you can run TEST=none make
  • If you're using the automation dashboard, you have the option of sharding the E2E test run: you can launch the make command in parallel on different machines (NB: you must use the same BUILD_ID and BRANCH values that you used for make generate-test-cycle) to distribute running the test cases across them. When doing this, you should also set on each machine the CI_BASE_URL variable to a value that uniquely identifies the instance where make is running.
  • This script will also parse the local test results, and write a e2e-tests/${TEST}/results/summary.json file containing the following keys: passed, failed and failed_expected (the total number of testcases that were run is the sum of these three numbers)
  1. make stop: tears down the server (and the dashboard, if running)
  • This will stop and cleanup all of the E2E testing containers, including the database and its persistent volume.
  • This also implicitly runs make clean, which also removes any generated environment or docker-compose files.

Notes:

  • Setting a variable in .ci/env is functionally equivalent to exporting variables in your current shell's environment, before invoking the makefile.
  • The .ci/.env.* files are auto-generated by the pipeline scripts and aren't meant to be modified manually. The only file you should edit to control the containers' environment is .ci/env, as specified in the instructions above.
  • All of the variables in .ci/env must be set before the make generate-server command is run (or, if using the dashboard, before the make generate-test-cycle command). Modifying that file afterward has no effect because the containers' env files are generated in that step.
  • If you restart the dashboard at any point, you must also restart the server containers, so that it picks up the new IP of the dashboard from the newly generated .env.dashboard file
  • If new variables need to be passed to any of the containers, here are the general principles to follow when deciding where to populate it:
    • If their value is fixed (e.g. a static server configuration), these may be simply added to the docker_compose_generator.sh file, to the appropriate container.
    • If you need to introduce variables that you want to control from .ci/env: you need to update the scripts under the .ci/ dir and configure them to write the new variables' values over to the appropriate .env.* file. In particular, avoid defining variables that depend on other variables within the docker-compose override files: this is to ensure uniformity in their availability and simplifies the question of what container has access to which variable considerably.
    • Exceptions are of course accepted wherever it makes sense (e.g. if you need to group variables based on some common functionality)
  • The report Make target is meant for internal usage. Usage and variables are documented in the respective scripts.
  • make start-server won't cleanup containers that don't change across runs. This means that you can use it to emulate a Mattermost server upgrade while retaining your database data by simply changing the SERVER_IMAGE variable on your machine, and then re-running make start-server. But this also means that if you want to run a clean local environment, you may have to manually run make stop to cleanup any running containers and their volumes, which include e.g. the database.
For code changes:
  • make fmt-ci to format and check yaml files and shell scripts.
For test stressing an E2E testcase

For Cypress:

  1. Enter the cypress/ subdirectory
  2. Identify which test files you want to run, and how many times each. For instance: suppose you want to run create_a_team_spec.js and demoted_user_spec.js (which you can locate with the find command, under cypress/tests/), each run 3 times
  3. Run the chosen testcases the desired amount of times: node run_tests.js --include-file=create_a_team_spec.js,demoted_user_spec.js --invert --stress-test-count=3
  1. The cypress/results/testPasses.json file will count, for each of the testfiles, how many times it was run, and how many times each of the testcases contained in it passed. If the attempts and passes numbers do not match, that specific testcase may be flaky.

For Playwright: not currently supported — Playwright tests are run and re-run individually via npm run test -- <spec>, see playwright/README.md.