* 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>
Local development
1. Start local server in a separate terminal.
There are two ways to run the local server:
Option 1: Run from source
# Typically run the local server with:
cd server && make run
# Or run webapp and server on separate terminals for better performance
# First terminal: Build and run the webapp
cd webapp && make run
# Second terminal: Run the server
cd server && make run-server
Option 2: Testcontainers (recommended for testing, and what CI uses)
No separate terminal or setup step needed — Playwright brings up Postgres, Inbucket, and the Mattermost server itself via Testcontainers, then tears them down after the run.
# Run with defaults (Postgres, Inbucket, Mattermost server, minio, openldap, keycloak, elasticsearch)
PW_USE_TESTCONTAINERS=true npm run test -- login
# Change which additional services start, comma-separated (or "" to start none)
PW_USE_TESTCONTAINERS=true PW_TESTCONTAINERS_SERVICES=minio,openldap npm run test
# Pin a specific server image (defaults to mattermostdevelopment/mattermost-enterprise-edition:master)
PW_USE_TESTCONTAINERS=true SERVER_IMAGE=mattermostdevelopment/mattermost-enterprise-edition:<tag> npm run test
# Pass arbitrary MM_* config overrides as comma-separated KEY=VALUE pairs
PW_USE_TESTCONTAINERS=true MM_ENV=MM_LICENSE=<your-license-key> npm run test
Containers are reused across invocations by default (PW_TESTCONTAINERS_REUSE=true) instead of being recreated every run — tear the stack down explicitly when you're done with npm run testcontainers:down. Set PW_TESTCONTAINERS_REUSE=false for a one-off run that tears itself down when it finishes. Use npm run testcontainers:up to just bring the stack up (or confirm an existing one's still reachable) without running any tests.
See lib/README.md for every available environment variable.
2. Install dependencies and run the test.
# Install npm packages
npm i
# Install browser binaries as prompted if Playwright is just installed or updated
# See https://playwright.dev/docs/browsers
npx playwright install
# Run a specific test of all projects -- Chrome, Firefox, iPhone and iPad.
# See https://playwright.dev/docs/test-cli.
npm run test -- login
# Run a specific test of a project
npm run test -- login --project=chrome
# Run all tests (including visual tests)
npm run test
# Run CI tests (excludes visual tests, runs only in Chrome)
# Note: visual tests run in a separate workflow
npm run test:ci
3. Inspect test results at /results/output folder when something fails unexpectedly.
Run tests in UI mode
Check out https://playwright.dev/docs/test-ui-mode for detailed guide on UI Mode to learn more about its features.
npm run playwright-ui
Note: If no tests appear in the UI, check your filter settings:
- Test name filters
- Project filters (setup, ipad, chrome, firefox)
- Tag filters (@tag)
- Execution status filters
The "setup" project runs the initial configuration tests in
specs/test_setup.ts(ensuring plugins are loaded and server deployment is correct). These setup tests are typically run only once before other tests and may be unchecked for subsequent runs, though they can remain checked if needed.
Visual Testing
All visual tests must be placed in the specs/visual/ directory and tagged with @visual in the test tags array. This organization ensures proper test discovery and execution patterns.
Visual tests are used to verify the UI appearance is consistent across browsers and remains stable across code changes. There are two types of visual tests supported:
- Built-in snapshot testing: Uses Playwright's built-in snapshot comparison
- Percy integration: Uses the Percy service for more advanced visual testing and reporting
CI Pipeline for Visual Tests
In CI environments, visual tests run in a separate dedicated pipeline:
- Regular tests run with
npm run test:ciwhich excludes all tests with the@visualtag - Visual tests run in a separate workflow using the Playwright Docker container
- This separation prevents visual tests from slowing down the main test pipeline
- It also ensures visual tests always run in a consistent environment
Writing Visual Tests
When creating visual tests:
-
Follow the test documentation format like other tests:
- Include JSDoc with
@objectivetag - Use action-oriented test title
- Add proper comment prefixes (
// #for actions,// *for verifications)
- Include JSDoc with
-
Place in the correct location:
- Put visual tests in the
specs/visual/directory, organized by feature area - Example:
specs/visual/channels/intro_channel.spec.ts
- Put visual tests in the
-
Add required tags:
- Always include
@visualtag - Add feature-specific tags as needed (e.g.,
@login_page,@channel_page)
- Always include
-
Manage dynamic content:
- Use
pw.hideDynamicChannelsContent()to hide elements that could change between runs - Take snapshots only after UI is fully loaded and stable
- Use
Example:
/**
* @objective Capture visual snapshot of the landing/login page
*/
test(
'displays landing page with login options',
{tag: ['@visual', '@landing_page']},
async ({pw, page, browserName, viewport}, testInfo) => {
// # Go to landing login page
await pw.landingLoginPage.goto();
await pw.landingLoginPage.toBeVisible();
// * Verify landing page appears as expected
await pw.matchSnapshot(testInfo, {page, browserName, viewport});
},
);
Updating screenshots is done strictly via Playwright's docker container for consistency
1. Run Playwright's docker container
Change to the ./ project directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.61.0-noble /bin/bash
2. Inside the docker container
export PW_BASE_URL=http://host.docker.internal:8065
export PW_HEADLESS=true
cd mattermost/e2e-tests/playwright
# Install npm packages. Use "npm ci" to match the automated environment
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci
# Run specific test. See https://playwright.dev/docs/test-cli.
npm run test -- login --project=chrome
# Or run all tests
npm run test
# Run visual tests (must be run inside Docker for consistency)
npm run test -- specs/visual
# Update snapshots of visual tests (must be run inside Docker)
npm run test -- specs/visual --update-snapshots
# Run Percy visual tests (requires PERCY_TOKEN environment variable)
export PERCY_TOKEN=<your-percy-token>
npm run percy:docker
Accessibility Testing
Accessibility tests ensure Mattermost meets WCAG 2.1 AA compliance standards. Tests are located in specs/accessibility/ and cover keyboard navigation, screen reader support, focus management, and automated accessibility scanning.
For comprehensive guidelines on writing accessibility tests, aria snapshots, and folder structure, see docs/accessibility/.
Accessibility Locators
Playwright's accessibility locators should be the preferred approach for all tests, not just accessibility tests. These locators query elements based on how users and assistive technologies perceive them, making tests more resilient to implementation changes and ensuring better accessibility by design.
Why Use Accessibility Locators?
- Resilient to changes: Tests won't break when CSS classes or data-testid attributes change
- Encourages accessibility: Forces proper ARIA roles, labels, and semantic HTML
- Better readability:
page.getByRole('button', {name: 'Save'})is clearer thanpage.locator('[data-testid="save-btn"]') - Aligns with user experience: Tests what users actually perceive, not implementation details
Preferred Locators (in order of preference)
- Role-based:
page.getByRole('button', {name: 'Save'}),page.getByRole('textbox', {name: 'Email'}) - Label-based:
page.getByLabel('Email address') - Text-based:
page.getByText('Welcome'),page.getByPlaceholder('Enter email') - Test IDs:
page.locator('[data-testid="..."]')- Use only when accessibility locators aren't possible - CSS selectors:
page.locator('.class')- Avoid unless absolutely necessary
When Test IDs Are Acceptable
Use data-testid only when:
- Element has no semantic role (e.g., decorative divs)
- Multiple identical elements need distinction
- Component is not interactive or visible to assistive tech
For all test examples, see docs/accessibility/ for comprehensive patterns and best practices.
Page/Component Object Model
See https://playwright.dev/docs/test-pom.
Page and component abstractions are in shared library located at ./lib/src/ui. They should be established before writing a spec file so that any future changes in the DOM structure will be made in one place only. No static UI text or fixed locator should be written in the spec file.