snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
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
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
410
apps/meteor/tests/e2e/README.md
Normal file
410
apps/meteor/tests/e2e/README.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 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/`.
|
||||
Reference in New Issue
Block a user