Files
upstream-rocketchat/apps/meteor/tests/e2e/README.md
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

411 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# E2E Testing with playwright
## Running tests
The application must be started with `TEST_MODE=true`
```sh
$ TEST_MODE=true yarn dev
```
Then we can run a single suite with
```sh
$ yarn test:e2e ./tests/e2e/administration.spec.ts
```
Or all the tests with
```sh
$ yarn test:e2e
```
We can also provide some env vars to `test:e2e` script:
- `BASE_URL=<any_url>` Run the tests to the given url
- `PWDEBUG=1` Control the test execution
## Page Objects
- Any locator name must start with of one the following prefixes: `btn`, `link`, `input`, `select`, `checkbox`, `text`
## Important links
- [playwright docs](https://playwright.dev/docs/intro)
## Assertions and waiting
- Prefer Playwright web-first assertions such as `toBeVisible()` and `toHaveText()`. They retry until the expected condition is met.
- Use Playwright's `expect` matchers instead of Node-style `assert` statements.
- Rely on Playwright's built-in auto-waiting whenever possible.
- When an explicit wait is necessary, wait for a specific condition with APIs such as `locator.waitFor()`, `page.waitForURL()`, or `page.waitForResponse()`.
- Prefer waiting for observable conditions over fixed delays. If a fixed delay is unavoidable, document why no observable condition can be awaited.
```ts
await expect(anyElement).toBeVisible();
```
# Playwright Locator Best Practices
## Preferred locator types:
### 1. By role `.getByRole()`:
This is the most recommended locator type.
- It ensures our element is accessible to screen readers and other assistive technologies.
- It ensures that the element is uniquely identifiable.
- It's recommended to use the `exact` option to ensure that the locator doesn't match any other element with the same role
```ts
await page.getByRole('button', { name: 'Save', exact: true });
// Without the `exact` option, Playwright would match buttons with 'Save' and 'Save changes' labels.
```
### 2. By text `.getByText()` or `.getByLabel()`:
- Use this when locating by role is not possible or not sufficient.
- Be sure to restrict the scope of the locator to the element you are looking for.
### 3. Locator with `has`:
Our input elements hide the native input and render a custom component.
To target the input and trigger changes, you should locate the label that wraps the input and use the `has` locator.
```ts
page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Private' }) });
```
## Locator types to avoid (at all costs):
If you are not able to find the element by role, label or text, it's a sign that the element is not accessible to the user.
The component should be refactored to allow a more accessible locator.
### 1. By `data-qa-id` or testId `.getByTestId()`:
```ts
// DON'T ❌
page.locator('[data-qa-id="menu-more-actions"]');
// DO ✅
page.getByRole('menu', {name: 'More actions', exact: true });
```
### 2. By elements and class names `.locator('div.class-name')`:
The HTML structure and/or class names can easily change over time, so it's better to use the locator by role and name.
```ts
// DON'T ❌
page.locator('#modal-root .rcx-button-group--align-end .rcx-button--primary');
// #modal-root was used to locate the injected portal element for the modal.
// This can be better located by role 'dialog' and making sure the modal has the proper name attribute.
// DO ✅
page.getByRole('dialog', name: 'Modal name example').getByRole('button', { name: 'Confirm', exact: true });
```
### 3. By position:
Using `nth-child` or similar selectors is not recommended as it can easily change.
It's a fragile locator and it's hard to maintain.
### 4. Parent/Child relationships:
A change in the DOM structure can break the test.
## Use our page-objects:
> apps/meteor/tests/e2e/page-objects
- page-objects are a great way to reuse locators across tests using `getters` and `methods`.
- They make it easier to write tests that are more readable and maintainable.
- Always make sure to use the most restricted scope possible - not the whole page to avoid multiple matches.
- Page objects own navigation - see [Navigation](#navigation-goto-owns-the-wait).
If you are writing a new test, make sure to look at the existing page-objects to see if there is a suitable one for your use case.
If not, evaluate if creating a reusable getter/method is worth it and create the ones you need in the respective context.
E.g.:
Writing a new test for a sidebar behavior.
- check `apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts` if the fragments you need are already there.
- If not, create new getters and/or methods in this file.
Usage example:
```ts
// apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
// ...
get sidebar(): Locator {
return this.page.getByRole('navigation', { name: 'sidebar' });
}
get channelsList(): Locator {
return this.sidebar.getByRole('list', { name: 'Channels' });
}
// Restricted scope: inside navigation > sidebar > list named Channels > link with name
getSearchRoomByName(name: string) {
return this.channelsList.getByRole('link', { name });
}
```
```ts
// test.spec.ts
test('should display sidebar items', async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
const targetChannel = 'channel-test';
await expect(poHomeChannel.sidebar.getSearchRoomByName(targetChannel)).toBeVisible();
});
```
## Navigation: `goto()` owns the wait
`page.goto()` resolves as soon as the document loads, long before the SPA has rendered - the next
locator then races the app. So page objects own navigation: `goto()` navigates *and* waits.
```ts
poAdminUsers = new AdminUsers(page);
await poAdminUsers.goto(); // the next line can act on the page
```
Never call `page.goto()` from a test. If the page you need has no page object, add one - under
`Admin`, `Account` or `OmnichannelAdmin` it is usually two lines:
```ts
export class AdminThirdPartyLogin extends Admin {
protected readonly route = AdminSectionsHref.thirdPartyLogin;
protected readonly title = 'Third-party login';
}
```
`title` is the page's heading, matched with `exact: true` - copy it from
`packages/i18n/src/locales/en.i18n.json` rather than guessing the casing. It also scopes
`pageContent`, the `main` region to hang the page's other locators off.
Anything else extends `RoutedPage` directly and implements `waitForReady()`. Override it when the
heading is not enough - `AdminUsers` waits for its search box, `OmnichannelAdmin` for its table.
Two more hooks:
- `gotoExpecting(until)` - the route settles on something else, e.g. an upsell dialog on CE.
- `navigateTo(route, until?)` - build extra `gotoSomething()` methods when a page object owns more
than one route (`gotoGeneral()`, `gotoChannel(name)`). `until` takes a `Locator` or an async waiter.
Auth screens (`Login`, `Registration`, `Authenticated`) take the url to navigate to, since an
anonymous visitor asking for `/home` lands somewhere else.
A raw `page.goto()` is still right for external urls, urls derived from `page.url()`, and tests that
must not wait for the route to settle.
## Cleanup after tests
- Remember to delete all users, channels, rooms, etc, created during the tests.
- Reset settings to their default values after the tests, if changed.
- Close all new pages opened during the tests.
- You can use the `test.afterAll()` or `test.afterEach()` methods to clean up after the tests.
```ts
// test.spec.ts
test.describe.serial('feature example', ({ api}) => {
let targetChannel;
let targetDiscussion;
test.beforeAll(async ({ api }) => {
// change setting value
await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true);
// create channel
targetChannel = await createTargetChannel(api, { members: ['user1'] });
// create discussion
targetDiscussion = await createTargetDiscussion(api);
});
test.afterAll(async ({ api }) => {
// reset setting value
await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false);
// delete channel
await deleteChannel(api, targetChannel);
// delete discussion
await deleteRoom(api, targetDiscussion._id);
});
})
```
## General recommendations
### Use descriptive test names
Test names should clearly describe the behavior and expected outcome being verified.
### Use `test.describe` for grouping tests
It can be used to group related tests into a test suite. It provides a way to organize tests logically and improve the readability and maintainability of your test code.
```ts
test.describe('Feature Test', () => {
test('should display the feature heading after navigation', async ({ api}) => {
// do some tests
});
});
```
### Use `test.step` for grouping steps
Enhances test readability and provides more detailed information in test reports
```ts
test.describe('Feature Test', () => {
test('should display the feature heading after navigation', async ({ page }) => {
await test.step('open the feature page', async () => {
const poFeature = new FeaturePage(page);
await poFeature.goto();
await expect(page.getByRole('heading', { name: 'Feature' })).toBeVisible();
});
});
});
```
### Big test files should not be `.serial`:
- Ok for tests with very few steps, when steps have dependencies on each other, you can simulate the user flow sequentially.
- Avoid chaining big tests with `test.serial` - dependant steps make it harder to debug and/or make small changes
> If you are changing something on the 34th step, you would have to run the whole test suite multiple times during development, instead of olny running the test step in question.
## Performance patterns
E2E tests are the most expensive tests we run. Two patterns, applied together, typically cut a suite's runtime by ~80% and remove the most common sources of flake. They are the standard for new suites and the target for migrating existing ones.
### Pattern 1 — seed state through the API, not the UI
Use the browser **only for what the test actually verifies**. Everything else — creating rooms, DMs, discussions, threads, sending setup messages — goes through REST.
Why:
- A REST call costs 30–150ms; the equivalent UI flow (menu → modal → input → submit → wait for nav) costs 2–8s per step.
- API errors are deterministic and synchronous. UI errors manifest as timeouts on overloaded CI, which is a slow and ambiguous signal.
- The test fails only when the feature under test breaks, not when an unrelated setup UI regresses. A broken "create discussion" modal should not take down every test that needs a discussion as fixture.
- Removes the most flake-prone interactions: hover-revealed menus, modal animations, search debounces.
Do **not** apply when the setup UI is the actual subject of the test (`create-direct.spec.ts`, `create-discussion.spec.ts`, `create-channel.spec.ts`, etc.). There, keep one test per creation flow going through the UI; unrelated tests rely on the API path.
### Pattern 2 — share the browser context across a serial suite
For suites already using `test.describe.serial`, move `browser.newContext(), pageObject.goto()` and the initial room navigation from `beforeEach` to `beforeAll`.
Why: bootstrapping a Playwright context plus hydrating the Meteor app costs ~1.5–2.5s per test. In a serial suite the isolation cost is already paid — collecting the speed benefit is free.
Preconditions:
- Suite is `test.describe.serial`.
- All tests share the same user / storage state.
- Each test leaves the page in a known "home position" (base room open, no modals, threads, or side panels lingering).
Do **not** apply when:
- Tests need different users or storage states.
- Suite covers auth, 2FA, or session lifecycle — the browser state is the subject.
- Suite has more than ~15 tests: the debug cost of shared state outgrows the speed win (see "Big test files should not be `.serial`" above).
### Pattern 3 — navigate to a known room by URL, not by search
When a test already knows the room it needs (a channel created via API, `general`, etc.), open it directly with `poHomeChannel.gotoChannel(name)` instead of going home and searching for it.
```ts
// ❌ fragile — three independent things must align before the combobox `fill` resolves
await page.goto('/home');
await poHomeChannel.navbar.openChat(targetChannel);
// ✅ deterministic — navigate straight to the room, then wait for it to be ready
await poHomeChannel.gotoChannel(targetChannel);
```
Why: searching the navbar couples the navigation to (1) the app shell hydrating, (2) the **search index** having picked up the room — a real lag for a channel created moments earlier via API — and (3) the listbox rendering and the option becoming clickable. Any one stalling makes `searchInput.fill` hang until the test timeout, surfacing as the misleading `Target page, context or browser has been closed`. `gotoChannel` loads `/channel/<name>` and then `waitForChannel()` (main region, heading, message list, `aria-busy` cleared), so it has a proper readiness wait without the search dependency.
Do **not** apply when:
- The navbar search is the actual subject of the test.
- Navigating to a **DM** (`/direct/<username>`) or a **discussion** — `gotoChannel` builds a `/channel/` URL only.
- Reaching a channel the user is **not a member of** — the direct URL hits a different preview/join flow; verify the behavior before switching.
## API helpers for state seeding
Prefer these helpers in `beforeAll` / `beforeEach` and in setup `test.step`s. All live under `apps/meteor/tests/e2e/utils/`.
| Intent | Helper | REST endpoint |
| ------------------------------------ | --------------------------------------------------------- | ------------------------- |
| Create public channel | `createTargetChannel(api)` | `/channels.create` |
| Create public channel (full room) | `createTargetChannelAndReturnFullRoom(api)` | `/channels.create` |
| Create private channel | `createTargetPrivateChannel(api)` | `/groups.create` |
| Create private group (full room) | `createTargetGroupAndReturnFullRoom(api)` | `/groups.create` |
| Create team | `createTargetTeam(api)` | `/teams.create` |
| Create discussion (fresh parent) | `createTargetDiscussion(api)` | `/rooms.createDiscussion` |
| Create discussion on existing msg | `createDiscussion(api, parentRoomId, parentMsgId, name)` | `/rooms.createDiscussion` |
| Create DM room (get id back) | `createDirectMessageRoom(api, username)` | `/im.create` |
| Send message to a room | `sendMessage(api, roomId, msg)` | `/chat.sendMessage` |
| Send message inside a thread | `sendMessage(api, roomId, msg, parentMsgId)` | `/chat.sendMessage` |
| Send message as a specific user | `sendMessageFromUser(request, user, rid, msg)` | `/chat.postMessage` |
| Delete channel (by name) | `deleteChannel(api, roomName)` | `/channels.delete` |
| Delete room (by id) | `deleteRoom(api, roomId)` | `/rooms.delete` |
| Delete team | `deleteTeam(api, teamName)` | `/teams.delete` |
If the helper you need is missing, add it under `utils/` and re-export it from `utils/index.ts` rather than inlining the REST call in the spec.
## Template: optimized `.serial` suite
Copy-paste starting point for a new suite that applies both patterns.
```ts
import type { Page, BrowserContext } from 'playwright-core';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel, sendMessage } from './utils';
import { expect, test } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('Feature X', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
let targetChannelId: string;
let page: Page;
let context: BrowserContext;
test.beforeAll(async ({ browser, api }) => {
targetChannel = await createTargetChannel(api);
const info = await (await api.get(`/channels.info?roomName=${targetChannel}`)).json();
targetChannelId = info.channel._id;
context = await browser.newContext();
page = await context.newPage();
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test.afterAll(async ({ api }) => {
await api.post('/channels.delete', { roomName: targetChannel });
await page.close();
await context.close();
});
test('exercises the feature', async ({ api }) => {
const messageText = 'seed';
await sendMessage(api, targetChannelId, messageText);
await expect(poHomeChannel.content.lastUserMessage).toContainText(messageText);
// assertions for the feature under test
});
});
```
## Migrating an existing suite
Recipe for a single spec. Keep PRs to at most 5 files so reviews stay tractable.
1. **Baseline**. Run the spec three times locally and record the median per-test time. Paste it in the PR body.
2. **Map setup**. For every test, mark each `test.step` and every `beforeEach` call that only prepares state (creates rooms, messages, threads, opens menus). These are candidates for API seeding.
3. **Apply Pattern 1**. Replace the marked UI setup with helpers from the table above. If a helper is missing, add it under `utils/` and export it from `utils/index.ts`.
4. **Apply Pattern 2** — only if the suite is `.serial` and the preconditions hold. Move context, page, navigation to `beforeAll`. Close page and context in `afterAll`. Ensure every test returns the page to the home position.
5. **Check coverage when consolidating**. If you merge tests (e.g. four formatting tests into one), confirm the merged test still asserts every behavior the originals covered. List the merges in the PR body.
6. **Compare**. Run three times again, record the new median. If the improvement is under 30%, revisit — the coupling may not be worth it.
7. **PR body must include**: before/after timings, the list of UI setups moved to API, and an explicit reason if you chose not to apply one of the patterns.
## Anti-patterns to flag in review
- `poHomeChannel.content.sendMessage(...)` used inside `beforeEach` or `beforeAll` — that is setup, should be `sendMessage(api, ...)`.
- Opening meatball menus or modals purely to create a discussion, thread, or DM as a setup step.
- `test.describe.serial` that builds a fresh context and navigates in `beforeEach` — both should be shared in `beforeAll`.
- Any raw `page.goto(...)` in a spec — use the page object's `goto()` (see [Navigation](#navigation-goto-owns-the-wait)).
- `goto()` + `navbar.openChat(name)` to reach a room the test already knows — use `poHomeChannel.gotoChannel(name)` (see Pattern 3). Searching a just-created channel races the search index and hangs until timeout.
- New non-serial suites whose tests still each carry >3s of UI setup.
- Inline `api.post('/im.create', …)` / `api.post('/chat.sendMessage', …)` in a spec instead of extending the helpers in `utils/`.