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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View 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/`.

View File

@@ -0,0 +1,120 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { MongoClient } from 'mongodb';
import { IS_EE, URL_MONGODB } from './config/constants';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetGroupAndReturnFullRoom, deleteRoom, setSettingValueById } from './utils';
import { convertHexToRGB } from './utils/convertHexToRGB';
import { expect, test } from './utils/test';
test.use({ storageState: Users.admin.state });
const attrKey = `clearance_${Date.now()}`;
const bannersConfig = {
version: 1,
enabled: true,
banner: {
style: 'classic',
uppercase: true,
monospace: false,
delimiter: ' // ',
colorMode: 'highest',
fallbackText: 'NO CLASSIFICATION DATA',
fallbackColor: '#6C727A',
},
attributes: [
{
id: 'clearance',
source: attrKey,
label: 'Clearance',
showInBanner: true,
showLabel: true,
bannerLabel: 'CLEARANCE',
labelSeparator: '-',
valueSeparator: '/',
sortAlpha: false,
groupThreshold: 0,
multipleLabel: 'MULTIPLE',
drivesColor: true,
values: [
{ source: 'TS', label: 'TOP SECRET', color: '#ff8c00' },
{ source: 'U', label: 'UNCLASSIFIED', color: '#007a33' },
],
},
],
};
test.describe.serial('abac-classification-banner', () => {
let poHomeChannel: HomeChannel;
let connection: MongoClient;
let room: IRoom;
let attributeId: string;
test.skip(!IS_EE, 'Enterprise Only');
test.beforeAll(async ({ api }) => {
connection = await MongoClient.connect(URL_MONGODB);
await Promise.all([
setSettingValueById(api, 'ABAC_Enabled', true),
setSettingValueById(api, 'ABAC_PDP_Type', 'local'),
setSettingValueById(api, 'ABAC_Attribute_Store', 'local'),
setSettingValueById(api, 'ABAC_Classification_Banners_Enabled', true),
setSettingValueById(api, 'ABAC_Classification_Banners_Config', JSON.stringify(bannersConfig)),
]);
expect((await api.post('/abac/attributes', { key: attrKey, values: ['TS', 'U'] })).status()).toBe(200);
const { attributes } = await (await api.get('/abac/attributes', { key: attrKey })).json();
attributeId = attributes.find((attribute: { key: string }) => attribute.key === attrKey)._id;
// the local PDP only keeps members whose own attributes match the room's,
// so the admin gets them straight in the database before the room is tagged
await connection
.db()
.collection<IUser>('users')
.updateOne({ username: Users.admin.data.username }, { $push: { abacAttributes: { key: attrKey, values: ['TS', 'U'] } } });
({ group: room } = await createTargetGroupAndReturnFullRoom(api));
expect((await api.put(`/abac/rooms/${room._id}/attributes/${attrKey}`, { values: ['TS'] })).status()).toBe(200);
});
test.afterAll(async ({ api }) => {
await deleteRoom(api, room._id);
await api.delete(`/abac/attributes/${attributeId}`);
await connection
.db()
.collection<IUser>('users')
.updateOne({ username: Users.admin.data.username }, { $pull: { abacAttributes: { key: attrKey } } });
await connection.close();
await Promise.all([
setSettingValueById(api, 'ABAC_Classification_Banners_Config', ''),
setSettingValueById(api, 'ABAC_Classification_Banners_Enabled', false),
setSettingValueById(api, 'ABAC_Enabled', false),
]);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
test('should show the classification banner built from the config in an ABAC room', async ({ page }) => {
await poHomeChannel.navbar.openChat(room.name as string);
const banner = page.getByRole('region', { name: 'Room Attributes' });
await expect(banner).toBeVisible();
await expect(banner).toHaveText('CLEARANCE-TOP SECRET');
await expect(banner).toHaveCSS('background-color', convertHexToRGB('#ff8c00'));
});
test('should not show the banner when classification banners are disabled', async ({ page, api }) => {
await setSettingValueById(api, 'ABAC_Classification_Banners_Enabled', false);
await poHomeChannel.navbar.openChat(room.name as string);
await expect(poHomeChannel.composer.inputMessage).toBeVisible();
await expect(page.getByRole('region', { name: 'Room Attributes' })).toHaveCount(0);
});
});

View File

@@ -0,0 +1,110 @@
import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { AccountSecurity, HomeChannel, Login } from './page-objects';
import { test, expect } from './utils/test';
import { createTestUser, type ITestUser } from './utils/user-helpers';
test.describe.serial('Forget session on window close setting', () => {
let poLogin: Login;
test.beforeEach(async ({ page }) => {
poLogin = new Login(page);
await poLogin.goto();
});
test.describe('Setting off', async () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false });
});
test('Login using credentials and reload to stay logged in', async ({ page, context }) => {
await poLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
const newPage = await context.newPage();
await new HomeChannel(newPage).goto();
await expect(newPage.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
});
});
test.describe('Setting on', async () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: true });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false });
});
test('Login using credentials and reload to stay logged in', async ({ page }) => {
await poLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
await page.reload();
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
});
test('Login using credentials in a new tab after first tab logged in', async ({ page, context }) => {
await poLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
const newPage = await context.newPage();
const newPoLogin = new Login(newPage);
await newPoLogin.goto();
await newPoLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);
await expect(newPage.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
});
test.describe('E2EE save password flow', () => {
// Dedicated throwaway user: resetE2EEPassword() calls Users.unsetLoginTokens(),
// which would invalidate the shared admin login token the `api` fixture
// authenticates with — breaking every subsequent admin-authenticated request
// in the worker (e.g. the next spec's beforeAll settings changes silently 401).
let e2eeUser: ITestUser;
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false });
e2eeUser = await createTestUser(api);
});
test.afterAll(async ({ api }) => {
await e2eeUser?.delete();
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enable', { value: false });
});
test('E2EE save password button opens modal in SAVE_PASSWORD state', async ({ page }) => {
const poHomeChannel = new HomeChannel(page);
const poAccountSecurity = new AccountSecurity(page);
await poLogin.login(e2eeUser.data.username, DEFAULT_USER_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
await poAccountSecurity.goto();
await poAccountSecurity.resetE2EEPassword();
await page.locator('role=button[name="Login"]').waitFor();
await poLogin.login(e2eeUser.data.username, DEFAULT_USER_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
await expect(poHomeChannel.bannerSaveEncryptionPassword).toBeVisible();
await poHomeChannel.bannerSaveEncryptionPassword.click();
const randomPassword = await page.evaluate(() => window.sessionStorage.getItem('e2e.randomPassword') ?? '');
expect(randomPassword).toBeTruthy();
await expect(poHomeChannel.dialogSaveE2EEPassword).toBeVisible();
await expect(poHomeChannel.dialogSaveE2EEPassword).toContainText(randomPassword);
});
});
});
});

View File

@@ -0,0 +1,44 @@
import { Users, restoreState } from './fixtures/userStates';
import { test, expect } from './utils/test';
test.use({ storageState: Users.userE2EE.state });
test.describe('Account Login', () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: false });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false });
});
test.beforeEach(async ({ page }) => {
await page.goto('/home');
await restoreState(page, Users.userE2EE);
});
test('should remove private_key and public_key from localStorage when token expires', async ({ page }) => {
expect(await page.evaluate(() => localStorage.getItem('Meteor.userId'))).not.toBeNull();
expect(await page.evaluate(() => localStorage.getItem('Meteor.loginToken'))).not.toBeNull();
expect(await page.evaluate(() => localStorage.getItem('Meteor.loginTokenExpires'))).not.toBeNull();
expect(await page.evaluate(() => localStorage.getItem('private_key'))).not.toBeNull();
// public key acquisition takes time and now happens only after login cycle is complete
await page.waitForFunction(() => localStorage.getItem('public_key'));
await page.evaluate(() => {
localStorage.setItem('Meteor.loginTokenExpires', new Date().toString());
});
await page.reload();
await expect(page.getByRole('form', { name: 'Login' })).toBeVisible();
expect(await page.evaluate(() => localStorage.getItem('Meteor.userId'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('Meteor.loginToken'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('Meteor.loginTokenExpires'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('private_key'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('public_key'))).toBeNull();
});
});

View File

@@ -0,0 +1,66 @@
import type { Page } from 'playwright-core';
import { IS_EE } from './config/constants';
import { createAuxContext } from './fixtures/createAuxContext';
import injectInitialData from './fixtures/inject-initial-data';
import { Users } from './fixtures/userStates';
import { Login } from './page-objects';
import { AccountSectionsHref } from './page-objects/account';
import { AccountManageDevices } from './page-objects/account.manage-devices';
import { test, expect } from './utils/test';
test.describe('Account Manage Devices Page', () => {
test.skip(!IS_EE);
test.use({ storageState: Users.user1.state });
let page: Page;
let accountDevices: AccountManageDevices;
let poLogin: Login;
test.beforeEach(async ({ browser }) => {
({ page } = await createAuxContext(browser, Users.user1));
accountDevices = new AccountManageDevices(page);
poLogin = new Login(page);
await accountDevices.goto();
});
test.afterEach(async () => {
await page.close();
await injectInitialData();
});
test('should show Manage Devices page', async () => {
await expect(accountDevices.pageContent).toBeVisible();
});
test('should logout current device and redirect to login page', async () => {
await accountDevices.table.orderByLastLogin();
const deviceId = await accountDevices.getNthDeviceId(1);
await accountDevices.logoutDeviceById(deviceId);
await poLogin.waitForDisplay();
});
test('should logout other device successfully', async ({ browser }) => {
const context2 = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const page2 = await context2.newPage();
const loginPage2 = new Login(page2);
const accountDevices2 = new AccountManageDevices(page2);
await test.step('should login same user in another session', async () => {
await loginPage2.goto(AccountSectionsHref.manageDevices);
await loginPage2.login('user1', 'password');
await expect(accountDevices2.pageContent).toBeVisible();
});
await test.step('should logout device 1 from session 2', async () => {
await accountDevices2.table.orderByLastLogin();
const device1Id = await accountDevices2.getNthDeviceId(2);
await accountDevices2.logoutDeviceById(device1Id);
await poLogin.waitForDisplay();
await expect(accountDevices2.table.getDeviceRowById(device1Id)).not.toBeVisible();
});
await context2.close();
});
});

View File

@@ -0,0 +1,161 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import {
AccountAccessibility,
AccountFeaturePreview,
AccountOmnichannel,
AccountProfile,
AccountTokens,
HomeChannel,
} from './page-objects';
import { setSettingValueById } from './utils/setSettingValueById';
import { test, expect } from './utils/test';
test.use({ storageState: Users.user3.state });
test.describe.serial('settings-account-profile', () => {
let poHomeChannel: HomeChannel;
let poAccountProfile: AccountProfile;
const token = faker.string.alpha(10);
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
poAccountProfile = new AccountProfile(page);
});
test.describe('Profile', () => {
test.beforeEach(async () => {
await poAccountProfile.goto();
});
// FIXME: solve test intermitencies
test.skip('expect update profile with new name/username', async () => {
const newName = faker.person.fullName();
const newUsername = faker.internet.userName({ firstName: newName });
await poAccountProfile.inputName.fill(newName);
await poAccountProfile.inputUsername.fill(newUsername);
await poAccountProfile.btnSubmit.click();
await poAccountProfile.btnClose.click();
await poHomeChannel.navbar.openChat('general');
await poHomeChannel.content.sendMessage('any_message');
await expect(poHomeChannel.content.lastUserMessageNotSequential).toContainText(newUsername);
await poHomeChannel.content.lastUserMessageNotSequential.locator('figure').click();
await poHomeChannel.userCard.openUserInfo();
await expect(poHomeChannel.tabs.userInfo.username).toHaveText(newUsername);
});
test.describe('Avatar', () => {
test('should change avatar image by uploading file', async () => {
await poAccountProfile.inputImageFile.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg');
await poAccountProfile.btnSubmit.click();
await expect(poAccountProfile.userAvatarEditor).toHaveAttribute('src');
});
test('should change avatar image from url', async () => {
await poAccountProfile.inputAvatarLink.fill('https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50');
await poAccountProfile.btnSetAvatarLink.click();
await poAccountProfile.btnSubmit.click();
await expect(poAccountProfile.userAvatarEditor).toHaveAttribute('src');
});
test('should show inline error if the image url is not valid', async () => {
await poAccountProfile.inputAvatarLink.fill('https://invalidUrl');
await poAccountProfile.btnSetAvatarLink.click();
await expect(poAccountProfile.errorInvalidUrl).toBeVisible();
});
test('should show inline error if url does not point to an image', async () => {
await poAccountProfile.inputAvatarLink.fill('https://google.com');
await poAccountProfile.btnSetAvatarLink.click();
await expect(poAccountProfile.errorInvalidUrl).toBeVisible();
});
test('should not allow avatar URL change when avatar changes are disabled', async ({ api }) => {
await setSettingValueById(api, 'Accounts_AllowUserAvatarChange', false);
await expect(poAccountProfile.btnSetAvatarLink).toBeDisabled();
await expect(poAccountProfile.inputAvatarLink).toBeDisabled();
await setSettingValueById(api, 'Accounts_AllowUserAvatarChange', true);
});
});
});
test('Personal Access Tokens', async ({ page }) => {
const poAccountTokens = new AccountTokens(page);
const response = page.waitForResponse('**/api/v1/users.getPersonalAccessTokens');
await poAccountTokens.goto();
await response;
await test.step('should show empty personal access tokens table', async () => {
await expect(poAccountTokens.tokensTableEmpty).toBeVisible();
await expect(poAccountTokens.inputToken).toBeVisible();
});
await test.step('should show new personal token', async () => {
await poAccountTokens.inputToken.fill(token);
await poAccountTokens.btnTokensAdd.click();
await expect(poAccountTokens.tokenAddedModal).toBeVisible();
await poAccountTokens.btnTokenAddedOk.click();
});
await test.step('should not allow add new personal with no name', async () => {
await poAccountTokens.btnTokensAdd.click();
await expect(page.getByRole('alert').filter({ hasText: 'Please provide a name for your token' })).toBeVisible();
});
await test.step('should not allow add new personal token with same name', async () => {
await poAccountTokens.inputToken.fill(token);
await poAccountTokens.btnTokensAdd.click();
await expect(poAccountTokens.tokensRows).toHaveCount(1);
});
await test.step('should regenerate personal token', async () => {
await poAccountTokens.tokenInTable(token).locator('button >> nth=0').click();
await poAccountTokens.btnRegenerateTokenModal.click();
await expect(poAccountTokens.tokenAddedModal).toBeVisible();
await poAccountTokens.btnTokenAddedOk.click();
});
await test.step('should delete personal token', async () => {
await poAccountTokens.tokenInTable(token).locator('button >> nth=1').click();
await poAccountTokens.btnRemoveTokenModal.click();
await expect(poAccountTokens.tokensTableEmpty).toBeVisible();
});
});
test.describe('Omnichannel', () => {
test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => {
await new AccountOmnichannel(page).goto();
const results = await makeAxeBuilder().analyze();
expect(results.violations).toEqual([]);
});
});
test.describe('Feature Preview', () => {
test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => {
await new AccountFeaturePreview(page).goto();
const results = await makeAxeBuilder().analyze();
expect(results.violations).toEqual([]);
});
});
test.describe('Accessibility & Appearance', () => {
test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => {
await new AccountAccessibility(page).goto();
const results = await makeAxeBuilder().analyze();
expect(results.violations).toEqual([]);
});
});
});

View File

@@ -0,0 +1,114 @@
import { faker } from '@faker-js/faker';
import { ADMIN_CREDENTIALS } from './config/constants';
import { Users } from './fixtures/userStates';
import { AccountSecurity } from './page-objects';
import { setSettingValueById, updateOwnUserPassword } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
const RANDOM_PASSWORD = faker.helpers
.shuffle([
faker.string.alpha({ casing: 'upper' }),
faker.string.alpha({ casing: 'lower' }),
faker.string.numeric(),
faker.string.symbol(),
faker.string.alphanumeric(10),
])
.join('');
test.describe.serial('account-security', () => {
let poAccountSecurity: AccountSecurity;
test.beforeEach(async ({ page, api }) => {
poAccountSecurity = new AccountSecurity(page);
await poAccountSecurity.gotoExpecting(poAccountSecurity.securityHeader.or(poAccountSecurity.notAuthorizedMessage));
await setSettingValueById(api, 'Accounts_Password_Policy_Enabled', false);
});
test.afterAll(async ({ api }) =>
Promise.all([
setSettingValueById(api, 'Accounts_AllowPasswordChange', true),
setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true),
setSettingValueById(api, 'E2E_Enable', false),
setSettingValueById(api, 'Accounts_Password_Policy_Enabled', true),
]),
);
test('should disable and enable email 2FA', async () => {
await poAccountSecurity.security2FASection.click();
await expect(poAccountSecurity.email2FASwitch).toBeVisible();
await poAccountSecurity.email2FASwitch.click();
await poAccountSecurity.toastMessage.waitForDisplay();
await poAccountSecurity.toastMessage.dismissToast();
await poAccountSecurity.email2FASwitch.click();
await poAccountSecurity.toastMessage.waitForDisplay();
});
test('should be able to change password', async ({ api }) => {
await test.step('change password', async () => {
await poAccountSecurity.changePassword(RANDOM_PASSWORD, RANDOM_PASSWORD, ADMIN_CREDENTIALS.password);
await expect(poAccountSecurity.inputNewPassword).toHaveValue('');
});
await test.step('change back to the original password', async () => {
expect(
(await updateOwnUserPassword(api, { newPassword: ADMIN_CREDENTIALS.password, currentPassword: RANDOM_PASSWORD })).status(),
).toBe(200);
});
});
test.describe('settings disabled', () => {
test.beforeAll(async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_AllowPasswordChange', false),
setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false),
setSettingValueById(api, 'E2E_Enable', false),
]);
});
test('security tab is invisible when password change, 2FA and E2E are disabled', async () => {
await expect(poAccountSecurity.sidebar.linkSecurity).not.toBeVisible();
await expect(poAccountSecurity.notAuthorizedMessage).toBeVisible();
});
});
test.describe('account security sections', () => {
test.beforeAll(async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_AllowPasswordChange', true),
setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false),
setSettingValueById(api, 'E2E_Enable', false),
]);
});
test.beforeEach(async () => {
await poAccountSecurity.securityHeader.waitFor({ state: 'visible' });
});
test('should display security tab and section when password change is enabled but 2FA and E2E are disabled', async () => {
await expect(poAccountSecurity.sidebar.linkSecurity).toBeVisible();
await expect(poAccountSecurity.securityPasswordSection).toBeVisible();
});
test('can access 2FA setting when enabled but password change and E2E are disabled', async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_AllowPasswordChange', false),
setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true),
setSettingValueById(api, 'E2E_Enable', false),
]);
await expect(poAccountSecurity.security2FASection).toBeVisible();
});
test('can access E2E setting when enabled but password change and 2FA are disabled', async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_AllowPasswordChange', false),
setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false),
setSettingValueById(api, 'E2E_Enable', true),
]);
await expect(poAccountSecurity.securityE2EEncryptionSection).toBeVisible();
});
});
});

View File

@@ -0,0 +1,93 @@
import { Users } from './fixtures/userStates';
import { Authenticated } from './page-objects/auth';
import { test, expect } from './utils/test';
// We mock the REST `method.call` response instead of registering a real
// Meteor method that requires 2FA: there isn't one in the codebase right now
// (every twoFactorRequired/`requireSecondFactor: true` user-facing surface is
// a typed REST endpoint), and the bug we're guarding against lives inside
// `ddpOverREST`'s `.catch` — a unit-style server fixture exercises it more
// reliably than wiring up TOTP for a real account.
test.use({ storageState: Users.admin.state });
const METHOD_NAME = 'rc-test-totp-flow';
test.describe('account-totp ddpOverREST preserves the totp-required error', () => {
test('opens the TOTP modal when the REST envelope carries a `totp-required` DDP frame, and resolves on retry', async ({ page }) => {
await new Authenticated(page).goto();
let calls = 0;
await page.route(`**/api/v1/method.call/${METHOD_NAME}`, async (route) => {
calls += 1;
const body = JSON.parse(route.request().postData() ?? '{}');
const message = JSON.parse(body.message);
const lastParam = message.params[message.params.length - 1];
const hasTotp = lastParam && typeof lastParam === 'object' && lastParam.twoFactorCode && lastParam.twoFactorMethod === 'totp';
if (!hasTotp) {
// First leg: server demands a TOTP code. The body is shaped exactly
// like `mountResult({ id, error })` in server/api/v1/misc.ts —
// `message` is a DDP-encoded `result` frame whose `error.error`
// carries the original `totp-required` code. The bug under test
// rebuilt this envelope as `error: 'unknown'` and the 2FA modal
// never opened.
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({
success: false,
message: JSON.stringify({
msg: 'result',
id: message.id,
error: {
isClientSafe: true,
error: 'totp-required',
reason: 'TOTP Required',
message: 'TOTP Required [totp-required]',
errorType: 'Meteor.Error',
details: { method: 'totp', availableMethods: ['totp'] },
},
}),
}),
});
return;
}
// Second leg: client retried with the code; respond success.
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
message: JSON.stringify({
msg: 'result',
id: message.id,
result: { ok: true, code: lastParam.twoFactorCode },
}),
}),
});
});
// Drive the call from the page so it goes through Meteor.callAsync →
// withAsyncTOTP → ddpOverREST → process2faAsyncReturn — the exact path
// where the catch handler used to drop `error: 'totp-required'` and the
// TOTP modal therefore never opened.
await page.evaluate((method) => {
(window as any).__totpCallResult = (window as any).Meteor.callAsync(method)
.then((value: unknown) => ({ ok: true, value }))
.catch((err: unknown) => ({ ok: false, error: String((err as Error)?.message ?? err) }));
}, METHOD_NAME);
const totpModal = page.getByRole('dialog', { name: 'Enter TOTP password' });
await expect(totpModal).toBeVisible({ timeout: 10_000 });
await totpModal.getByPlaceholder('Enter code here').fill('123456');
await totpModal.getByRole('button', { name: 'Verify' }).click();
await expect(totpModal).toBeHidden();
const result = await page.evaluate(() => (window as any).__totpCallResult);
expect(result).toEqual({ ok: true, value: { ok: true, code: '123456' } });
expect(calls).toBe(2);
});
});

View File

@@ -0,0 +1,118 @@
import type { Page } from 'playwright-core';
import { IS_EE } from './config/constants';
import { createAuxContext } from './fixtures/createAuxContext';
import injectInitialData from './fixtures/inject-initial-data';
import { Users } from './fixtures/userStates';
import { Authenticated, HomeChannel, Login, Registration } from './page-objects';
import { AdminDeviceManagement } from './page-objects/admin-device-management';
import { test, expect } from './utils/test';
test.describe('Admin Device Management Page', () => {
test.skip(!IS_EE);
test.use({ storageState: Users.admin.state });
let page: Page;
let adminDeviceManagement: AdminDeviceManagement;
let poLogin: Login;
test.beforeEach(async ({ browser }) => {
({ page } = await createAuxContext(browser, Users.admin));
adminDeviceManagement = new AdminDeviceManagement(page);
poLogin = new Login(page);
await adminDeviceManagement.goto();
});
test.afterEach(async () => {
await page.close();
await injectInitialData();
});
test('should show Device management page', async () => {
await expect(adminDeviceManagement.pageContent).toBeVisible();
});
test('should logout current device and redirect to login page', async () => {
const deviceId = await adminDeviceManagement.getUsersDeviceId('rocketchat.internal.admin.test');
await adminDeviceManagement.logoutDeviceById(deviceId);
await poLogin.waitForDisplay();
});
test('should logout current device from device info tab and redirect to login page', async () => {
const deviceId = await adminDeviceManagement.getUsersDeviceId('rocketchat.internal.admin.test');
await adminDeviceManagement.searchUserDevice('rocketchat.internal.admin.test');
await adminDeviceManagement.table.getDeviceRowById(deviceId).click();
await expect(adminDeviceManagement.deviceInfo.getDeviceInfoId(deviceId)).toBeVisible();
await adminDeviceManagement.deviceInfo.btnLogoutDevice.click();
await adminDeviceManagement.logoutModal.confirmLogout();
await poLogin.waitForDisplay();
});
test('should logout other device successfully', async ({ browser }) => {
const user2Page = await browser.newPage({ storageState: Users.user2.state });
const loginPage2 = new Registration(user2Page);
await new Authenticated(user2Page).goto();
const user2DeviceId = await adminDeviceManagement.getUsersDeviceId('user2');
await test.step('should logout user2 and redirect to login page', async () => {
await adminDeviceManagement.logoutDeviceById(user2DeviceId);
await loginPage2.waitForDisplay();
});
await test.step('should no longer show user2 device in admin device management page', async () => {
await adminDeviceManagement.searchUserDevice('user2');
await expect(adminDeviceManagement.table.getDeviceRowById(user2DeviceId)).not.toBeVisible();
});
await user2Page.close();
});
test('should remove user2 from device list when user2 logs out from their own session', async ({ browser }) => {
const user2Page = await browser.newPage({ storageState: Users.user2.state });
const loginPage2 = new Registration(user2Page);
const poUser2Home = new HomeChannel(user2Page);
await poUser2Home.goto();
await test.step('should list user2 device while user2 is logged in', async () => {
await expect(adminDeviceManagement.pageContent).toBeVisible();
await adminDeviceManagement.searchUserDevice('user2');
await expect(adminDeviceManagement.table.getRowsForUsername('user2')).toHaveCount(1);
});
await test.step('should log user2 out from the app and redirect to login page', async () => {
await poUser2Home.navbar.logout();
await loginPage2.waitForDisplay();
});
await test.step('should no longer show user2 device in admin device management page', async () => {
await page.reload();
await expect(adminDeviceManagement.pageContent).toBeVisible();
await adminDeviceManagement.searchUserDevice('user2');
await expect(adminDeviceManagement.table.getRowsForUsername('user2')).toHaveCount(0);
});
await user2Page.close();
});
test('Should show empty state when searching for user without result', async () => {
const noResultQuery = 'nonexistentuser';
await adminDeviceManagement.searchUserDevice('noResultQuery');
await expect(adminDeviceManagement.table.getRowsForUsername(noResultQuery)).toHaveCount(0);
await expect(adminDeviceManagement.emptyState).toBeVisible();
});
});
test.describe('Admin Device Management Page - unauthorized access', () => {
test.skip(!IS_EE);
test.use({ storageState: Users.user2.state });
test('should not access device-management when user has no view-device-management permission', async ({ page }) => {
const user2DeviceManagement = new AdminDeviceManagement(page);
await user2DeviceManagement.gotoExpecting(user2DeviceManagement.notAuthorizedMessage);
await expect(user2DeviceManagement.notAuthorizedMessage).toBeVisible();
await expect(user2DeviceManagement.pageContent).not.toBeVisible();
});
});

View File

@@ -0,0 +1,94 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { AdminInfo, AdminRooms, AdminSectionsHref } from './page-objects';
import { createTargetChannel, createTargetPrivateChannel } from './utils';
import { expect, test } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('admin-rooms', () => {
let channel: string;
let privateRoom: string;
let adminRooms: AdminRooms;
let adminInfo: AdminInfo;
test.beforeEach(async ({ page }) => {
adminRooms = new AdminRooms(page);
await adminRooms.goto();
});
test.beforeAll(async ({ api }) => {
[channel, privateRoom] = await Promise.all([createTargetChannel(api), createTargetPrivateChannel(api)]);
});
test('should display the Rooms Table', async ({ page }) => {
await expect(page.getByRole('main').getByRole('heading', { level: 1, name: 'Rooms', exact: true })).toBeVisible();
await expect(page.getByRole('main').getByRole('table')).toBeVisible();
});
test('should filter room by name', async ({ page }) => {
await adminRooms.inputSearchRooms.fill(channel);
await expect(page.locator(`[qa-room-name="${channel}"]`)).toBeVisible();
});
test('should filter rooms by type', async ({ page }) => {
const dropdown = await adminRooms.dropdownFilterRoomType();
await dropdown.click();
const privateOption = page.locator('text=Private channels');
await privateOption.waitFor();
await privateOption.click();
const selectedDropdown = await adminRooms.dropdownFilterRoomType('Rooms (1)');
await expect(selectedDropdown).toBeVisible();
await expect(page.locator('text=Private Channel').first()).toBeVisible();
});
test('should filter rooms by type and name', async ({ page }) => {
await adminRooms.inputSearchRooms.fill(privateRoom);
const dropdown = await adminRooms.dropdownFilterRoomType();
await dropdown.click();
await page.locator('text=Private channels').click();
await expect(page.locator(`[qa-room-name="${privateRoom}"]`)).toBeVisible();
});
test('should be empty in case of the search does not find any room', async ({ page }) => {
const nonExistingChannel = faker.string.alpha(10);
await adminRooms.inputSearchRooms.fill(nonExistingChannel);
const dropdown = await adminRooms.dropdownFilterRoomType();
await dropdown.click();
await page.locator('text=Private channels').click();
await expect(page.locator('text=No results found')).toBeVisible();
});
test('should filter rooms by type and name and clean the filter after changing section', async ({ page }) => {
adminInfo = new AdminInfo(page);
await adminRooms.inputSearchRooms.fill(privateRoom);
const dropdown = adminRooms.dropdownFilterRoomType();
await dropdown.click();
await page.locator('text=Private channels').click();
const workspaceButton = await adminRooms.adminSectionButton(AdminSectionsHref.workspace);
await workspaceButton.click();
await expect(adminInfo.pageContent).toBeVisible();
const roomsButton = await adminRooms.adminSectionButton(AdminSectionsHref.rooms);
await roomsButton.click();
const selectDropdown = await adminRooms.dropdownFilterRoomType('All rooms');
await expect(selectDropdown).toBeVisible();
});
});

View File

@@ -0,0 +1,157 @@
import { Users } from './fixtures/userStates';
import { AdminUsers, HomeChannel } from './page-objects';
import { test, expect } from './utils/test';
import { createTestUser, type ITestUser } from './utils/user-helpers';
const customFieldInitial1 = 'initial1';
const adminCustomFieldValue1 = 'admin_value1';
const adminCustomFieldValue2 = 'admin_value2';
const adminCustomFieldUpdated1 = 'updated_admin1';
test.describe('Admin users custom fields', () => {
let poHomeChannel: HomeChannel;
let poAdmin: AdminUsers;
let addTestUser: ITestUser;
let updateTestUser: ITestUser;
test.use({ storageState: Users.admin.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/Accounts_CustomFields', {
value: JSON.stringify({
customFieldText1: {
type: 'text',
required: false,
},
customFieldText2: {
type: 'text',
required: false,
},
}),
});
[addTestUser, updateTestUser] = await Promise.all([
createTestUser(api),
createTestUser(api, {
data: {
customFields: {
customFieldText1: customFieldInitial1,
customFieldText2: adminCustomFieldValue2,
},
},
}),
]);
});
test.afterAll(async ({ api }) => {
await Promise.all([
api.post('/settings/Accounts_CustomFields', {
value: '',
}),
addTestUser.delete(),
updateTestUser.delete(),
]);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
poAdmin = new AdminUsers(page);
await poAdmin.goto();
});
test('should allow admin to add user custom fields', async () => {
await test.step('should find and click on add test user', async () => {
await poAdmin.searchUser(addTestUser.data.username);
await poAdmin.getUserRowByUsername(addTestUser.data.username).click();
});
await test.step('should navigate to edit user form', async () => {
await poAdmin.userInfo.btnEdit.click();
});
await test.step('should fill custom fields for user', async () => {
await poAdmin.editUser.getCustomField('customFieldText1').fill(adminCustomFieldValue1);
await poAdmin.editUser.getCustomField('customFieldText2').fill(adminCustomFieldValue2);
});
await test.step('should save user custom fields', async () => {
await poAdmin.editUser.btnSaveUser.click();
await poHomeChannel.toastMessage.dismissToast();
await poAdmin.editUser.close();
});
await test.step('should verify custom fields were saved', async () => {
await poAdmin.getUserRowByUsername(addTestUser.data.username).click();
await poAdmin.userInfo.btnEdit.click();
await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldValue1);
await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2);
});
});
test('should allow admin to update existing user custom fields', async () => {
await test.step('should find and click on update test user', async () => {
await poAdmin.searchUser(updateTestUser.data.username);
await poAdmin.getUserRowByUsername(updateTestUser.data.username).click();
});
await test.step('should navigate to edit user form', async () => {
await poAdmin.userInfo.btnEdit.click();
});
await test.step('should verify existing values and update one custom field', async () => {
await poAdmin.editUser.inputName.waitFor();
await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(customFieldInitial1);
await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2);
await poAdmin.editUser.getCustomField('customFieldText1').clear();
await poAdmin.editUser.getCustomField('customFieldText1').fill(adminCustomFieldUpdated1);
});
await test.step('should save and verify partial update', async () => {
await poAdmin.editUser.btnSaveUser.click();
await poHomeChannel.toastMessage.dismissToast();
await poAdmin.editUser.close();
await poAdmin.getUserRowByUsername(updateTestUser.data.username).click();
await poAdmin.userInfo.btnEdit.click();
await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldUpdated1);
await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2);
});
});
test.describe('with invalid custom field type', () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/Accounts_CustomFields', {
value: JSON.stringify({
customFieldText1: {
type: 'invalid_type',
required: false,
},
customFieldText2: {
type: 'text',
required: false,
},
}),
});
});
test('should not render fields with invalid custom field type', async () => {
await test.step('should find and click on add test user', async () => {
await poAdmin.searchUser(addTestUser.data.username);
await poAdmin.getUserRowByUsername(addTestUser.data.username).click();
});
await test.step('should navigate to edit user form', async () => {
await poAdmin.userInfo.btnEdit.click();
});
await test.step('should verify custom field is not rendered', async () => {
await expect(poAdmin.editUser.getCustomField('customFieldText1')).not.toBeVisible();
await expect(poAdmin.editUser.getCustomField('customFieldText2')).toBeVisible();
});
});
});
});

View File

@@ -0,0 +1,66 @@
import { Users } from './fixtures/userStates';
import { AdminUsers } from './page-objects';
import { test, expect } from './utils/test';
import type { ITestUser } from './utils/user-helpers';
import { createTestUser } from './utils/user-helpers';
let userWithoutAdminAccess: ITestUser;
let userWithAdminAccess: ITestUser;
let admin: AdminUsers;
test.describe('Admin > Users Role Management', () => {
test.beforeAll('Create test users', async ({ api }) => {
userWithoutAdminAccess = await createTestUser(api);
userWithAdminAccess = await createTestUser(api, { roles: ['admin'] });
});
test.afterAll('Delete test users', async () => {
await userWithoutAdminAccess.delete();
await userWithAdminAccess.delete();
});
test.describe('Admin Role Management', () => {
test.use({ storageState: Users.admin.state });
test.beforeEach('Go to /admin/users', async ({ page }) => {
admin = new AdminUsers(page);
await admin.goto();
});
test('Make a newly created user as admin', async () => {
await test.step('should be visible in the All tab', async () => {
await admin.getTabByName().click();
await admin.searchUser(userWithoutAdminAccess.data.username);
});
await test.step('make a user admin', async () => {
await admin.dispatchUserAction(userWithoutAdminAccess.data.username, 'Make Admin');
await admin.toastMessage.waitForDisplay({ type: 'success', message: 'User is now an admin' });
});
await test.step('verify user is admin', async () => {
await admin.getUserRowByUsername(userWithoutAdminAccess.data.username).click();
await expect(admin.userInfo.root).toBeVisible();
await expect(admin.userInfo.root).toContainText('Admin');
});
});
test('Remove role as admin', async () => {
await test.step('User should be visible in the All tab', async () => {
await admin.getTabByName().click();
await admin.searchUser(userWithAdminAccess.data.username);
});
await test.step('remove admin role', async () => {
await admin.dispatchUserAction(userWithAdminAccess.data.username, 'Remove Admin');
await admin.toastMessage.waitForDisplay({ type: 'success', message: 'User is no longer an admin' });
});
await test.step('verify user role as admin is removed', async () => {
await admin.getUserRowByUsername(userWithAdminAccess.data.username).click();
await expect(admin.userInfo.root).toBeVisible();
await expect(admin.userInfo.root).not.toHaveText('Admin');
});
});
});
});

View File

@@ -0,0 +1,80 @@
import type { BrowserContext, Page } from '@playwright/test';
import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { Users } from './fixtures/userStates';
import { AdminUsers, Authenticated, Login } from './page-objects';
import { test, expect } from './utils/test';
import type { ITestUser } from './utils/user-helpers';
import { createTestUser } from './utils/user-helpers';
let user: ITestUser;
let adminUsers: AdminUsers;
test.describe.serial('Admin > Users', () => {
test.beforeAll('Create a new user', async ({ api }) => {
user = await createTestUser(api);
});
test.afterAll('Delete the new user', async () => {
await user.delete();
});
test.describe('Login', () => {
let context: BrowserContext;
let page: Page;
let poLogin: Login;
let poAuth: Authenticated;
test.beforeAll(async ({ browser }) => {
context = await browser.newContext();
page = await context.newPage();
poLogin = new Login(page);
poAuth = new Authenticated(page);
});
test.afterAll(async () => {
await context.close();
});
test('Login as a newly created user and verify its status is active', async () => {
await poLogin.goto('/login');
await test.step('should log in as the newly created user', async () => {
await poLogin.login(user.data.username, DEFAULT_USER_CREDENTIALS.password);
});
await test.step('Assert user is logged in', async () => {
await poAuth.waitForDisplay();
});
});
});
test.describe('Admin > Users Status Management', () => {
test.use({ storageState: Users.admin.state });
test.beforeEach('Go to /admin/users', async ({ page }) => {
adminUsers = new AdminUsers(page);
await adminUsers.goto();
});
test('After the first login, the user gets listed under the Active tab', async () => {
await test.step('should be visible in the All tab', async () => {
await adminUsers.getTabByName().click();
await adminUsers.searchUser(user.data.username);
});
await test.step('should not be visible in the Pending tab', async () => {
await adminUsers.getTabByName('Pending').click();
await expect(adminUsers.getUserRowByUsername(user.data.username)).not.toBeVisible();
});
await test.step('should be visible in the Active tab', async () => {
await adminUsers.getTabByName('Active').click();
await expect(adminUsers.getUserRowByUsername(user.data.username)).toBeVisible();
});
await test.step('should not be visible in the Deactivated tab', async () => {
await adminUsers.getTabByName('Deactivated').click();
await expect(adminUsers.getUserRowByUsername(user.data.username)).not.toBeVisible();
});
});
});
});

View File

@@ -0,0 +1,66 @@
import { Users } from './fixtures/userStates';
import { AdminUsers } from './page-objects';
import { setSettingValueById } from './utils/setSettingValueById';
import { test, expect } from './utils/test';
import type { ITestUser } from './utils/user-helpers';
import { createTestUser } from './utils/user-helpers';
let user: ITestUser;
let admin: AdminUsers;
test.use({ storageState: Users.admin.state });
test.describe('Admin > Users', () => {
test.beforeAll('Create a new user', async ({ api }) => {
await setSettingValueById(api, 'Accounts_ManuallyApproveNewUsers', true);
user = await createTestUser(api);
});
test.afterAll('Delete the new user', async ({ api }) => {
await user.delete();
await setSettingValueById(api, 'Accounts_ManuallyApproveNewUsers', false);
});
test.beforeEach('Go to /admin/users', async ({ page }) => {
admin = new AdminUsers(page);
await admin.goto();
});
test('New user shows in correct tabs when deactivated', async () => {
await test.step('should be visible in the All tab', async () => {
await admin.getTabByName().click();
await admin.searchUser(user.data.username);
});
await test.step('should be visible in the Pending tab', async () => {
await admin.getTabByName('Pending').click();
await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible();
});
await test.step('should not be visible in the Active tab', async () => {
await admin.getTabByName('Active').click();
await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible();
});
await test.step('should not be visible in the Deactivated tab', async () => {
await admin.getTabByName('Deactivated').click();
await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible();
});
await test.step('should move from Pending to Active tab', async () => {
await admin.getTabByName('Pending').click();
await admin.activatePendingUser(user.data.username);
await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible();
await admin.getTabByName('Active').click();
await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible();
});
await test.step('should move from Active to Deactivated tab', async () => {
await admin.getTabByName('Active').click();
await admin.dispatchUserAction(user.data.username, 'Deactivate');
await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible();
await admin.getTabByName('Deactivated').click();
await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible();
});
});
});

View File

@@ -0,0 +1,101 @@
import { Users } from './fixtures/userStates';
import { AdminSettings, AdminSectionsHref } from './page-objects';
import { Navbar } from './page-objects/fragments';
import { LoginPage } from './page-objects/login';
import { getSettingValueById, setSettingValueById } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.parallel('administration-settings', () => {
let poAdminSettings: AdminSettings;
let navbar: Navbar;
let poLoginPage: LoginPage;
test.beforeEach(async ({ page }) => {
poAdminSettings = new AdminSettings(page);
navbar = new Navbar(page);
poLoginPage = new LoginPage(page);
});
test.describe('Settings Page', () => {
test.beforeEach(async () => {
await poAdminSettings.goto();
});
test('should display settings list', async ({ page }) => {
await test.step('should list settings on load', async () => {
await expect(page.getByText('No results found')).not.toBeVisible();
});
await test.step('should list settings after logout and login', async () => {
await poAdminSettings.sidebar.close();
await navbar.logout();
await poLoginPage.loginByUserState(Users.admin);
await navbar.openAdminPanel();
const settingsButton = await poAdminSettings.adminSectionButton(AdminSectionsHref.settings);
await settingsButton.click();
await expect(page.getByText('No results found')).not.toBeVisible();
});
});
});
test.describe('General', () => {
let inputSiteURLSetting: string;
test.beforeAll(async ({ api }) => {
inputSiteURLSetting = (await getSettingValueById(api, 'Site_Url')) as string;
});
test.beforeEach(async () => {
await poAdminSettings.gotoGeneral();
});
test('should be able to reset a setting after a change', async () => {
await poAdminSettings.inputSiteURL.fill('any_text');
await poAdminSettings.btnResetSiteURL.click();
await expect(poAdminSettings.inputSiteURL).toHaveValue(inputSiteURLSetting);
});
test('should be able to go back to the settings page', async ({ page }) => {
await poAdminSettings.btnBack.click();
await expect(page).toHaveURL('/admin/settings');
});
});
test.describe('Layout', () => {
test.beforeEach(async () => {
await poAdminSettings.gotoLayout();
});
test.afterAll(async ({ api }) => setSettingValueById(api, 'theme-custom-css', ''));
test('should display the code mirror correctly', async ({ page, api }) => {
await poAdminSettings.getAccordionBtnByName('Custom CSS').click();
await test.step('should render only one code mirror element', async () => {
const codeMirrorParent = page.getByRole('code');
await expect(codeMirrorParent.locator('.CodeMirror')).toHaveCount(1);
});
await test.step('should display full screen properly', async () => {
await poAdminSettings.btnFullScreen.click();
await expect(page.getByRole('code')).toHaveCSS('width', '920px');
await poAdminSettings.btnExitFullScreen.click();
});
await test.step('should reflect updated value when valueProp changes after server update', async () => {
const codeValue = `.test-class-${Date.now()} { background-color: red; }`;
await setSettingValueById(api, 'theme-custom-css', codeValue);
const codeMirrorParent = page.getByRole('code');
await expect(codeMirrorParent.locator('.CodeMirror-line')).toHaveText(codeValue);
});
});
});
});

View File

@@ -0,0 +1,469 @@
import { faker } from '@faker-js/faker';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
import { IS_EE } from './config/constants';
import { Users } from './fixtures/userStates';
import {
AdminUsers,
AdminPermissions,
AdminRooms,
AdminThirdPartyLogin,
AdminIntegrations,
AdminInfo,
AdminEngagement,
AdminMailer,
} from './page-objects';
import { AdminDeviceManagement } from './page-objects/admin-device-management';
import { ToastMessages } from './page-objects/fragments';
import { createTargetChannel, setSettingValueById } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.parallel('administration', () => {
let targetChannel: string;
test.describe('Workspace', () => {
test.beforeEach(async ({ page }) => {
await new AdminInfo(page).goto();
});
test('expect download info as JSON', async ({ page }) => {
const [download] = await Promise.all([page.waitForEvent('download'), page.locator('button:has-text("Download info")').click()]);
await expect(download.suggestedFilename()).toBe('statistics.json');
});
});
test.describe('Engagement dashboard', () => {
test('Should show upsell modal', async ({ page }) => {
test.skip(IS_EE);
const poAdminEngagement = new AdminEngagement(page);
await poAdminEngagement.gotoExpecting(poAdminEngagement.upsellModal);
await expect(poAdminEngagement.upsellModal).toBeVisible();
});
test('Should show engagement dashboard', async ({ page }) => {
test.skip(!IS_EE);
await new AdminEngagement(page).goto();
await expect(page.locator('h1 >> text="Engagement"')).toBeVisible();
});
});
test.describe('Device management', () => {
test('Should show upsell modal', async ({ page }) => {
test.skip(IS_EE);
const poAdminDeviceManagement = new AdminDeviceManagement(page);
await poAdminDeviceManagement.gotoExpecting(poAdminDeviceManagement.upsellModal);
await expect(poAdminDeviceManagement.upsellModal).toBeVisible();
});
test('Should show device management page', async ({ page }) => {
test.skip(!IS_EE);
await new AdminDeviceManagement(page).goto();
await expect(page.locator('h1 >> text="Device management"')).toBeVisible();
});
});
test.describe('Users', () => {
let poAdminUsers: AdminUsers;
test.beforeEach(async ({ page }) => {
poAdminUsers = new AdminUsers(page);
await poAdminUsers.goto();
});
test('expect find "user1" user', async () => {
await poAdminUsers.searchUser('user1');
});
test('expect create a user', async () => {
await poAdminUsers.btnNewUser.click();
await poAdminUsers.editUser.inputEmail.fill(faker.internet.email());
await poAdminUsers.editUser.inputName.fill(faker.person.firstName());
await poAdminUsers.editUser.inputUserName.fill(faker.internet.userName());
await poAdminUsers.editUser.inputSetManually.click();
await poAdminUsers.editUser.inputPassword.fill('P@ssw0rd1234.!');
await poAdminUsers.editUser.inputConfirmPassword.fill('P@ssw0rd1234.!');
await expect(poAdminUsers.editUser.chipUserRole).toBeVisible();
await poAdminUsers.editUser.btnAddUser.click();
});
test('expect SMTP setup warning and routing to email settings', async ({ page }) => {
await poAdminUsers.btnInvite.click();
await poAdminUsers.editUser.setupSmtpLink.click();
await expect(page).toHaveURL('/admin/settings/Email');
});
test('expect to show join default channels option only when creating new users, not when editing users', async () => {
const username = faker.internet.userName();
await poAdminUsers.btnNewUser.click();
await poAdminUsers.editUser.inputName.fill(faker.person.firstName());
await poAdminUsers.editUser.inputUserName.fill(username);
await poAdminUsers.editUser.inputEmail.fill(faker.internet.email());
await poAdminUsers.editUser.inputSetManually.click();
await poAdminUsers.editUser.inputPassword.fill('P@ssw0rd1234.!');
await poAdminUsers.editUser.inputConfirmPassword.fill('P@ssw0rd1234.!');
await expect(poAdminUsers.editUser.chipUserRole).toBeVisible();
await expect(poAdminUsers.editUser.joinDefaultChannels).toBeVisible();
await poAdminUsers.editUser.btnAddUser.click();
await poAdminUsers.searchUser(username);
await poAdminUsers.getUserRowByUsername(username).click();
await poAdminUsers.userInfo.btnEdit.click();
await expect(poAdminUsers.editUser.inputUserName).toHaveValue(username);
await expect(poAdminUsers.editUser.joinDefaultChannels).not.toBeVisible();
});
test.describe('Delete user', () => {
const nonEmptyChannelName = faker.string.uuid();
const emptyChannelName = faker.string.uuid();
let ownerUser: IUser;
let user: IUser;
let poToastMessage: ToastMessages;
test.beforeEach(({ page }) => {
poToastMessage = new ToastMessages(page);
});
test.beforeAll(async ({ api }) => {
const createUserResponse = await api.post('/users.create', {
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.internet.password(),
username: faker.internet.userName(),
});
user = (await createUserResponse.json()).user;
const createOwnerUserResponse = await api.post('/users.create', {
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.internet.password(),
username: faker.internet.userName(),
});
ownerUser = (await createOwnerUserResponse.json()).user;
// TODO: refactor createChannel utility in order to get channel data when creating
const response = await api.post('/channels.create', { name: nonEmptyChannelName, members: [ownerUser.username] });
const { channel: nonEmptyChannel } = await response.json();
await api.post('/channels.addOwner', { roomId: nonEmptyChannel._id, username: ownerUser.username });
await api.post('/channels.removeOwner', { roomId: nonEmptyChannel._id, userId: Users.admin.data._id });
// TODO: refactor createChannel utility in order to get channel data when creating
const res = await api.post('/groups.create', { name: emptyChannelName, members: [ownerUser.username] });
const { group: emptyRoom } = await res.json();
await api.post('/groups.addOwner', { roomId: emptyRoom._id, username: ownerUser.username });
await api.post('/groups.leave', { roomId: emptyRoom._id });
});
test('expect to show owner change modal, when deleting last owner of any room', async ({ page }) => {
await poAdminUsers.deleteUser(ownerUser.username);
await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toBeVisible();
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toContainText(
`A new owner will be assigned automatically to the ${nonEmptyChannelName} room.`,
);
await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toContainText(
`The empty room ${emptyChannelName} will be removed automatically.`,
);
await expect(page.getByRole('dialog').getByRole('button', { name: 'Delete' })).toBeVisible();
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
await poToastMessage.waitForDisplay();
await expect(page.getByRole('heading', { name: 'No users' })).toBeVisible();
});
test('expect to delete user', async ({ page }) => {
await poAdminUsers.deleteUser(user.username);
await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toBeVisible();
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
await poToastMessage.waitForDisplay();
await expect(page.getByRole('heading', { name: 'No users' })).toBeVisible();
});
});
});
// TODO: Move this suit to admin-rooms.spec.ts
test.describe('Rooms', () => {
let poAdminRooms: AdminRooms;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poAdminRooms = new AdminRooms(page);
await poAdminRooms.goto();
});
test('should find "general" channel', async ({ page }) => {
await poAdminRooms.inputSearchRooms.type('general');
await page.waitForSelector('[qa-room-id="GENERAL"]');
});
test('should edit target channel name', async () => {
await poAdminRooms.inputSearchRooms.fill(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.roomNameInput.fill(`${targetChannel}-edited`);
await poAdminRooms.editRoom.btnSave.click();
await expect(poAdminRooms.getRoomRow(targetChannel)).toContainText(`${targetChannel}-edited`);
targetChannel = `${targetChannel}-edited`;
});
test('should edit target channel type', async () => {
await poAdminRooms.inputSearchRooms.type(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.privateLabel.click();
await poAdminRooms.editRoom.btnSave.click();
await expect(poAdminRooms.getRoomRow(targetChannel)).toContainText('Private Channel');
});
test('should archive target channel', async () => {
await poAdminRooms.inputSearchRooms.type(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.archivedLabel.click();
await poAdminRooms.editRoom.btnSave.click();
await poAdminRooms.editRoom.waitForDismissal();
await poAdminRooms.getRoomRow(targetChannel).click();
await expect(poAdminRooms.editRoom.archivedInput).toBeChecked();
});
test.describe.serial('Default rooms', () => {
test('expect target channel to be default', async () => {
await poAdminRooms.inputSearchRooms.type(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.defaultLabel.click();
await test.step('should close contextualbar after saving', async () => {
await poAdminRooms.editRoom.btnSave.click();
await poAdminRooms.editRoom.waitForDismissal();
});
await poAdminRooms.getRoomRow(targetChannel).click();
await expect(poAdminRooms.editRoom.defaultInput).toBeChecked();
});
test('should mark target default channel as "favorite by default"', async () => {
await poAdminRooms.inputSearchRooms.fill(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.favoriteLabel.click();
await poAdminRooms.editRoom.btnSave.click();
await expect(poAdminRooms.editRoom.btnSave).not.toBeVisible();
await poAdminRooms.getRoomRow(targetChannel).click();
await expect(poAdminRooms.editRoom.favoriteInput).toBeChecked();
});
test('should see favorite switch disabled when default is not true', async () => {
await poAdminRooms.inputSearchRooms.fill(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await poAdminRooms.editRoom.defaultLabel.click();
await expect(poAdminRooms.editRoom.favoriteInput).toBeDisabled();
});
test('should see favorite switch enabled when default is true', async () => {
await poAdminRooms.inputSearchRooms.type(targetChannel);
await poAdminRooms.getRoomRow(targetChannel).click();
await expect(poAdminRooms.editRoom.favoriteInput).toBeEnabled();
});
});
});
test.describe('Permissions', () => {
let poAdminPermissions: AdminPermissions;
test.beforeEach(async ({ page }) => {
poAdminPermissions = new AdminPermissions(page);
await poAdminPermissions.goto();
});
test('expect open upsell modal if not enterprise', async ({ page }) => {
test.skip(IS_EE);
await poAdminPermissions.btnCreateRole.click();
await expect(page.getByRole('dialog', { name: 'Custom roles' })).toBeVisible();
});
test.describe('Users in role', () => {
const channelName = faker.string.uuid();
test.beforeAll(async ({ api }) => {
// TODO: refactor createChannel utility in order to get channel data when creating
const response = await api.post('/channels.create', { name: channelName, members: ['user1'] });
const { channel } = await response.json();
await api.post('/channels.addOwner', { roomId: channel._id, userId: Users.user1.data._id });
await api.post('/channels.removeOwner', { roomId: channel._id, userId: Users.admin.data._id });
});
test('admin should be able to get the owners of a room that wasnt created by him', async ({ page }) => {
await poAdminPermissions.openRoleByName('Owner').click();
await poAdminPermissions.btnUsersInRole.click();
await poAdminPermissions.inputRoom.fill(channelName);
await page.getByRole('option', { name: channelName }).click();
await expect(poAdminPermissions.getUserInRoleRowByUsername('user1')).toBeVisible();
});
test('should add user1 as moderator of target channel', async ({ page }) => {
await poAdminPermissions.openRoleByName('Moderator').click();
await poAdminPermissions.btnUsersInRole.click();
await poAdminPermissions.inputRoom.fill(channelName);
await page.getByRole('option', { name: channelName }).click();
await poAdminPermissions.inputUsers.pressSequentially('user1');
await page.getByRole('option', { name: 'user1' }).click();
await poAdminPermissions.btnAdd.click();
await expect(poAdminPermissions.getUserInRoleRowByUsername('user1')).toBeVisible();
});
test('should remove user1 as moderator of target channel', async ({ page }) => {
await poAdminPermissions.openRoleByName('Moderator').click();
await poAdminPermissions.btnUsersInRole.click();
await poAdminPermissions.inputRoom.fill(channelName);
await page.getByRole('option', { name: channelName }).click();
await poAdminPermissions.removeUserFromRoleByUsername('user1');
await expect(page.locator('h3 >> text="No results found"')).toBeVisible();
});
test('should back to the permissions page', async ({ page }) => {
await poAdminPermissions.openRoleByName('Moderator').click();
await poAdminPermissions.btnUsersInRole.click();
await poAdminPermissions.btnBack.click();
await expect(page.locator('h1 >> text="Permissions"')).toBeVisible();
});
});
});
test.describe('Mailer', () => {
test.beforeEach(async ({ page }) => {
await new AdminMailer(page).goto();
});
test('should not have any accessibility violations', async ({ makeAxeBuilder }) => {
const results = await makeAxeBuilder().analyze();
expect(results.violations).toEqual([]);
});
});
test.describe.serial('Third party login', () => {
let poAdminThirdPartyLogin: AdminThirdPartyLogin;
const appName = faker.string.uuid();
const appRedirectURI = faker.internet.url();
test.beforeEach(async ({ page }) => {
poAdminThirdPartyLogin = new AdminThirdPartyLogin(page);
await poAdminThirdPartyLogin.goto();
});
test('should show Third-party login page', async ({ page }) => {
await expect(page.locator('h1 >> text="Third-party login"')).toBeVisible();
});
test('should not be able to create a new application without application name', async ({ page }) => {
await poAdminThirdPartyLogin.btnNewApplication.click();
await poAdminThirdPartyLogin.inputRedirectURI.fill(appRedirectURI);
await poAdminThirdPartyLogin.btnSave.click();
await expect(page.getByText('Name required')).toBeVisible();
});
test('should not be able to create a new application without redirect URI', async ({ page }) => {
await poAdminThirdPartyLogin.btnNewApplication.click();
await poAdminThirdPartyLogin.inputApplicationName.fill(appName);
await poAdminThirdPartyLogin.btnSave.click();
await expect(page.getByText('Redirect URI required')).toBeVisible();
});
test('should be able to create a new application', async ({ page }) => {
await poAdminThirdPartyLogin.btnNewApplication.click();
await poAdminThirdPartyLogin.inputApplicationName.fill(appName);
await poAdminThirdPartyLogin.inputRedirectURI.fill(appRedirectURI);
await poAdminThirdPartyLogin.btnSave.click();
await expect(poAdminThirdPartyLogin.getThirdPartyAppByName(appName)).toBeVisible();
await expect(page.getByText('Application added')).toBeVisible();
});
test('should be able see aplication fields', async () => {
await poAdminThirdPartyLogin.getThirdPartyAppByName(appName).click();
await expect(poAdminThirdPartyLogin.inputApplicationName).toBeVisible();
await expect(poAdminThirdPartyLogin.inputRedirectURI).toBeVisible();
await expect(poAdminThirdPartyLogin.inputClientId).toBeVisible();
await expect(poAdminThirdPartyLogin.inputClientSecret).toBeVisible();
await expect(poAdminThirdPartyLogin.inputAuthUrl).toBeVisible();
await expect(poAdminThirdPartyLogin.inputTokenUrl).toBeVisible();
});
test('should be able to delete an application', async ({ page }) => {
await poAdminThirdPartyLogin.deleteThirdPartyAppByName(appName);
await expect(poAdminThirdPartyLogin.getThirdPartyAppByName(appName)).not.toBeVisible();
await expect(page.getByText('Your entry has been deleted.')).toBeVisible();
});
});
test.describe('Integrations', () => {
let poAdminIntegrations: AdminIntegrations;
const messageCodeHighlightDefault =
'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim';
const incomingIntegrationName = faker.string.uuid();
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'Message_Code_highlight', '');
});
test.beforeEach(async ({ page }) => {
poAdminIntegrations = new AdminIntegrations(page);
await poAdminIntegrations.goto();
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'Message_Code_highlight', messageCodeHighlightDefault);
});
test('should display the example payload correctly', async () => {
await poAdminIntegrations.btnNew.click();
await poAdminIntegrations.btnInstructions.click();
await expect(poAdminIntegrations.codeExamplePayload('Loading')).not.toBeVisible();
});
test('should be able to create new incoming integration', async () => {
await poAdminIntegrations.btnNew.click();
await poAdminIntegrations.inputName.fill(incomingIntegrationName);
await poAdminIntegrations.inputPostToChannel.fill('#general');
await poAdminIntegrations.inputPostAs.fill('rocket.cat');
await poAdminIntegrations.btnSave.click();
await expect(poAdminIntegrations.inputWebhookUrl).not.toHaveValue('Will be available here after saving.');
await poAdminIntegrations.btnBack.click();
await expect(poAdminIntegrations.getIntegrationByName(incomingIntegrationName)).toBeVisible();
});
test('should be able to delete an incoming integration', async () => {
await poAdminIntegrations.deleteIntegrationByName(incomingIntegrationName);
await expect(poAdminIntegrations.getIntegrationByName(incomingIntegrationName)).not.toBeVisible();
});
});
});

View File

@@ -0,0 +1,46 @@
import { faker } from '@faker-js/faker';
import { HomeChannel, Registration } from './page-objects';
import { setSettingValueById } from './utils/setSettingValueById';
import { expect, test } from './utils/test';
test.describe('anonymous-user', () => {
let poHomeChannel: HomeChannel;
test.beforeAll(async ({ api }) => {
await expect((await setSettingValueById(api, 'Accounts_AllowAnonymousRead', true)).status()).toBe(200);
await expect((await setSettingValueById(api, 'Accounts_AllowAnonymousWrite', true)).status()).toBe(200);
});
test.afterAll(async ({ api }) => {
await expect((await setSettingValueById(api, 'Accounts_AllowAnonymousRead', false)).status()).toBe(200);
await expect((await setSettingValueById(api, 'Accounts_AllowAnonymousWrite', false)).status()).toBe(200);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
await poHomeChannel.navbar.openChat('general');
});
test('expect to go to the login page as anonymous user', async ({ page }) => {
await poHomeChannel.content.btnAnonymousSignIn.click();
await expect(page.locator('role=form')).toBeVisible();
});
test('expect to chat as anonymous user', async ({ page }) => {
poHomeChannel = new HomeChannel(page);
const poRegistration = new Registration(page);
await poHomeChannel.content.btnAnonymousTalk.click();
await expect(poRegistration.username).toBeVisible();
await poRegistration.username.type(faker.internet.userName());
await poRegistration.btnRegisterConfirmUsername.click();
await poHomeChannel.content.sendMessage('hello world');
});
});

View File

@@ -0,0 +1,72 @@
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { createTargetChannel } from '../utils/create-target-channel';
import { test, expect } from '../utils/test';
test.use({ storageState: Users.admin.state });
// TODO: Remove the skip on this test once a fix in the apps engine is done
// For some reason the app doesn't work properly when installed via insertApp method
test.describe.skip('app-surfaces-interaction', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test('expect to submit an success modal', async ({ page }) => {
await page.locator('role=button[name="Options"]').click();
await page.locator('[data-key="success"]').click();
await page.locator('role=button[name="success"]').click();
const updatedButton = page.locator('role=button[name="success"]');
await expect(updatedButton).not.toBeVisible();
});
test('expect to not close the modal and there is an error in the modal', async ({ page }) => {
await page.locator('role=button[name="Options"]').click();
await page.locator('[data-key="error"]').click();
await page.locator('role=button[name="error"]').click();
const updatedTitle = page.locator('role=button[name="error"]');
expect(updatedTitle).toBeDefined();
const input = page.locator('input[type="text"]');
await input.click();
await input.fill('fixed');
await page.locator('role=button[name="error"]').click();
await expect(input).not.toBeVisible();
});
test('expect to show the toaster error for modal that timeout the execution', async ({ page }) => {
await page.locator('role=button[name="Options"]').click();
await page.locator('[data-key="timeout"]').click();
await page.locator('role=button[name="timeout"]').click();
await page.locator('role=alert').waitFor();
const toaster = page.locator('role=alert');
await expect(toaster).toBeVisible();
});
test('expect change the modal and then submit the updated modal', async ({ page }) => {
await page.locator('role=button[name="Options"]').click();
await page.locator('[data-key="update"]').click();
await page.locator('role=button[name="update"]').click();
const updatedTitle = page.locator('role=button[name="title updated"]');
expect(updatedTitle).toBeDefined();
const updatedButton = page.locator('role=button[name="updated"]');
expect(updatedButton).toBeDefined();
});
});

View File

@@ -0,0 +1,36 @@
import type { Page } from '@playwright/test';
import { IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { expect, test } from '../utils/test';
test.use({ storageState: Users.user1.state });
test.describe.serial('Apps > ContextualBar', () => {
test.skip(!IS_EE, 'Premium Only');
let poHomeChannel: HomeChannel;
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel('general');
});
test.afterAll(async () => {
await page.close();
});
test('expect allow user open app contextualbar', async () => {
await poHomeChannel.content.dispatchSlashCommand('/contextualbar');
await expect(poHomeChannel.btnContextualbarClose).toBeVisible();
});
test('expect app contextualbar to be closed', async () => {
await poHomeChannel.btnContextualbarClose.click();
await expect(poHomeChannel.btnContextualbarClose).toBeHidden();
});
});

View File

@@ -0,0 +1,44 @@
import type { Page } from '@playwright/test';
import { IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { AppsModal } from '../page-objects/fragments/modals';
import { expect, test } from '../utils/test';
test.use({ storageState: Users.user1.state });
test.describe.serial('Apps > Modal', () => {
test.skip(!IS_EE, 'Premium Only');
let poHomeChannel: HomeChannel;
let poModal: AppsModal;
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
poHomeChannel = new HomeChannel(page);
poModal = new AppsModal(page);
await poHomeChannel.gotoChannel('general');
});
test.afterAll(async () => {
await page.close();
});
test('should allow user open app modal', async () => {
await poHomeChannel.content.dispatchSlashCommand('/modal');
await poModal.waitForDisplay();
});
test('should display validation error message in app modal', async () => {
await poModal.btnSubmit.click();
await expect(poModal.textInputErrorMessage).toBeVisible();
});
test('should not display validation error message in app modal', async () => {
await poModal.submit('something');
});
});

View File

@@ -0,0 +1,116 @@
import { IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { Marketplace } from '../page-objects';
import { expect, test } from '../utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('Private apps upload', () => {
let poMarketplace: Marketplace;
test.beforeEach(async ({ page }) => {
poMarketplace = new Marketplace(page);
await poMarketplace.goto();
});
test.describe('Premium', () => {
test.skip(!IS_EE, 'Premium Only');
test('expect to allow admin to upload a private app in EE, which should be enabled by default', async ({ page }) => {
const fileChooserPromise = page.waitForEvent('filechooser');
await poMarketplace.btnUploadPrivateApp.click();
await expect(poMarketplace.btnInstallPrivateApp).toBeDisabled();
await poMarketplace.btnUploadPrivateAppFile.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('./tests/e2e/fixtures/files/test-app_0.0.1.zip');
await expect(poMarketplace.btnInstallPrivateApp).toBeEnabled();
await poMarketplace.btnInstallPrivateApp.click();
await page.getByRole('button', { name: 'Agree' }).click();
await expect(poMarketplace.appStatusTag).toHaveText('Enabled');
});
test('expect to allow admin to update a enabled private app in EE, which should remain enabled', async ({ page }) => {
const fileChooserPromise = page.waitForEvent('filechooser');
await poMarketplace.btnUploadPrivateApp.click();
await expect(poMarketplace.btnInstallPrivateApp).toBeDisabled();
await poMarketplace.btnUploadPrivateAppFile.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('./tests/e2e/fixtures/files/test-app_0.0.1.zip');
await expect(poMarketplace.btnInstallPrivateApp).toBeEnabled();
await poMarketplace.btnInstallPrivateApp.click();
await poMarketplace.btnConfirmAppUpdate.click();
await page.getByRole('button', { name: 'Agree' }).click();
await poMarketplace.goto();
await poMarketplace.lastAppRow.click();
await expect(poMarketplace.appStatusTag).toHaveText('Enabled');
});
test('expect to allow disabling a recently installed private app in EE', async () => {
await poMarketplace.lastAppRow.click();
await expect(poMarketplace.appStatusTag).toHaveText('Enabled');
await poMarketplace.appMenu.click();
await expect(poMarketplace.btnDisableApp).toBeEnabled();
await poMarketplace.btnDisableApp.click();
await poMarketplace.btnConfirmAppUpdate.click();
await expect(poMarketplace.appStatusTag).toHaveText('Disabled');
});
test('expect to allow admin to update a disabled private app in EE, which should remain disabled', async ({ page }) => {
const fileChooserPromise = page.waitForEvent('filechooser');
await poMarketplace.btnUploadPrivateApp.click();
await expect(poMarketplace.btnInstallPrivateApp).toBeDisabled();
await poMarketplace.btnUploadPrivateAppFile.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('./tests/e2e/fixtures/files/test-app_0.0.1.zip');
await expect(poMarketplace.btnInstallPrivateApp).toBeEnabled();
await poMarketplace.btnInstallPrivateApp.click();
await poMarketplace.btnConfirmAppUpdate.click();
await page.getByRole('button', { name: 'Agree' }).click();
await poMarketplace.goto();
await poMarketplace.lastAppRow.click();
await expect(poMarketplace.appStatusTag).toHaveText('Disabled');
});
});
test.describe('Community Edition', () => {
test.skip(IS_EE, 'CE Only');
test('expect to allow admin to upload a private app in CE, but it should be disabled by default', async ({ page }) => {
const fileChooserPromise = page.waitForEvent('filechooser');
await poMarketplace.btnUploadPrivateApp.click();
await expect(poMarketplace.btnConfirmAppUploadModal).toBeEnabled();
await poMarketplace.btnConfirmAppUploadModal.click();
await expect(poMarketplace.btnInstallPrivateApp).toBeDisabled();
await poMarketplace.btnUploadPrivateAppFile.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('./tests/e2e/fixtures/files/test-app_0.0.1.zip');
await expect(poMarketplace.btnInstallPrivateApp).toBeEnabled();
await poMarketplace.btnInstallPrivateApp.click();
await page.getByRole('button', { name: 'Agree' }).click();
await expect(poMarketplace.appStatusTag).toHaveText('Disabled');
});
test('expect not to allow enabling a recently installed private app in CE', async () => {
await poMarketplace.lastAppRow.click();
await expect(poMarketplace.appStatusTag).toHaveText('Disabled');
await poMarketplace.appMenu.click();
await expect(poMarketplace.btnEnableApp).toBeDisabled();
});
});
});

View File

@@ -0,0 +1,288 @@
import { appUiKitRoomTest } from '../../data/apps/app-packages';
import { IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { getAppLogs, installLocalTestPackage, uninstallApp } from '../utils/apps';
import { expect, test } from '../utils/test';
test.use({ storageState: Users.user1.state });
test.describe.serial('Apps > UIKit interactions data', () => {
test.skip(!IS_EE, 'Premium Only');
let poHomeChannel: HomeChannel;
let appId: string;
test.beforeAll(async () => {
const result = await installLocalTestPackage(appUiKitRoomTest);
appId = result.app.id;
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel('general');
});
test.afterAll(async () => {
await uninstallApp(appId);
});
/**
* Finds a log entry matching a handler method and a specific debug label.
* The app logs using `this.getLogger().debug(label, value)`, creating entries with args = [label, value].
* Each handler invocation creates a log group with `method` like `app:executeBlockActionHandler`.
*/
function findLogItem(
logs: Awaited<ReturnType<typeof getAppLogs>>['logs'],
methodFragment: string,
[arg0, arg1]: [arg0: string, arg1?: string],
) {
return logs.find(
(log) =>
log.method.includes(methodFragment) && log.entries.some((entry) => arg0 === entry.args[0] && (!arg1 || arg1 === entry.args[1])),
);
}
test('should include correct data in executeBlockActionHandler when triggered in a message', async ({ api, page }) => {
const seed = Date.now().toString();
// Send a message with a button via the slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal message ${seed}`);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the message with the button to appear and click it
await page.getByRole('button', { name: 'Click!' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
const blockActionLog = findLogItem(logsResult.logs, 'executeBlockActionHandler', ['block_action_value', seed]);
expect(blockActionLog, 'Block action handler log not found for message').toBeTruthy();
// Verify room is present (GENERAL room)
const roomEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_room');
expect(roomEntry?.args[1], 'Room id should be present for message block action').toBe('GENERAL');
// Verify user is present
const userEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_user');
expect(userEntry?.args[1], 'User should be present for message block action').toBe('user1');
// Verify triggerId is present
const triggerEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_triggerId');
expect(triggerEntry?.args[1], 'TriggerId should be present').not.toBe('no-triggerId');
// Verify actionId is correct
const actionIdEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_actionId');
expect(actionIdEntry?.args[1], 'ActionId should be msg-button').toBe('msg-button');
// Verify container type
const containerEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_container');
expect(containerEntry?.args[1], 'Container type should be message').toBe('message');
});
test('should include correct data in executeBlockActionHandler when triggered in a contextual bar surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a contextual bar via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal ctx ${seed}`);
// Opening a contextual bar via the Apps Engine causes a client-side URL navigation
// (e.g. /channel/general/app/{viewId}). Wait for that navigation to complete before
// using any locators, otherwise Playwright throws "Target page, context or browser
// has been closed" while the navigation is still in progress.
await page.waitForURL(/\/app\//);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the contextual bar to appear and click the button
const surface = page.getByRole('dialog', { name: 'UIKit Room Test Contextual Bar' });
await surface.getByRole('button', { name: 'Click!' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
// Find the most recent block action log with ctx-button actionId
const blockActionLog = findLogItem(logsResult.logs, 'executeBlockActionHandler', ['block_action_value', seed]);
expect(blockActionLog, 'Block action handler log not found for contextual bar').toBeTruthy();
// Verify room is present
const roomEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_room');
expect(roomEntry?.args[1], 'Room id should be present for contextual bar block action').toBe('GENERAL');
// Verify user is present
const userEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_user');
expect(userEntry?.args[1], 'User should be present for contextual bar block action').toBe('user1');
// Verify triggerId is present
const triggerEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_triggerId');
expect(triggerEntry?.args[1], 'TriggerId should be present').not.toBe('no-triggerId');
// Close the contextual bar
await surface.getByRole('button', { name: 'Close' }).click();
});
test('should include correct data in executeBlockActionHandler when triggered in a modal surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a modal via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal modal ${seed}`);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the modal to appear and click the button
const surface = page.getByRole('dialog', { name: 'UIKit Room Test Modal' });
await surface.getByRole('button', { name: 'Click!' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
// Find the most recent block action log with modal-button actionId
const blockActionLog = findLogItem(logsResult.logs, 'executeBlockActionHandler', ['block_action_value', seed]);
expect(blockActionLog, 'Block action handler log not found for modal').toBeTruthy();
// Verify user is present
const userEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_user');
expect(userEntry?.args[1], 'User should be present for modal block action').toBe('user1');
// Verify container type is view (modal)
const containerEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_container');
expect(containerEntry?.args[1], 'Container type should be view for modal').toBe('view');
await test.step('room is intentionally absent for modal block actions (modals have no room context)', async () => {
const roomEntry = blockActionLog?.entries.find((e) => e.args[0] === 'block_action_room');
expect(roomEntry?.args[1], 'Room entry should not be present for modal block action').toBe('no-room');
});
// Close the modal for the next test
await surface.getByRole('button', { name: 'Close' }).click();
});
test('should include correct data in executeViewSubmitHandler when triggered in a modal surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a modal via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal modal ${seed}`);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the modal and submit it
await page.getByLabel('UIKit Room Test Modal').getByRole('button', { name: 'Submit' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
const viewSubmitLog = findLogItem(logsResult.logs, 'executeViewSubmitHandler', ['view_submit_id', `modal-${seed}`]);
expect(viewSubmitLog, 'View submit handler log not found for modal').toBeTruthy();
// Verify user is present
const userEntry = viewSubmitLog?.entries.find((e) => e.args[0] === 'view_submit_user');
expect(userEntry?.args[1], 'User should be present for modal view submit').toBe('user1');
// Verify triggerId is present
const triggerEntry = viewSubmitLog?.entries.find((e) => e.args[0] === 'view_submit_triggerId');
expect(triggerEntry?.args[1], 'TriggerId should be present').not.toBe('no-triggerId');
});
test('should include correct data in executeViewSubmitHandler when triggered in a contextual bar surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a contextual bar via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal ctx ${seed}`);
// Wait for the client-side navigation to the contextual bar URL to complete
await page.waitForURL(/\/app\//);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the contextual bar and submit it
await page.getByLabel('UIKit Room Test Contextual Bar').getByRole('button', { name: 'Submit' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
// Find the most recent view submit log
const viewSubmitLog = findLogItem(logsResult.logs, 'executeViewSubmitHandler', ['view_submit_id', `ctx-${seed}`]);
expect(viewSubmitLog, 'View submit handler log not found for contextual bar').toBeTruthy();
// Verify room is present
const roomEntry = viewSubmitLog?.entries.find((e) => e.args[0] === 'view_submit_room');
expect(roomEntry?.args[1], 'Room id should be present for contextual bar view submit').toBe('GENERAL');
// Verify user is present
const userEntry = viewSubmitLog?.entries.find((e) => e.args[0] === 'view_submit_user');
expect(userEntry?.args[1], 'User should be present for contextual bar view submit').toBe('user1');
});
test('should include correct data in executeViewClosedHandler when triggered in a modal surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a modal via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal modal ${seed}`);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the modal and close it (via X button, not submit)
await page.getByLabel('UIKit Room Test Modal').getByRole('button', { name: 'Close' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
const viewClosedLog = findLogItem(logsResult.logs, 'executeViewClosedHandler', ['view_closed_id', `modal-${seed}`]);
expect(viewClosedLog, 'View closed handler log not found for modal').toBeTruthy();
// Verify user is present
const userEntry = viewClosedLog?.entries.find((e) => e.args[0] === 'view_closed_user');
expect(userEntry?.args[1], 'User should be present for modal view closed').toBe('user1');
});
test('should include correct data in executeViewClosedHandler when triggered in a contextual bar surface', async ({ api, page }) => {
const seed = Date.now().toString();
// Open a contextual bar via slash command
await poHomeChannel.content.dispatchSlashCommand(`/open-uikit-room-test-modal ctx ${seed}`);
// Wait for the client-side navigation to the contextual bar URL to complete
await page.waitForURL(/\/app\//);
const interactionResponse = page.waitForResponse('**/ui.interaction/*');
// Wait for the contextual bar to appear and close it
await page.getByLabel('UIKit Room Test Contextual Bar').getByRole('button', { name: 'Close' }).click();
await interactionResponse;
// Fetch app logs and validate
const logsResult = await getAppLogs(api, appId);
expect(logsResult.logs).toBeDefined();
// Find the most recent view closed log
const viewClosedLog = findLogItem(logsResult.logs, 'executeViewClosedHandler', ['view_closed_id', `ctx-${seed}`]);
expect(viewClosedLog, 'View closed handler log not found for contextual bar').toBeTruthy();
// Verify room is present
const roomEntry = viewClosedLog?.entries.find((e) => e.args[0] === 'view_closed_room');
expect(roomEntry?.args[1], 'Room id should be present for contextual bar view closed').toBe('GENERAL');
// Verify user is present
const userEntry = viewClosedLog?.entries.find((e) => e.args[0] === 'view_closed_user');
expect(userEntry?.args[1], 'User should be present for contextual bar view closed').toBe('user1');
});
});

View File

@@ -0,0 +1,60 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannelAndReturnFullRoom, deleteChannel, sendTargetChannelMessage } from './utils';
import { preserveSettings } from './utils/preserveSettings';
import { seedTranslatedMessage } from './utils/seedTranslatedMessage';
import { setSettingValueById } from './utils/setSettingValueById';
import { expect, test } from './utils/test';
test.use({ storageState: Users.admin.state });
const ORIGINAL_MESSAGE = 'Good morning everyone';
const TRANSLATED_MESSAGE = 'Guten Morgen zusammen';
test.describe.serial('auto-translate', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
preserveSettings(['AutoTranslate_Enabled']);
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'AutoTranslate_Enabled', true);
const { channel } = await createTargetChannelAndReturnFullRoom(api);
targetChannel = channel.name as string;
await sendTargetChannelMessage(api, targetChannel, { msg: ORIGINAL_MESSAGE });
await seedTranslatedMessage({
rid: channel._id,
msg: ORIGINAL_MESSAGE,
author: Users.user1,
translations: { de: TRANSLATED_MESSAGE },
});
await api.post('/autotranslate.saveSettings', { roomId: channel._id, field: 'autoTranslateLanguage', value: 'de' });
});
test.afterAll(async ({ api }) => {
await deleteChannel(api, targetChannel);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
});
test('should render the translated message once the user enables automatic translation', async () => {
await poHomeChannel.gotoChannel(targetChannel);
await expect(poHomeChannel.content.lastUserMessage).toContainText(ORIGINAL_MESSAGE);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemAutoTranslate.click();
await poHomeChannel.tabs.autoTranslate.waitForDisplay();
await poHomeChannel.tabs.autoTranslate.setAutomaticTranslation(true);
await poHomeChannel.toastMessage.waitForDisplay({ type: 'success' });
await expect(poHomeChannel.content.lastUserMessage).toContainText(TRANSLATED_MESSAGE);
await expect(poHomeChannel.content.lastUserMessage).not.toContainText(ORIGINAL_MESSAGE);
});
});

View File

@@ -0,0 +1,98 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel, createTargetPrivateChannel, createDirectMessage } from './utils';
import { setSettingValueById } from './utils/setSettingValueById';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
const testAvatars = (homeChannel: HomeChannel, channel: string, url: string) => {
test('expect sidebar avatar to have provider prefix', async () => {
expect(homeChannel.sidebar.getSidebarItemByName(channel).locator('img').getAttribute('src')).toBe(url);
});
test('expect channel header avatar to have provider prefix', async () => {
await homeChannel.navbar.openChat(channel);
expect(homeChannel.content.channelHeader.locator('img').getAttribute('src')).toBe(url);
});
test('expect channel info avatar to have provider prefix', async () => {
await homeChannel.navbar.openChat(channel);
expect(homeChannel.content.channelHeader.locator('img').getAttribute('src')).toBe(url);
});
};
test.describe('avatar-settings', () => {
let poHomeChannel: HomeChannel;
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
test.describe('external avatar provider', () => {
const providerUrlPrefix = 'https://example.com/avatar/';
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_RoomAvatarExternalUrl', `${providerUrlPrefix}{username}`);
await setSettingValueById(api, 'Accounts_AvatarExternalUrl', `${providerUrlPrefix}{username}`);
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_RoomAvatarExternalUrl', '');
await setSettingValueById(api, 'Accounts_AvatarExternalUrl', '');
});
test.describe('public channels', () => {
let channelName = '';
let avatarUrl = '';
test.beforeAll(async ({ api }) => {
channelName = await createTargetChannel(api);
avatarUrl = `${providerUrlPrefix}${channelName}`;
});
testAvatars(poHomeChannel, channelName, avatarUrl);
});
test.describe('private channels', () => {
let channelName = '';
let avatarUrl = '';
test.beforeAll(async ({ api }) => {
channelName = await createTargetPrivateChannel(api);
avatarUrl = `${providerUrlPrefix}${channelName}`;
});
testAvatars(poHomeChannel, channelName, avatarUrl);
});
test.describe('direct messages', () => {
const channelName = Users.user2.data.username;
const avatarUrl = `${providerUrlPrefix}${channelName}`;
test.beforeAll(async ({ api }) => {
await createDirectMessage(api);
// send a message as user 2
test.use({ storageState: Users.user2.state });
await poHomeChannel.navbar.openChat(Users.user1.data.username);
await poHomeChannel.content.sendMessage('hello world');
test.use({ storageState: Users.user1.state });
});
testAvatars(poHomeChannel, channelName, avatarUrl);
test('expect message avatar to have provider prefix', async () => {
await expect(poHomeChannel.content.lastUserMessage.locator('img')).toHaveAttribute('src', avatarUrl);
});
test('expect user card avatar to have provider prefix', async () => {
await poHomeChannel.content.lastUserMessage.locator('.rcx-message-header__name-container').click();
await expect(poHomeChannel.userCard.imgUserCard).toHaveAttribute('src', avatarUrl);
});
});
});
});

View File

@@ -0,0 +1,71 @@
import type { CalendarEventImportProps } from '@rocket.chat/rest-typings';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { test, expect, type BaseTest } from './utils/test';
import { links } from '../../client/lib/links';
test.use({ storageState: Users.admin.state });
// TODO: Make this test run on CI - it's slow and requires an open browser, run manually for now.
test.describe.skip('Calendar', () => {
test.beforeAll(async ({ api }) => {
expect((await api.post('/settings/Calendar_BusyStatus_Enabled', { value: true })).status()).toBe(200);
});
test.afterAll(async ({ api }) => {
expect((await api.post('/settings/Calendar_BusyStatus_Enabled', { value: false })).status()).toBe(200);
});
test.describe('Status changes', () => {
test.beforeEach(async ({ page, api }) => {
expect((await api.post('/users.setStatus', { status: 'away', message: '' })).status()).toBe(200);
await new HomeChannel(page).goto();
});
test('Should change user status back to away when there are overlapping imported events', async ({ page, api }) => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('away');
const event1 = await importCalendarEvent(api, { start: 1, end: 2 });
await importCalendarEvent(api, { start: 1.5, end: 2.5 });
const event3 = await importCalendarEvent(api, { start: 2.5, end: 3 });
await test.step('Should change status to busy', async () => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('busy', {
timeout: event1.endTime.getTime() - Date.now(),
});
});
await test.step('Should change status to away again', async () => {
await expect(page.getByRole('button', { name: 'User menu' }).getByRole('status')).toHaveAccessibleName('away', {
timeout: event3.endTime.getTime() - Date.now() + 1000,
});
});
});
});
});
async function importCalendarEvent(api: BaseTest['api'], { now = Date.now(), start = 2, end = 4 } = {}) {
const startTime = new Date(now + 1000 * 60 * start);
const endTime = new Date(now + 1000 * 60 * end);
const apiResponse = await api.post('/calendar-events.import', {
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
subject: 'Test appointment',
description: 'Test appointment description',
meetingUrl: links.rocketChat,
busy: true,
externalId: `test-${start}-${end}-${now}`,
} satisfies CalendarEventImportProps);
expect(apiResponse.status()).toBe(200);
const { id } = await apiResponse.json();
if (typeof id !== 'string') {
throw new Error(`Expected id to be a string, but got ${typeof id}`);
}
return { startTime, endTime, now, id };
}

View File

@@ -0,0 +1,355 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { IS_EE } from './config/constants';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { CreateNewDiscussionModal } from './page-objects/fragments';
import { createTargetChannel } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('channel-management', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
let discussionName: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
// TODO: this should be replaced by a unit test
test('should navigate on toolbar using arrow keys', async ({ page }) => {
const roomHeaderFavoriteBtn = poHomeChannel.getRoomHeaderFavoriteBtn(IS_EE);
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');
await roomHeaderFavoriteBtn.focus();
await expect(roomHeaderFavoriteBtn).toBeFocused();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
await expect(poHomeChannel.roomHeaderToolbar.getByRole('button', { name: 'Threads', exact: true })).toBeFocused();
});
// TODO: this should be replaced by a unit test
test('should move the focus away from toolbar using tab key', async ({ page }) => {
const roomHeaderFavoriteBtn = poHomeChannel.getRoomHeaderFavoriteBtn(IS_EE);
await poHomeChannel.navbar.openChat(targetChannel);
await roomHeaderFavoriteBtn.focus();
await expect(roomHeaderFavoriteBtn).toBeFocused();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await expect(poHomeChannel.roomHeaderToolbar.getByRole('button', { name: 'Call' })).not.toBeFocused();
});
test('should add user1 to targetChannel', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.addUser('user1');
await expect(poHomeChannel.tabs.members.memberOption('user1')).toBeVisible();
await expect(poHomeChannel.content.getSystemMessageByText('added user1')).toBeVisible();
});
test('should edit topic of targetChannel', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openRoomInfo();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.editRoom.inputTopic.fill('hello-topic-edited');
await poHomeChannel.tabs.editRoom.btnSave.click();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.roomToolbar.openRoomInfo();
await expect(poHomeChannel.tabs.room.root).toContainText('hello-topic-edited');
await expect(poHomeChannel.content.getSystemMessageByText('changed room topic to hello-topic-edited')).toBeVisible();
});
test('should edit announcement of targetChannel', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openRoomInfo();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.editRoom.inputAnnouncement.fill('hello-announcement-edited');
await poHomeChannel.tabs.editRoom.btnSave.click();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.roomToolbar.openRoomInfo();
await expect(poHomeChannel.tabs.room.root).toContainText('hello-announcement-edited');
await expect(poHomeChannel.content.getSystemMessageByText('changed room announcement to: hello-announcement-edited')).toBeVisible();
});
test('should edit description of targetChannel', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openRoomInfo();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.editRoom.inputDescription.fill('hello-description-edited');
await poHomeChannel.tabs.editRoom.btnSave.click();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.roomToolbar.openRoomInfo();
await expect(poHomeChannel.tabs.room.root).toContainText('hello-description-edited');
await expect(poHomeChannel.content.getSystemMessageByText('changed room description to: hello-description-edited')).toBeVisible();
});
test('should edit name of targetChannel', async ({ page }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openRoomInfo();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.editRoom.inputName.fill(`NAME-EDITED-${targetChannel}`);
await poHomeChannel.tabs.editRoom.btnSave.click();
targetChannel = `NAME-EDITED-${targetChannel}`;
await expect(page.getByRole('main').getByRole('heading', { name: targetChannel })).toBeVisible();
await poHomeChannel.navbar.openChat(targetChannel);
await expect(page).toHaveURL(`/channel/${targetChannel}`);
});
test.skip('should truncate the room name for small screens', async ({ page }) => {
const hugeName = faker.string.alpha(200);
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openRoomInfo();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.editRoom.inputName.fill(hugeName);
await poHomeChannel.tabs.editRoom.btnSave.click();
targetChannel = hugeName;
await page.setViewportSize({ width: 640, height: 460 });
await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '407px');
});
test('should open sidebar clicking on sidebar toggler', async ({ page }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await page.setViewportSize({ width: 640, height: 460 });
await page.getByRole('button', { name: 'Open sidebar' }).click();
await expect(page.getByRole('navigation', { name: 'Sidebar' })).toBeVisible();
});
test('should open room info when clicking on roomName', async ({ page }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.getBtnOpenRoomInfo(targetChannel).focus();
await expect(poHomeChannel.getBtnOpenRoomInfo(targetChannel)).toBeFocused();
await page.keyboard.press('Space');
await page.getByRole('dialog').waitFor();
await expect(page.getByRole('dialog')).toBeVisible();
});
test('should create a discussion using the message composer', async ({ page }) => {
discussionName = faker.string.uuid();
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.composer.btnMenuMoreActions.click();
await page.getByRole('menuitem', { name: 'Discussion' }).click();
const createDiscussionModal = new CreateNewDiscussionModal(page);
await createDiscussionModal.inputName.fill(discussionName);
await createDiscussionModal.create();
await expect(page.getByRole('heading', { name: discussionName })).toBeVisible();
});
test('should access targetTeam through discussion header', async ({ page }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await page.getByRole('listitem', { name: discussionName }).getByRole('button', { name: 'Reply' }).click();
await page.getByRole('button', { name: `Back to ${targetChannel} channel`, exact: true }).focus();
await expect(page.getByRole('button', { name: `Back to ${targetChannel} channel`, exact: true })).toBeFocused();
await page.keyboard.press('Space');
await expect(page).toHaveURL(`/channel/${targetChannel}`);
});
test('should edit notification preferences of targetChannel', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemNotificationsPreferences.click();
await poHomeChannel.tabs.notificationPreferences.updateAllNotificationPreferences();
await poHomeChannel.tabs.notificationPreferences.btnSave.click();
await expect(poHomeChannel.tabs.notificationPreferences.getPreferenceByDevice('Desktop')).toContainText('Mentions');
await expect(poHomeChannel.tabs.notificationPreferences.getPreferenceByDevice('Mobile')).toContainText('Mentions');
await expect(poHomeChannel.tabs.notificationPreferences.getPreferenceByDevice('Email')).toContainText('Mentions');
});
test.describe.serial('cross user tests', () => {
let user1Page: Page;
test.beforeEach(async ({ browser }) => {
user1Page = await browser.newPage({ storageState: Users.user1.state });
});
test.afterEach(async () => {
await user1Page.close();
});
test('should mute user1', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.muteUser('user1');
await expect(poHomeChannel.content.getSystemMessageByText('muted user1')).toBeVisible();
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await expect(user1Channel.composer.readOnlyFooter).toBeVisible();
});
test('should unmuteUser user1', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.unmuteUser('user1');
await expect(poHomeChannel.content.getSystemMessageByText('unmuted user1')).toBeVisible();
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await expect(user1Channel.composer.inputMessage).toBeVisible();
});
test('should set user1 as moderator', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.setUserAsModerator('user1');
await expect(poHomeChannel.content.getSystemMessageByText('set user1 as moderator')).toBeVisible();
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await user1Channel.roomToolbar.openRoomInfo();
await expect(user1Channel.tabs.room.btnEdit).toBeVisible();
});
test('should set user1 as owner', async ({ browser }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.setUserAsOwner('user1');
await expect(poHomeChannel.content.getSystemMessageByText('set user1 as owner')).toBeVisible();
const user1Page = await browser.newPage({ storageState: Users.user1.state });
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await user1Channel.roomToolbar.openRoomInfo();
await user1Channel.tabs.room.btnMore.click();
await expect(user1Channel.tabs.room.optionDelete).toBeVisible();
await user1Page.close();
});
test('should ignore user1 messages', async ({ page }) => {
await poHomeChannel.navbar.openChat(targetChannel);
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.ignoreUser('user1');
await poHomeChannel.toastMessage.waitForDisplay({ type: 'success', message: 'User has been ignored' });
await poHomeChannel.tabs.members.userInfo.openMoreActions();
await expect(poHomeChannel.tabs.members.userInfo.menu.getMenuItem('Unignore')).toBeVisible();
await page.keyboard.press('Escape');
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.sendMessage('message to check ignore');
await expect(poHomeChannel.content.lastUserMessageBody.getByRole('button', { name: 'This message was ignored' })).toBeVisible();
await user1Channel.content.openReplyInThread();
await user1Channel.content.waitForThread();
await user1Channel.content.toggleAlsoSendThreadToChannel(false);
await user1Channel.content.sendMessageInThread('thread reply from an ignored user');
await poHomeChannel.content.openReplyInThread();
await poHomeChannel.content.waitForThread();
await expect(poHomeChannel.content.threadMessageListItems).toHaveCount(2);
await expect(poHomeChannel.content.ignoredThreadMessages).toHaveCount(2);
await expect(poHomeChannel.content.threadMessageList).not.toContainText('thread reply from an ignored user');
});
test('should unignore single user1 message', async () => {
await poHomeChannel.navbar.openChat(targetChannel);
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.sendMessage('only message to be unignored');
await poHomeChannel.navbar.openChat(targetChannel);
await expect(poHomeChannel.content.lastUserMessageBody).toContainText('This message was ignored');
await poHomeChannel.content.lastIgnoredUserMessage.click();
await expect(poHomeChannel.content.lastUserMessageBody).toContainText('only message to be unignored');
await user1Channel.content.openReplyInThread();
await user1Channel.content.waitForThread();
await user1Channel.content.toggleAlsoSendThreadToChannel(false);
await user1Channel.content.sendMessageInThread('thread reply that stays ignored');
await user1Channel.content.sendMessageInThread('thread reply to be revealed');
await poHomeChannel.content.openReplyInThread();
await poHomeChannel.content.waitForThread();
await expect(poHomeChannel.content.ignoredThreadMessages).toHaveCount(3);
await poHomeChannel.content.lastIgnoredThreadMessage.click();
await expect(poHomeChannel.content.lastUserThreadMessage).toContainText('thread reply to be revealed');
await expect(poHomeChannel.content.ignoredThreadMessages).toHaveCount(2);
});
test('should unignore user1 messages', async ({ page }) => {
const user1Channel = new HomeChannel(user1Page);
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.sendMessage('message before being unignored');
await poHomeChannel.navbar.openChat(targetChannel);
await expect(poHomeChannel.content.lastUserMessageBody).toContainText('This message was ignored');
await poHomeChannel.roomToolbar.openMembersTab();
await poHomeChannel.tabs.members.showAllUsers();
await poHomeChannel.tabs.members.unignoreUser('user1');
await poHomeChannel.tabs.members.userInfo.openMoreActions();
await expect(poHomeChannel.tabs.members.userInfo.menu.getMenuItem('Ignore')).toBeVisible();
await page.keyboard.press('Escape');
await user1Channel.content.sendMessage('message after being unignored');
await expect(poHomeChannel.content.nthMessage(-2)).toContainText('message before being unignored');
await expect(poHomeChannel.content.lastUserMessageBody).toContainText('message after being unignored');
});
test('should readOnlyChannel show join button', async () => {
const channelName = faker.string.uuid();
await poHomeChannel.navbar.createNew('Channel', channelName, { private: false, readOnly: true });
const channel = new HomeChannel(user1Page);
await channel.gotoChannel(channelName);
await expect(user1Page.getByRole('button', { name: 'Join', exact: true })).toBeVisible();
});
});
});

View File

@@ -0,0 +1,24 @@
export const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
export const API_PREFIX = '/api/v1';
export const BASE_API_URL = BASE_URL + API_PREFIX;
export const IS_LOCALHOST = BASE_URL.startsWith('http://localhost');
export const IS_EE = process.env.IS_EE ? !!JSON.parse(process.env.IS_EE) : false;
export const URL_MONGODB = process.env.MONGO_URL || 'mongodb://localhost:3001/meteor?retryWrites=false';
export const MOCK_SERVER_URL = process.env.MOCK_SERVER_URL || 'http://localhost:4000';
export const ADMIN_CREDENTIALS = {
email: 'rocketchat.internal.admin.test@rocket.chat',
password: 'rocketchat.internal.admin.test',
username: 'rocketchat.internal.admin.test',
} as const;
export const DEFAULT_USER_CREDENTIALS = {
password: 'password',
bcrypt: '$2b$10$LNYaqDreDE7tt9EVEeaS9uw.C3hic9hcqFfIocMBPTMxJaDCC6QWW',
} as const;

View File

@@ -0,0 +1,11 @@
import addCustomOAuth from '../fixtures/addCustomOAuth';
import injectInitialData from '../fixtures/inject-initial-data';
import { insertDefaultTestApp } from '../utils/apps';
export default async function (): Promise<void> {
await injectInitialData();
await insertDefaultTestApp();
await addCustomOAuth();
}

View File

@@ -0,0 +1,7 @@
declare namespace PlaywrightTest {
interface Matchers<R> {
hasAttribute(a: string): Promise<R>;
toBeInvalid(): Promise<R>;
toBeBusy(): Promise<R>;
}
}

View File

@@ -0,0 +1,36 @@
FROM php:8.2-apache-bookworm
# Utilities
RUN apt-get update && \
apt-get -y install apt-transport-https git curl vim --no-install-recommends && \
rm -r /var/lib/apt/lists/*
# SimpleSAMLphp
ARG SIMPLESAMLPHP_VERSION=1.19.6
RUN curl -s -L -o /tmp/simplesamlphp.tar.gz https://github.com/simplesamlphp/simplesamlphp/releases/download/v$SIMPLESAMLPHP_VERSION/simplesamlphp-$SIMPLESAMLPHP_VERSION.tar.gz && \
tar xzf /tmp/simplesamlphp.tar.gz -C /tmp && \
rm -f /tmp/simplesamlphp.tar.gz && \
mv /tmp/simplesamlphp-* /var/www/simplesamlphp && \
touch /var/www/simplesamlphp/modules/exampleauth/enable
COPY config/simplesamlphp/config.php /var/www/simplesamlphp/config
COPY config/simplesamlphp/authsources.php /var/www/simplesamlphp/config
COPY config/simplesamlphp/saml20-idp-hosted.php /var/www/simplesamlphp/metadata
COPY config/simplesamlphp/saml20-sp-remote.php /var/www/simplesamlphp/metadata
COPY config/simplesamlphp/server_crt /var/www/simplesamlphp/cert/server.crt
COPY config/simplesamlphp/server_pem /var/www/simplesamlphp/cert/server.pem
# Apache
COPY config/apache/ports.conf /etc/apache2
COPY config/apache/simplesamlphp.conf /etc/apache2/sites-available
COPY config/apache/cert_crt /etc/ssl/cert/cert.crt
COPY config/apache/private_key /etc/ssl/private/private.key
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && \
a2enmod ssl && \
a2dissite 000-default.conf default-ssl.conf && \
a2ensite simplesamlphp.conf
# Set work dir
WORKDIR /var/www/simplesamlphp
# General setup
EXPOSE 8080 8443

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJANdMEUvsTntJMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzMjIxWhcNNDgwNjI1MTQzMjIxWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAyXRKD3KzV/hOqwThFRtA+eoitJpIIEmWbugPMC1G+7bFqxHmtxiuQhOw
yHrij35biiD8VpboY69Zep7n1QCfmIodh9uxdaNxFtzxjryRLzfP3MpPFkpBHCdV
HZWDP2TzIvOxWcnlLmikSnBrBM1nvhKSWjaFsjDAXMLXT0mceiDpQ0QQkDA6RAyx
JWWRJILjudBh56ukqvdz4eFWAAViZX5MUwCDxiBxtP3NIXVmODM7kDLqZ9+QcfpM
N5QvfcUjHP9584yrYiJ9N64Fy5vU2OH1RX5EsMHTtdqR4H5K6zNfVlgRSG170Mcj
ksTSbo1kcDCSuzTO82NrVkU+R78W/QIDAQABo1AwTjAdBgNVHQ4EFgQUkYMQMPqY
leTTtBHS1f7yNFmY86QwHwYDVR0jBBgwFoAUkYMQMPqYleTTtBHS1f7yNFmY86Qw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGZpbEWzYLoa5keg9rQDa
S2cf9rQFMNflwR7hQ6OtSXeP0JsQ6yFhRq3TgpBdbkmdealDJbyG8pWQxqwOBD/j
45jr+NsHap0HvQAg9pfq/QIqjH5osCGAHmNdHfP638FOpi0s6hAX11+nCW6ClHMO
ofn0fYXCBtM18qZvtoeB8swi6MlRXFLvvRL4tWzGugdQj+0f+ukk/GZAEitdpkuj
qFCHqfKwg9FJ4My5M8lIyB/P4+SK/ail/BoCJ/qGBXky2bob0MQuLysd35zrTA62
j5IvPp1XZj/2KnuPtqMuFhNtE5wCnOEC1WG02ZVIfs4DAxX78z59VSEaFlnstT9k
aQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,9 @@
Listen 8080
<IfModule ssl_module>
Listen 8443
</IfModule>
<IfModule mod_gnutls.c>
Listen 8443
</IfModule>

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJdEoPcrNX+E6r
BOEVG0D56iK0mkggSZZu6A8wLUb7tsWrEea3GK5CE7DIeuKPfluKIPxWluhjr1l6
nufVAJ+Yih2H27F1o3EW3PGOvJEvN8/cyk8WSkEcJ1UdlYM/ZPMi87FZyeUuaKRK
cGsEzWe+EpJaNoWyMMBcwtdPSZx6IOlDRBCQMDpEDLElZZEkguO50GHnq6Sq93Ph
4VYABWJlfkxTAIPGIHG0/c0hdWY4MzuQMupn35Bx+kw3lC99xSMc/3nzjKtiIn03
rgXLm9TY4fVFfkSwwdO12pHgfkrrM19WWBFIbXvQxyOSxNJujWRwMJK7NM7zY2tW
RT5Hvxb9AgMBAAECggEAVT4KtHyxXJDqIL1gzICKvvUOmGMMD/VzXRx+iMEv3wTY
oWliuakM21LfpAUzZspty4XnoHAch0nET/l7WYr4/R+8HSed8IwnJyh4YhByUouI
PgGw81qaMGKIRotkTOfXZbu+GKMwgbGvivwEnLSZqDjNirS1X8/3JYkgeCFKv/X6
K4Z1SKoebtA4oGDGZtxNpeGJ6TibaSO3PRW+oH3dD5j5ez1k+dlRasMnuKG76jBy
naz052t52UZxAnGkbZUU84VljO5R8x1jip+M6+qILC/PsI1hb45Dx5VRXNUBGP2s
/PRVFptNGmsIlXBYMSdOo9RJAIsZ1kTty34nIqeuYQKBgQD4Nv+QoeRXjG7iAmzj
JabcA+2FiHftvjdaYjahXQ/Ma6ns/vv//tVfxJaNyCxMObjIKhidsrf/AAn03FgT
LkL0Q2tTBNL9jejzHstnCKSSu/pSgqatsABm1cokC9qsuMjweMVy+tGjSsF6YgjI
2K0heQpF0bMBSYmyQU4aTLbytQKBgQDPxdWVn7ReCscDwpchx+zffKh5JOshW44z
GYBfVbugBljj9w6fDjKyQzk0jMGmuG9rvqfZzu9MpQhjBFKxL4aXPBN7bQOY3DKc
FuGSbIbV7ldPmZhOxqLy6NLyiry6eSQGNf1Q8YCF6bsUR/rvdKDtBLrbJObgRVbu
ChT2PXRYKQKBgFSEDZMGvMReqebE4qSZTm592+Nq60MFUL2y0V0yXc3CHxL2Y4Hw
GGFKg+T08rhlsxhc1RLlJqdqMPmyCT9Gsj+PsTyMWPdC2b3mj2We2MKpxPtRR0W+
tvRM+U46xxOmu6y9wqV65+TM8IImXU1eEd1i5G+Pjn7ytjL+74Qe+PA9AoGAEasM
F5YmG10lQU+Z1HiQzwxlsy+Ngx+q/vNrNDAxLVF8253Vs3bcnsYSpkJV8Vx7tRjY
YzAyrzzVcr4aXhDhjBjCu1sw1B3de+KCOhZafPSwngc8qW5AyxE7Zv6fP+gvRQvw
R6LRwBF5JCde0mADk0Q0s4/2xhl/Y+ydjbb6HskCgYEAysuIUrDslGGsK57loxMO
FVz9SmLTZIJqkSW+l3dDHMG+BvnJFP+yf0Kr2zGbXRzOvNVWAFP2aU39RFDxbUIM
Nz7obLWrVKbQUDaU7fCbP4OBVuo9p4UM6j/PZy+3Cyps+GMTrC9wi4HbbNEWHyCW
xaNL9LNQ3B9hJG77htK1oRw=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,23 @@
<VirtualHost *:8080>
ServerName localhost
DocumentRoot /var/www/simplesamlphp
Alias /simplesaml /var/www/simplesamlphp/www
<Directory /var/www/simplesamlphp/www>
<IfModule !mod_authz_core.c>
Require all granted
</IfModule>
</Directory>
</VirtualHost>
<VirtualHost *:8443>
ServerName localhost
DocumentRoot /var/www/simplesamlphp
SSLEngine on
SSLCertificateFile /etc/ssl/cert/cert.crt
SSLCertificateKeyFile /etc/ssl/private/private.key
Alias /simplesaml /var/www/simplesamlphp/www
<Directory /var/www/simplesamlphp/www>
<IfModule !mod_authz_core.c>
Require all granted
</IfModule>
</Directory>
</VirtualHost>

View File

@@ -0,0 +1,74 @@
<?php
$config = array(
'admin' => array(
'core:AdminPassword',
),
'example-userpass' => array(
'exampleauth:UserPass',
'samluser1:password' => array(
'uid' => array('1'),
'username' => 'samluser1',
'cn' => 'Saml User 1',
'eduPersonAffiliation' => array('group1'),
'email' => 'samluser1@example.com',
),
'samluser2:password' => array(
'uid' => array('2'),
'username' => 'samluser2',
'cn' => 'Saml User 2',
'eduPersonAffiliation' => array('group2'),
'email' => 'user_for_saml_merge@email.com',
),
'samluser3:password' => array(
'uid' => array('3'),
'username' => 'user_for_saml_merge2',
'cn' => 'Saml User 3',
'eduPersonAffiliation' => array('group2'),
'email' => 'samluser3@example.com',
),
'samluser4:password' => array(
'uid' => array('4'),
'username' => 'samluser4',
'cn' => 'Saml User 4',
'eduPersonAffiliation' => array('group4'),
'email' => 'samluser4@example.com',
'role' => 'saml-role',
),
'samluser5:password' => array(
'uid' => array('5'),
'username' => 'samluser5',
'eduPersonAffiliation' => array('group5'),
'email' => 'samluser5@example.com',
),
'samluser6:password' => array(
'uid' => array('6'),
'username' => 'samluser6',
'displayName' => 'Saml User 6 Display Name',
'eduPersonAffiliation' => array('group6'),
'email' => 'samluser6@example.com',
),
'samluser7:password' => array(
'uid' => array('7'),
'username' => 'samluser7',
'cn' => 'Saml User 7',
'eduPersonAffiliation' => array('group7'),
'email' => 'samluser7@example.com',
'channels' => array('saml-channel-1', 'saml-channel-2'),
),
'samlusernoname:password' => array(
'uid' => array('5'),
'cn' => 'Saml User No Username',
'eduPersonAffiliation' => array('group2'),
'email' => 'samlusernoname@example.com',
),
'samlusernoname2:password' => array(
'uid' => array('6'),
'cn' => 'Saml User No Username 2',
'eduPersonAffiliation' => array('group2'),
'email' => 'samlusernoname2@example.com',
),
),
);

View File

@@ -0,0 +1,112 @@
<?php
$config = array(
'baseurlpath' => 'simplesaml/',
'certdir' => 'cert/',
'loggingdir' => 'log/',
'datadir' => 'data/',
'tempdir' => '/tmp/simplesaml',
'debug' => true,
'showerrors' => true,
'errorreporting' => true,
'debug.validatexml' => false,
'auth.adminpassword' => 'secret',
'admin.protectindexpage' => false,
'admin.protectmetadata' => false,
'secretsalt' => 'defaultsecretsalt',
'technicalcontact_name' => 'Administrator',
'technicalcontact_email' => 'na@example.org',
'timezone' => null,
'logging.level' => SimpleSAML_Logger::DEBUG,
'logging.handler' => 'errorlog',
//'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg',
'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER,
'logging.processname' => 'simplesamlphp',
'logging.logfile' => 'simplesamlphp.log',
'statistics.out' => array(
),
'database.dsn' => 'mysql:host=localhost;dbname=saml',
'database.username' => 'simplesamlphp',
'database.password' => 'secret',
'database.prefix' => '',
'database.persistent' => false,
'database.slaves' => array(
),
'enable.saml20-idp' => true,
'enable.shib13-idp' => true,
'enable.adfs-idp' => false,
'enable.wsfed-sp' => false,
'enable.authmemcookie' => false,
'session.duration' => 8 * (60 * 60), // 8 hours.
'session.datastore.timeout' => (4 * 60 * 60), // 4 hours
'session.state.timeout' => (60 * 60), // 1 hour
'session.cookie.name' => 'SimpleSAMLSessionIDIdp',
'session.cookie.lifetime' => 0,
'session.cookie.path' => '/',
'session.cookie.domain' => null,
'session.cookie.secure' => false,
'session.cookie.samesite' => 'Lax',
'enable.http_post' => false,
'session.phpsession.cookiename' => 'PHPSESSIDIDP',
'session.phpsession.savepath' => null,
'session.phpsession.httponly' => true,
'session.authtoken.cookiename' => 'SimpleSAMLAuthTokenIdp',
'session.rememberme.enable' => false,
'session.rememberme.checked' => false,
'session.rememberme.lifetime' => (14 * 86400),
'language.available' => array(
'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'fr', 'it', 'nl', 'lb', 'cs',
'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw', 'ru', 'et',
'he', 'id', 'sr', 'lv', 'ro', 'eu'
),
'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'),
'language.default' => 'en',
'language.parameter.name' => 'language',
'language.parameter.setcookie' => true,
'language.cookie.name' => 'language',
'language.cookie.domain' => null,
'language.cookie.path' => '/',
'language.cookie.lifetime' => (60 * 60 * 24 * 900),
'attributes.extradictionary' => null,
'theme.use' => 'default',
'default-wsfed-idp' => 'urn:federation:pingfederate:localhost',
'idpdisco.enableremember' => true,
'idpdisco.rememberchecked' => true,
'idpdisco.validate' => true,
'idpdisco.extDiscoveryStorage' => null,
'idpdisco.layout' => 'dropdown',
'shib13.signresponse' => true,
'authproc.idp' => array(
30 => 'core:LanguageAdaptor',
45 => array(
'class' => 'core:StatisticsWithAttribute',
'attributename' => 'realm',
'type' => 'saml20-idp-SSO',
),
50 => 'core:AttributeLimit',
99 => 'core:LanguageAdaptor',
),
'authproc.sp' => array(
90 => 'core:LanguageAdaptor',
),
'metadata.sources' => array(
array('type' => 'flatfile'),
),
'store.type' => 'phpsession',
'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3',
'store.sql.username' => null,
'store.sql.password' => null,
'store.sql.prefix' => 'SimpleSAMLphp',
'memcache_store.servers' => array(
array(
array('hostname' => 'localhost'),
),
),
'memcache_store.prefix' => null,
'memcache_store.expires' => 36 * (60 * 60), // 36 hours.
'metadata.sign.enable' => false,
'metadata.sign.privatekey' => null,
'metadata.sign.privatekey_pass' => null,
'metadata.sign.certificate' => null,
'proxy' => null,
'trusted.url.domains' => array('localhost:3000'),
);

View File

@@ -0,0 +1,23 @@
<?php
$metadata['http://localhost:3000/_saml/metadata/test-sp'] = [
'entityid' => 'http://localhost:3000/_saml/metadata/test-sp',
'host' => '__DEFAULT__',
'privatekey' => 'server.pem',
'certificate' => 'server.crt',
'auth' => 'example-userpass',
'redirect.sign' => true,
/*
* Force the NameID to be the raw email attribute value in emailAddress
* format, bypassing SimpleSAMLphp's default persistent (hashed) nameID
* generation.
*/
'authproc' => [
100 => [
'class' => 'saml:AttributeNameID',
'attribute' => 'email',
'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
],
],
];

View File

@@ -0,0 +1,31 @@
<?php
$metadata['http://localhost:3000/_saml/metadata/test-sp'] = array (
'entityid' => 'http://localhost:3000/_saml/metadata/test-sp',
'contacts' => array (
),
'metadata-set' => 'saml20-sp-remote',
'AssertionConsumerService' => array (
0 => array (
'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'Location' => 'http://localhost:3000/_saml/validate/test-sp',
'index' => 1,
'isDefault' => true,
),
),
'SingleLogoutService' => array (
0 => array (
'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'Location' => 'http://localhost:3000/_saml/logout/test-sp/',
'ResponseLocation' => 'http://localhost:3000/_saml/logout/test-sp/',
),
),
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
// Other settings that may be useful for testing, but are not compatible with the existing e2e tests
'privatekey' => 'server.pem',
'certificate' => 'server.crt',
'redirect.sign' => true,
// 'redirect.validate' => true,
// 'validate.logout' => true,
// 'assertion.encryption' => true,
);

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav
Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+
YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc
+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix
YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8
jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C
YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b
lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs
X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7
yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7
NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n
aQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz
SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR
50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq
qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r
DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa
ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa
CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY
y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP
rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5
PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp
TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1
1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR
TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX
SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq
V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L
0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz
lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY
n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN
LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9
gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09
oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw
n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4
2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o
CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF
fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp
Sm75WXsflOxuTn08LbgGc4s=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,7 @@
version: '3'
services:
testsamlidp_idp:
build: .
ports:
- "8080:8080"
- "8443:8443"

View File

@@ -0,0 +1,33 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('create-channel', () => {
let poHomeChannel: HomeChannel;
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
test('expect create a public channel', async ({ page }) => {
const channelName = faker.string.uuid();
await poHomeChannel.navbar.createNew('Channel', channelName, { private: false });
await expect(page).toHaveURL(`/channel/${channelName}`);
});
test('expect create a private channel', async ({ page }) => {
const channelName = faker.string.uuid();
await poHomeChannel.navbar.createNew('Channel', channelName, { private: true });
await expect(page).toHaveURL(`/group/${channelName}`);
});
});

View File

@@ -0,0 +1,20 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('channel-direct-message', () => {
let poHomeChannel: HomeChannel;
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
test('expect create a direct room', async ({ page }) => {
await poHomeChannel.navbar.createNewDM('rocket.cat');
await expect(page).toHaveURL(/direct\/.*/);
});
});

View File

@@ -0,0 +1,26 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { HomeDiscussion } from './page-objects';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('create-discussion', () => {
let poHomeDiscussion: HomeDiscussion;
test.beforeEach(async ({ page }) => {
poHomeDiscussion = new HomeDiscussion(page);
await poHomeDiscussion.goto();
});
test('expect create discussion', async ({ page }) => {
const discussionName = faker.string.uuid();
const discussionMessage = faker.animal.type();
await poHomeDiscussion.navbar.createNewDiscussion('general', discussionName, discussionMessage);
await expect(page).toHaveURL(/\/channel\/[a-z0-9]{0,17}$/i);
});
});

View File

@@ -0,0 +1,104 @@
import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { AccountProfile, Authenticated, Login } from './page-objects';
import { setSettingValueById } from './utils';
import { test, expect } from './utils/test';
import { createTestUser, type ITestUser } from './utils/user-helpers';
test.describe('Delete Own Account', () => {
let poAccountProfile: AccountProfile;
let poLogin: Login;
let poAuth: Authenticated;
let userToDelete: ITestUser;
let userWithInvalidPassword: ITestUser;
let userWithoutPermissions: ITestUser;
test.beforeAll(async ({ api }) => {
expect((await setSettingValueById(api, 'Accounts_AllowDeleteOwnAccount', true)).status()).toBe(200);
userToDelete = await createTestUser(api, { username: 'user-to-delete' });
userWithInvalidPassword = await createTestUser(api, { username: 'user-with-invalid-password' });
userWithoutPermissions = await createTestUser(api, { username: 'user-without-permissions' });
});
test.beforeEach(async ({ page }) => {
poAccountProfile = new AccountProfile(page);
poLogin = new Login(page);
poAuth = new Authenticated(page);
await poLogin.goto();
});
test.afterAll(async ({ api }) => {
expect((await setSettingValueById(api, 'Accounts_AllowDeleteOwnAccount', false)).status()).toBe(200);
await userWithInvalidPassword.delete();
await userWithoutPermissions.delete();
});
test('should not delete account when invalid password is provided', async () => {
await test.step('login with the user to delete', async () => {
await poLogin.login(userWithInvalidPassword.data.username, DEFAULT_USER_CREDENTIALS.password);
await poAuth.waitForDisplay();
});
await test.step('navigate to profile and locate Delete My Account button', async () => {
await poAccountProfile.goto();
await poAccountProfile.btnDeleteMyAccount.click();
await poAccountProfile.deleteAccountModal.waitForDisplay();
});
await test.step('enter invalid password in the confirmation field and click delete account', async () => {
await poAccountProfile.deleteAccountModal.inputPassword.fill('invalid-password');
await expect(poAccountProfile.deleteAccountModal.inputPassword).toHaveValue('invalid-password');
await poAccountProfile.deleteAccountModal.confirmDelete({ waitForDismissal: false });
});
await test.step('verify error message appears', async () => {
await expect(poAccountProfile.deleteAccountModal.inputErrorMessage).toBeVisible();
});
await test.step('verify user is still on the profile page', async () => {
await expect(poAccountProfile.profileHeading).toBeVisible();
});
});
test('should delete account when valid password is provided and permission is enabled', async () => {
await test.step('login with the user to delete', async () => {
await poLogin.login(userToDelete.data.username, DEFAULT_USER_CREDENTIALS.password);
await poAuth.waitForDisplay();
});
await test.step('navigate to profile and locate Delete My Account button', async () => {
await poAccountProfile.goto();
await poAccountProfile.btnDeleteMyAccount.click();
await poAccountProfile.deleteAccountModal.waitForDisplay();
});
await test.step('enter password in the confirmation field and click delete account', async () => {
await poAccountProfile.deleteAccountModal.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await expect(poAccountProfile.deleteAccountModal.inputPassword).toHaveValue(DEFAULT_USER_CREDENTIALS.password);
await poAccountProfile.deleteAccountModal.confirmDelete({ waitForDismissal: false });
});
await test.step('verify user is redirected to login page', async () => {
await poLogin.waitForDisplay();
userToDelete.markAsDeleted();
});
});
test.describe('Delete Own Account - Permission Disabled', () => {
test.beforeAll(async ({ api }) => {
const response = await api.post('/settings/Accounts_AllowDeleteOwnAccount', { value: false });
expect(response.status()).toBe(200);
});
test('should not show delete account button when permission is disabled', async () => {
await test.step('login with the user to delete', async () => {
await poLogin.login(userWithoutPermissions.data.username, DEFAULT_USER_CREDENTIALS.password);
await poAuth.waitForDisplay();
});
await test.step('navigate to profile and locate Delete My Account button', async () => {
await poAccountProfile.goto();
await expect(poAccountProfile.btnDeleteMyAccount).not.toBeVisible();
});
});
});
});

View File

@@ -0,0 +1,407 @@
import { faker } from '@faker-js/faker';
import { DEFAULT_USER_CREDENTIALS } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { ToastMessages } from '../page-objects/fragments';
import { CreateE2EEChannel } from '../page-objects/fragments/e2ee';
import { deletePrivateRoomsByName } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
preserveSettings(settingsList);
test.describe('E2EE Encrypted Channels', () => {
const createdChannels: string[] = [];
let poHomeChannel: HomeChannel;
let toastMessages: ToastMessages;
let createE2EEChannel: CreateE2EEChannel;
test.use({ storageState: Users.userE2EE.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
await api.post('/im.delete', { username: 'user2' });
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
createE2EEChannel = new CreateE2EEChannel(page);
await poHomeChannel.goto();
});
test.afterAll(async () => {
await deletePrivateRoomsByName(
{ username: Users.userE2EE.data.username, password: DEFAULT_USER_CREDENTIALS.password },
createdChannels,
);
});
test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => {
toastMessages = new ToastMessages(page);
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('hello world');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await toastMessages.dismissToast();
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemDisableE2EEncryption.click();
await expect(page.getByRole('dialog', { name: 'Disable encryption' })).toBeVisible();
await page.getByRole('button', { name: 'Disable encryption' }).click();
await poHomeChannel.toastMessage.dismissToast();
await page.waitForTimeout(1000);
await poHomeChannel.content.sendMessage('hello world not encrypted');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world not encrypted');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible();
await poHomeChannel.roomToolbar.openMoreOptions();
await expect(poHomeChannel.roomToolbar.menuItemEnableE2EEncryption).toBeVisible();
await poHomeChannel.roomToolbar.menuItemEnableE2EEncryption.click();
await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible();
await page.getByRole('button', { name: 'Enable encryption' }).click();
await poHomeChannel.toastMessage.dismissToast();
await page.waitForTimeout(1000);
await poHomeChannel.content.sendMessage('hello world encrypted again');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world encrypted again');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
test('expect create a private encrypted channel and send a encrypted thread message', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('This is the thread main message.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is the thread main message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.openReplyInThread();
await expect(page).toHaveURL(/.*thread/);
await expect(poHomeChannel.content.mainThreadMessageText).toContainText('This is the thread main message.');
await expect(poHomeChannel.content.mainThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.toggleAlsoSendThreadToChannel(true);
await page.getByRole('dialog').locator('[name="msg"]').last().fill('This is an encrypted thread message also sent in channel');
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserThreadMessage).toContainText('This is an encrypted thread message also sent in channel');
await expect(poHomeChannel.content.lastUserThreadMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.lastThreadMessagePreview).toContainText('This is an encrypted thread message also sent in channel');
await expect(poHomeChannel.content.mainThreadMessageText).toContainText('This is the thread main message.');
await expect(poHomeChannel.content.mainThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible();
});
test('expect create a private encrypted channel and check disabled message menu actions on an encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.lastUserMessage.hover();
await expect(page.locator('role=button[name="Forward message not available on encrypted content"]')).toBeDisabled();
await poHomeChannel.content.openLastMessageMenu();
await expect(page.locator('role=menuitem[name="Reply in direct message"]')).toHaveClass(/disabled/);
await expect(page.locator('role=menuitem[name="Copy link"]')).toHaveClass(/disabled/);
});
test('expect create a private channel, encrypt it and send an encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
await poHomeChannel.navbar.createNew('Channel', channelName);
createE2EEChannel.store(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await poHomeChannel.toastMessage.waitForDisplay();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.roomToolbar.openMoreOptions();
// TODO(@jessicaschelly/@dougfabris): fix this flaky behavior
if (!(await poHomeChannel.roomToolbar.menuItemEnableE2EEncryption.isVisible())) {
await poHomeChannel.roomToolbar.openMoreOptions();
}
await poHomeChannel.roomToolbar.menuItemEnableE2EEncryption.click();
await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible();
await page.getByRole('button', { name: 'Enable encryption' }).click();
await page.waitForTimeout(1000);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('hello world');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
test('expect create a encrypted private channel and mention user', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('hello @user1');
const userMention = page.getByRole('button', {
name: 'user1',
});
await expect(userMention).toBeVisible();
});
test('expect create a encrypted private channel, mention a channel and navigate to it', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('Are you in the #general channel?');
const channelMention = page.getByRole('button', {
name: 'general',
});
await expect(channelMention).toBeVisible();
await channelMention.click();
await expect(page).toHaveURL(`/channel/general`);
});
test('expect create a encrypted private channel, mention a channel and user', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('Are you in the #general channel, @user1 ?');
const channelMention = page.getByRole('button', {
name: 'general',
});
const userMention = page.getByRole('button', {
name: 'user1',
});
await expect(userMention).toBeVisible();
await expect(channelMention).toBeVisible();
});
test('expect create a private channel, send unecrypted messages, encrypt the channel and delete the last message and check the last message in the sidebar', async ({
page,
}) => {
const channelName = faker.string.uuid();
// Enable Sidebar Extended display mode
await poHomeChannel.navbar.setDisplayMode('Extended');
// Create private channel
await poHomeChannel.navbar.createNew('Channel', channelName, { private: true });
createE2EEChannel.store(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await poHomeChannel.toastMessage.waitForDisplay();
await poHomeChannel.toastMessage.dismissToast();
// Send Unencrypted Messages
await poHomeChannel.content.sendMessage('first unencrypted message');
await poHomeChannel.content.sendMessage('second unencrypted message');
// Encrypt channel
await poHomeChannel.roomToolbar.openMoreOptions();
await expect(poHomeChannel.roomToolbar.menuItemEnableE2EEncryption).toBeVisible();
await poHomeChannel.roomToolbar.menuItemEnableE2EEncryption.click();
await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible();
await page.getByRole('button', { name: 'Enable encryption' }).click();
await page.waitForTimeout(1000);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
// Send Encrypted Messages
const encriptedMessage1 = 'first ENCRYPTED message';
const encriptedMessage2 = 'second ENCRYPTED message';
await poHomeChannel.content.sendMessage(encriptedMessage1);
await poHomeChannel.content.sendMessage(encriptedMessage2);
// Delete last message
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(encriptedMessage2);
await poHomeChannel.content.openLastMessageMenu();
// TODO(@jessicaschelly/@dougfabris): fix this flaky behavior
if (!(await page.locator('role=menuitem[name="Delete"]').isVisible())) {
await poHomeChannel.content.openLastMessageMenu();
}
await page.locator('role=menuitem[name="Delete"]').click();
await page.locator('#modal-root .rcx-button-group--align-end .rcx-button--danger').click();
// Check last message in the sidebar
const sidebarChannel = poHomeChannel.sidebar.getSidebarItemByName(channelName);
await expect(sidebarChannel).toBeVisible();
await expect(sidebarChannel.getByText(`You: ${encriptedMessage2}`, { exact: true })).not.toBeVisible();
await expect(sidebarChannel.getByText(`You: ${encriptedMessage1}`, { exact: true })).toBeVisible();
});
test('expect create a private encrypted channel and pin/star an encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('This message should be pinned and stared.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This message should be pinned and stared.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.openLastMessageMenu();
await page.locator('role=menuitem[name="Star"]').click();
await poHomeChannel.toastMessage.waitForDisplay();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.content.openLastMessageMenu();
await page.locator('role=menuitem[name="Pin"]').click();
await page.locator('#modal-root >> button:has-text("Yes, pin message")').click();
await poHomeChannel.toastMessage.waitForDisplay();
await poHomeChannel.toastMessage.dismissToast();
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemPinnedMessages.click();
await expect(page.getByRole('dialog', { name: 'Pinned Messages' })).toBeVisible();
const lastPinnedMessage = page
.getByRole('dialog', { name: 'Pinned Messages' })
.locator('[role="listitem"][aria-roledescription="message"]')
.last();
await expect(lastPinnedMessage).toContainText('This message should be pinned and stared.');
await lastPinnedMessage.hover();
await lastPinnedMessage.locator('role=button[name="More"]').waitFor();
await lastPinnedMessage.locator('role=button[name="More"]').click();
await expect(page.locator('role=menuitem[name="Copy link"]')).toHaveClass(/disabled/);
await poHomeChannel.btnContextualbarClose.click();
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemStarredMessages.click();
const lastStarredMessage = page
.getByRole('dialog', { name: 'Starred Messages' })
.locator('[role="listitem"][aria-roledescription="message"]')
.last();
await expect(page.getByRole('dialog', { name: 'Starred Messages' })).toBeVisible();
await expect(lastStarredMessage).toContainText('This message should be pinned and stared.');
await lastStarredMessage.hover();
await lastStarredMessage.locator('role=button[name="More"]').waitFor();
await lastStarredMessage.locator('role=button[name="More"]').click();
await expect(page.locator('role=menuitem[name="Copy link"]')).toHaveClass(/disabled/);
});
test('expect to edit encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
const originalMessage = 'This is the original encrypted message';
const editedMessage = 'This is the edited encrypted message';
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage(originalMessage);
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(originalMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await poHomeChannel.composer.inputMessage.fill(editedMessage);
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(editedMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
test('expect to edit encrypted message to include mention', async ({ page }) => {
const channelName = faker.string.uuid();
const originalMessage = 'This is the original encrypted message';
const editedMessage = 'This is the edited encrypted message with a mention to @user1 and #general';
const displayedMessage = 'This is the edited encrypted message with a mention to user1 and general';
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage(originalMessage);
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(originalMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await poHomeChannel.composer.inputMessage.fill(editedMessage);
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(displayedMessage);
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
const userMention = page.getByRole('button', {
name: 'user1',
});
await expect(userMention).toBeVisible();
const channelMention = page.getByRole('button', {
name: 'general',
});
await expect(channelMention).toBeVisible();
});
});

View File

@@ -0,0 +1,217 @@
import { faker } from '@faker-js/faker';
import { resetOwnE2EKey } from './resetOwnE2EKey';
import { setupE2EEPassword } from './setupE2EEPassword';
import { ADMIN_CREDENTIALS, BASE_URL } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { EncryptedRoomPage } from '../page-objects/encrypted-room';
import { Navbar } from '../page-objects/fragments';
import { CreateE2EEChannel } from '../page-objects/fragments/e2ee';
import { FileUploadModal } from '../page-objects/fragments/modals';
import { LoginPage } from '../page-objects/login';
import { createTargetGroupAndReturnFullRoom, deletePrivateRoomsByName, deleteRoom } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { sendMessageFromUser } from '../utils/sendMessage';
import { test, expect } from '../utils/test';
const settingsList = ['E2E_Enable', 'E2E_Allow_Unencrypted_Messages'];
preserveSettings(settingsList);
test.describe('E2EE Encryption and Decryption - Basic Features', () => {
const createdChannels: string[] = [];
let loginPage: LoginPage;
let navbar: Navbar;
let encryptedRoomPage: EncryptedRoomPage;
let createE2EEChannel: CreateE2EEChannel;
test.use({ storageState: Users.admin.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
});
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
navbar = new Navbar(page);
encryptedRoomPage = new EncryptedRoomPage(page);
createE2EEChannel = new CreateE2EEChannel(page);
await expect(await resetOwnE2EKey(ADMIN_CREDENTIALS)).toBeOK();
await loginPage.goto();
await loginPage.loginByUserState(Users.admin);
});
test.afterAll(async () => {
await deletePrivateRoomsByName(ADMIN_CREDENTIALS, createdChannels);
});
test('expect placeholder text in place of encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();
const messageText = 'This is an encrypted message.';
await setupE2EEPassword(page);
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await encryptedRoomPage.waitForChannel();
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
await expect(encryptedRoomPage.encryptionNotReadyIndicator).not.toBeVisible();
await encryptedRoomPage.sendMessage(messageText);
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.body).toHaveText(messageText);
// Log out
await navbar.logout();
// Login again
await loginPage.loginByUserState(Users.admin);
// Navigate to the encrypted channel WITHOUT entering the password
await navbar.openChat(channelName);
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
await expect(encryptedRoomPage.encryptionNotReadyIndicator).toBeVisible();
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.body).toHaveText(
'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.',
);
});
test('expect placeholder text in place of encrypted file upload description', async ({ page }) => {
const fileUploadModal = new FileUploadModal(page);
const channelName = faker.string.uuid();
const fileName = faker.system.commonFileName('txt');
const fileDescription = faker.lorem.sentence();
await setupE2EEPassword(page);
// Create an encrypted channel
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
await expect(encryptedRoomPage.encryptionNotReadyIndicator).not.toBeVisible();
await test.step('upload the file with encryption', async () => {
// Upload a file
await encryptedRoomPage.dragAndDropTxtFile();
// Update file name and send
await encryptedRoomPage.composer.getFileByName('any_file.txt').click();
await fileUploadModal.setName(fileName);
await fileUploadModal.update();
await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible();
await encryptedRoomPage.composer.inputMessage.fill(fileDescription);
await encryptedRoomPage.composer.btnSend.click();
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName);
await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription);
});
await test.step('disable encryption in the room', async () => {
await encryptedRoomPage.disableEncryption();
await expect(encryptedRoomPage.encryptedTitle).not.toBeVisible();
});
await test.step('upload the file without encryption', async () => {
await encryptedRoomPage.dragAndDropTxtFile();
// Update file name and send
await encryptedRoomPage.composer.getFileByName('any_file.txt').click();
await fileUploadModal.setName(fileName);
await fileUploadModal.update();
await expect(encryptedRoomPage.composer.getFileByName(fileName)).toBeVisible();
await encryptedRoomPage.composer.inputMessage.fill(fileDescription);
await encryptedRoomPage.composer.btnSend.click();
await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible();
await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName);
await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription);
});
await test.step('enable encryption in the room', async () => {
await encryptedRoomPage.enableEncryption();
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
});
// Log out
await navbar.logout();
// Login again
await loginPage.loginByUserState(Users.admin);
await expect(navbar.btnCreateNew).toBeVisible();
await navbar.openChat(channelName);
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
await expect(encryptedRoomPage.lastNthMessage(1).body).toHaveText(
'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.',
);
await expect(encryptedRoomPage.lastNthMessage(1).encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible();
await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toContainText(fileName);
await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription);
});
test.describe('E2EE Quotes', () => {
let targetRoomId: string;
let targetChannelName: string;
test.afterAll(async ({ api }) => {
await deleteRoom(api, targetRoomId);
});
test('expect to not crash and not show quote message for a message_link which is not accessible to the user', async ({
page,
request,
api,
}) => {
const encryptedRoomPage = new EncryptedRoomPage(page);
targetChannelName = faker.string.uuid();
await createE2EEChannel.createAndStore(targetChannelName, createdChannels);
await expect(page).toHaveURL(`/group/${targetChannelName}`);
await expect(encryptedRoomPage.encryptedTitle).toBeVisible();
await expect(encryptedRoomPage.encryptionNotReadyIndicator).not.toBeVisible();
await encryptedRoomPage.sendMessage('First encrypted message.');
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.body).toHaveText('First encrypted message.');
// create a private group for user2
const { group: user1Channel } = await createTargetGroupAndReturnFullRoom(api, {
excludeSelf: true,
members: [Users.user2.data._id],
});
targetRoomId = user1Channel._id;
// send a message to the private group, which is not accessible to the main user
const sentMessage = (await sendMessageFromUser(request, Users.user2, targetRoomId, 'This is a test message.')).message;
const messageLink = `${BASE_URL}/group/${user1Channel.name}?msg=${sentMessage._id}`;
await encryptedRoomPage.sendMessage(`This is a message with message link - ${messageLink}`);
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.body).toContainText(`This is a message with message link - ${messageLink}`);
await expect(encryptedRoomPage.lastNthMessage(1).body).toContainText('First encrypted message.');
await page.reload();
await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible();
await expect(encryptedRoomPage.lastMessage.body).toContainText(`This is a message with message link - ${messageLink}`);
await expect(encryptedRoomPage.lastNthMessage(1).body).toContainText('First encrypted message.');
});
});
});

View File

@@ -0,0 +1,216 @@
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { createTargetGroupAndReturnFullRoom } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enable_Encrypt_Files',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
const originalSettings = preserveSettings(settingsList);
const TEST_FILE_TXT = 'any_file.txt';
test.use({ storageState: Users.userE2EE.state });
test.describe('E2EE File Encryption', () => {
let poHomeChannel: HomeChannel;
let encryptedRoomId: string;
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
await api.post('/im.delete', { username: 'user2' });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' });
await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' });
});
test.beforeEach(async ({ api, page }) => {
poHomeChannel = new HomeChannel(page);
const { group } = await createTargetGroupAndReturnFullRoom(api, {
extraData: {
broadcast: false,
encrypted: true,
},
members: [Users.userE2EE.data._id],
});
encryptedRoomId = group._id;
await poHomeChannel.gotoGroup(group.name as string);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await expect
.poll(
() =>
page.evaluate(async (rid) => {
// eslint-disable-next-line import-x/no-absolute-path
const { e2e } = require('/client/lib/e2ee/rocketchat.e2e.ts') as typeof import('../../../client/lib/e2ee/rocketchat.e2e');
const room = await e2e.getInstanceByRoomId(rid);
return room?.getState();
}, group._id),
{ message: 'expect room encryption key to be ready before sending messages' },
)
.toBe('READY');
});
test.afterEach(async ({ api }) => {
expect((await api.post('/groups.delete', { roomId: encryptedRoomId })).status()).toBe(200);
});
test('should edit encrypted message with file', async ({ page }) => {
const updatedFileName = `edited_${TEST_FILE_TXT}`;
await test.step('send a file in channel and edit it', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill(updatedFileName);
await poHomeChannel.content.btnUpdateFileUpload.click();
await expect(poHomeChannel.composer.getFileByName(updatedFileName)).toBeVisible();
await poHomeChannel.composer.inputMessage.fill('any_description');
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description');
await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName);
});
await test.step('edit the description', async () => {
await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await expect(poHomeChannel.composer.inputMessage).toHaveValue('any_description');
await poHomeChannel.composer.inputMessage.fill('edited any_description');
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.getFileDescription).toHaveText('edited any_description');
await expect(poHomeChannel.content.lastUserMessage.getByRole('link').getByText(updatedFileName)).toBeVisible();
});
await test.step('delete the file from files list', async () => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemFiles.click();
await poHomeChannel.tabs.files.deleteFile(updatedFileName);
await expect(poHomeChannel.tabs.files.getFileByName(updatedFileName)).toHaveCount(0);
await expect(poHomeChannel.content.lastUserMessage.getByRole('link').getByText(updatedFileName)).not.toBeVisible();
});
});
test('File with Unicode filename uploads and downloads correctly', async ({ page }) => {
const UNICODE_FILE_NAME = 'Новый текстовый документ.txt';
await test.step('upload file with Unicode filename', async () => {
await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT);
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill(UNICODE_FILE_NAME);
await poHomeChannel.content.btnUpdateFileUpload.click();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getLastMessageByFileName(UNICODE_FILE_NAME)).toBeVisible();
});
await test.step('download the file and verify the Unicode filename is preserved', async () => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemFiles.click();
await expect(poHomeChannel.tabs.files.getFileByName(UNICODE_FILE_NAME)).toBeVisible();
const [download] = await Promise.all([
page.waitForEvent('download'),
poHomeChannel.tabs.files.getFileByName(UNICODE_FILE_NAME).click(),
]);
expect(download.suggestedFilename()).toBe(UNICODE_FILE_NAME);
});
});
test('File encryption with whitelisted and blacklisted media types', async ({ api }) => {
await test.step('send a text file in channel', async () => {
const updatedFileName = `edited_${TEST_FILE_TXT}`;
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill(updatedFileName);
await poHomeChannel.content.btnUpdateFileUpload.click();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).not.toBeVisible();
await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName);
});
await test.step('set whitelisted media type setting', async () => {
await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: 'text/plain' });
});
await test.step('send text file again with whitelist setting set', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.inputMessage.fill('message 2');
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill('any_file2.txt');
await poHomeChannel.content.btnUpdateFileUpload.click();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2');
await expect(poHomeChannel.content.getLastMessageByFileName('any_file2.txt')).toBeVisible();
});
await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => {
await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' });
});
await test.step('send text file again with blacklisted setting set, file upload should fail', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
const composerFiles = await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).all();
await Promise.all(composerFiles.map((file) => expect(file).toHaveAttribute('readonly')));
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
});
test.describe('File encryption setting disabled', () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable_Encrypt_Files', { value: false });
await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/E2E_Enable_Encrypt_Files', { value: originalSettings.E2E_Enable_Encrypt_Files });
await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' });
});
test('Upload file without encryption in e2ee room', async () => {
await test.step('send a test encrypted message to check e2ee is working', async () => {
await poHomeChannel.content.sendMessage('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
await test.step('send a text file in channel, file should not be encrypted', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.inputMessage.fill('any_description');
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill('any_file1.txt');
await poHomeChannel.content.btnUpdateFileUpload.click();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description');
await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toContainText('any_file1.txt');
});
});
});
});

View File

@@ -0,0 +1,53 @@
import type { Page } from '@playwright/test';
import { createAuxContext } from '../fixtures/createAuxContext';
import injectInitialData from '../fixtures/inject-initial-data';
import { Users } from '../fixtures/userStates';
import { AccountSecurity, HomeChannel } from '../page-objects';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
preserveSettings(settingsList);
test.describe('E2EE Key Reset', () => {
let anotherClientPage: Page;
test.use({ storageState: Users.userE2EE.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
await api.post('/im.delete', { username: 'user2' });
});
test.beforeEach(async ({ browser, page }) => {
anotherClientPage = (await createAuxContext(browser, Users.userE2EE)).page;
const poHomeChannel = new HomeChannel(page);
await poHomeChannel.goto();
});
test.afterEach(async () => {
await anotherClientPage.close();
await injectInitialData();
});
test('expect force logout on e2e keys reset', async ({ page }) => {
const poAccountSecurity = new AccountSecurity(page);
await poAccountSecurity.goto();
await poAccountSecurity.securityE2EEncryptionSection.click();
await poAccountSecurity.securityE2EEncryptionResetKeyButton.click();
await expect(page.locator('role=button[name="Login"]')).toBeVisible();
await expect(anotherClientPage.locator('role=button[name="Login"]')).toBeVisible();
});
});

View File

@@ -0,0 +1,100 @@
import { faker } from '@faker-js/faker';
import type { APIRequestContext } from '@playwright/test';
import { BASE_API_URL, DEFAULT_USER_CREDENTIALS } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { CreateE2EEChannel } from '../page-objects/fragments/e2ee';
import { deletePrivateRoomsByName } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
preserveSettings(settingsList);
const sendEncryptedMessage = async (request: APIRequestContext, rid: string, encryptedMsg: string) => {
return request.post(`${BASE_API_URL}/chat.sendMessage`, {
headers: {
'X-Auth-Token': Users.userE2EE.data.loginToken,
'X-User-Id': Users.userE2EE.data._id,
},
data: {
message: {
rid,
msg: encryptedMsg,
t: 'e2e',
},
},
});
};
test.describe('E2EE Legacy Format', () => {
const createdChannels: string[] = [];
let poHomeChannel: HomeChannel;
let createE2EEChannel: CreateE2EEChannel;
test.use({ storageState: Users.userE2EE.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
await api.post('/im.delete', { username: 'user2' });
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
createE2EEChannel = new CreateE2EEChannel(page);
await poHomeChannel.goto();
});
test.afterAll(async () => {
await deletePrivateRoomsByName(
{ username: Users.userE2EE.data.username, password: DEFAULT_USER_CREDENTIALS.password },
createdChannels,
);
});
test('legacy expect create a private channel encrypted and send an encrypted message', async ({ page, request }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(page.getByTitle('Encrypted')).toBeVisible();
// TODO: Fix this flakiness
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();
const rid = (await page.locator('[data-qa-rc-room]').getAttribute('data-qa-rc-room')) || '';
expect(rid).toBeTruthy();
const kid = '32c9e7917b78';
const encryptedKey =
'ibtLAKG9zcQ/NTp+86nVelUjewPbPNW+EC+eagVPVVlbxvWNXkgltrBQB4gDao1Fp6fHUibQB3dirJ4rzy7CViww0o4QjAwPPQMIxZ9DLJhjKnu6bkkOp6Z0/a9g/8Wf/cvP9/bp7tUt7Et4XMmJwIe5iyJZ35lsyduLc8V+YyK8sJiGf4BRagJoBr8xEBgqBWqg6Vwn3qtbbiTs65PqErbaUmSM3Hn6tfkcS6ukLG/DbptW1B9U66IX3fQesj50zWZiJyvxOoxDeHRH9UEStyv9SP8nrFjEKM3TDiakBeDxja6LoN8l3CjP9K/5eg25YqANZAQjlwaCaeTTHndTgQ==';
const encryptedMessage =
'3JpM8aOVludqIRzx+DOqjEU9Mj3NUWb+/GLRl7sdkvTtCMChH1LBjMjJJvVJ6Rlw4dI8BYFftZWiCOiR7TPwriCoSPiZ7dY5C4H2q8MVSdR95ZiyG7eWQ5j5/rxzAYsSWDA9LkumW8JBb+WQ1hD9JMfQd4IXtlFMnaDgEhZhe/s=';
await page.evaluate(
async ({ rid, kid, encryptedKey }) => {
// eslint-disable-next-line import-x/no-absolute-path
const { e2e } = require('/client/lib/e2ee/rocketchat.e2e.ts') as typeof import('../../../client/lib/e2ee/rocketchat.e2e');
const room = await e2e.getInstanceByRoomId(rid);
await room?.importGroupKey(kid + encryptedKey);
},
{ rid, kid, encryptedKey },
);
await sendEncryptedMessage(request, rid, kid + encryptedMessage);
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
});

View File

@@ -0,0 +1,350 @@
import { faker } from '@faker-js/faker';
import { resetOwnE2EKey } from './resetOwnE2EKey';
import { ADMIN_CREDENTIALS } from '../config/constants';
import injectInitialData from '../fixtures/inject-initial-data';
import { Users, storeState, restoreState } from '../fixtures/userStates';
import { AccountSecurity, HomeChannel } from '../page-objects';
import { setupE2EEPassword } from './setupE2EEPassword';
import { Authenticated } from '../page-objects/auth';
import { Navbar } from '../page-objects/fragments';
import { CreateE2EEChannel, E2EEKeyDecodeFailureBanner, EnterE2EEPasswordBanner } from '../page-objects/fragments/e2ee';
import { EnterE2EEPasswordModal, ResetE2EEPasswordModal } from '../page-objects/fragments/modals';
import { LoginPage } from '../page-objects/login';
import { deletePrivateRoomsByName } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
const originalSettings = preserveSettings(settingsList);
test.describe('E2EE Passphrase Management - Initial Setup', () => {
test.use({ storageState: Users.admin.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: originalSettings.E2E_Enable });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: originalSettings.E2E_Allow_Unencrypted_Messages });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: originalSettings.E2E_Enabled_Default_DirectRooms });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: originalSettings.E2E_Enabled_Default_PrivateRooms });
});
test.describe('Generate', () => {
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await expect(await resetOwnE2EKey(ADMIN_CREDENTIALS)).toBeOK();
await loginPage.goto();
await loginPage.loginByUserState(Users.admin);
});
test('expect the randomly generated password to work', async ({ page }) => {
const loginPage = new LoginPage(page);
const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page);
const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page);
const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page);
const navbar = new Navbar(page);
const password = await setupE2EEPassword(page);
// Log out
await navbar.logout();
// Login again
await loginPage.loginByUserState(Users.admin);
// Enter the saved password
await enterE2EEPasswordBanner.click();
await enterE2EEPasswordModal.enterPassword(password);
// No error banner
await e2EEKeyDecodeFailureBanner.expectToNotBeVisible();
});
test('expect to manually reset the password', async ({ page }) => {
const accountSecurityPage = new AccountSecurity(page);
const loginPage = new LoginPage(page);
// Reset the E2EE key to start the flow from the beginning
await accountSecurityPage.goto();
await accountSecurityPage.resetE2EEPassword();
await loginPage.loginByUserState(Users.admin);
});
test('should reset e2e password from the modal', async ({ page }) => {
const navbar = new Navbar(page);
const loginPage = new LoginPage(page);
const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page);
const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page);
const resetE2EEPasswordModal = new ResetE2EEPasswordModal(page);
await setupE2EEPassword(page);
// Logout
await navbar.logout();
// Login again
await loginPage.loginByUserState(Users.admin);
// Reset E2EE password
await enterE2EEPasswordBanner.click();
await enterE2EEPasswordModal.forgotPassword();
await resetE2EEPasswordModal.confirmReset();
// restore login
await loginPage.loginByUserState(Users.admin);
});
test('expect to manually set a new password', async ({ page }) => {
const accountSecurityPage = new AccountSecurity(page);
const loginPage = new LoginPage(page);
const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page);
const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page);
const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page);
const navbar = new Navbar(page);
const newPassword = faker.internet.password({
length: 30,
prefix:
faker.string.alpha({ casing: 'lower' }) +
faker.string.alpha({ casing: 'upper' }) +
faker.string.numeric() +
faker.string.symbol(),
});
await setupE2EEPassword(page);
// Set a new password
await accountSecurityPage.goto();
await accountSecurityPage.setE2EEPassword(newPassword);
// Log out
await navbar.logout();
// Login again
await loginPage.loginByUserState(Users.admin);
// Enter the saved password
await enterE2EEPasswordBanner.click();
await enterE2EEPasswordModal.enterPassword(newPassword);
// No error banner
await e2EEKeyDecodeFailureBanner.expectToNotBeVisible();
});
});
test.describe('Recovery', () => {
test.use({ storageState: Users.userE2EE.state });
test('expect to recover the keys using the recovery key', async ({ page }) => {
await test.step('Recover the keys', async () => {
await page.goto('/home');
await injectInitialData();
await restoreState(page, Users.userE2EE);
const navbar = new Navbar(page);
await navbar.logout();
const loginPage = new LoginPage(page);
await loginPage.loginByUserState(Users.userE2EE, { except: ['private_key', 'public_key'] });
const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page);
const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page);
const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page);
await enterE2EEPasswordBanner.click();
await enterE2EEPasswordModal.enterPassword('minus mobile dexter forest elvis');
await e2EEKeyDecodeFailureBanner.expectToNotBeVisible();
});
});
});
});
test.use({ storageState: Users.admin.state });
const roomSetupSettingsList = ['E2E_Enable', 'E2E_Allow_Unencrypted_Messages'];
test.describe.serial('E2EE Passphrase Management - Room Setup States', () => {
const createdChannels: string[] = [];
let poAccountSecurity: AccountSecurity;
let poHomeChannel: HomeChannel;
let createE2EEChannel: CreateE2EEChannel;
let e2eePassword: string;
preserveSettings(roomSetupSettingsList);
test.beforeEach(async ({ page }) => {
poAccountSecurity = new AccountSecurity(page);
poHomeChannel = new HomeChannel(page);
createE2EEChannel = new CreateE2EEChannel(page);
});
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false });
});
test.afterEach(async ({ api }) => {
await api.recreateContext();
});
test.afterAll(async () => {
await deletePrivateRoomsByName(ADMIN_CREDENTIALS, createdChannels);
});
test('expect save password state on encrypted room', async ({ page }) => {
await poAccountSecurity.goto();
await poAccountSecurity.securityE2EEncryptionSection.click();
await poAccountSecurity.securityE2EEncryptionResetKeyButton.click();
await page.locator('role=button[name="Login"]').waitFor();
await injectInitialData();
await restoreState(page, Users.admin);
await new Authenticated(page).goto();
await expect(poHomeChannel.bannerSaveEncryptionPassword).toBeVisible();
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible();
await expect(poHomeChannel.btnRoomSaveE2EEPassword).toBeVisible();
await poHomeChannel.roomToolbar.btnDisableE2EEncryption.waitFor();
await expect(poHomeChannel.roomToolbar.btnDisableE2EEncryption).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnMembers).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnRoomInfo).toBeVisible();
await expect(poHomeChannel.composer.inputMessage).not.toBeVisible();
await poHomeChannel.btnRoomSaveE2EEPassword.click();
e2eePassword = (await page.evaluate(() => localStorage.getItem('e2e.randomPassword'))) || 'undefined';
await expect(poHomeChannel.dialogSaveE2EEPassword).toBeVisible();
await expect(poHomeChannel.dialogSaveE2EEPassword).toContainText(e2eePassword);
await poHomeChannel.btnSavedMyPassword.click();
await poHomeChannel.composer.inputMessage.waitFor();
await poHomeChannel.content.sendMessage('hello world');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});
test('expect enter password state on encrypted room', async ({ page }) => {
const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page);
await poHomeChannel.goto();
// Logout to remove e2ee keys
await poHomeChannel.navbar.logout();
await injectInitialData();
await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] });
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible();
await poHomeChannel.btnRoomEnterE2EEPassword.waitFor();
await expect(poHomeChannel.btnRoomEnterE2EEPassword).toBeVisible();
await poHomeChannel.roomToolbar.btnDisableE2EEncryption.waitFor();
await expect(poHomeChannel.roomToolbar.btnDisableE2EEncryption).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnMembers).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnRoomInfo).toBeVisible();
await expect(poHomeChannel.composer.inputMessage).not.toBeVisible();
await poHomeChannel.btnRoomEnterE2EEPassword.click();
await enterE2EEPasswordModal.enterPassword(e2eePassword);
await expect(poHomeChannel.bannerEnterE2EEPassword).not.toBeVisible();
await poHomeChannel.composer.inputMessage.waitFor();
// For E2EE to complete init setup
await page.waitForTimeout(300);
await poHomeChannel.content.sendMessage('hello world');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await storeState(page, Users.admin);
});
test('expect waiting for room keys state', async ({ page }) => {
await poHomeChannel.goto();
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible();
await poHomeChannel.content.sendMessage('hello world');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await poHomeChannel.navbar.btnUserMenu.click();
await poHomeChannel.navbar.getUserProfileMenuOption('Profile').click();
await poAccountSecurity.sidebar.linkSecurity.click();
await poAccountSecurity.securityE2EEncryptionSection.click();
await poAccountSecurity.securityE2EEncryptionResetKeyButton.click();
await page.locator('role=button[name="Login"]').waitFor();
await page.reload();
await page.locator('role=button[name="Login"]').waitFor();
await injectInitialData();
await restoreState(page, Users.admin);
await poHomeChannel.navbar.typeSearch(channelName);
await poHomeChannel.navbar.getSearchRoomByName(channelName).click();
await poHomeChannel.btnRoomSaveE2EEPassword.click();
await poHomeChannel.btnSavedMyPassword.click();
await expect(poHomeChannel.composer.inputMessage).not.toBeVisible();
await expect(page.locator('.rcx-states__title')).toContainText('Check back later');
await poHomeChannel.roomToolbar.btnDisableE2EEncryption.waitFor();
await expect(poHomeChannel.roomToolbar.btnDisableE2EEncryption).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnMembers).toBeVisible();
await expect(poHomeChannel.roomToolbar.btnRoomInfo).toBeVisible();
});
});

View File

@@ -0,0 +1,90 @@
import { faker } from '@faker-js/faker';
import { resetOwnE2EKey } from './resetOwnE2EKey';
import { ADMIN_CREDENTIALS } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { EncryptedRoomPage } from '../page-objects/encrypted-room';
import { CreateE2EEChannel } from '../page-objects/fragments/e2ee';
import { ExportMessagesFlexTab } from '../page-objects/fragments/flextabs';
import { LoginPage } from '../page-objects/login';
import { deletePrivateRoomsByName } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
preserveSettings(settingsList);
test.describe('E2EE PDF Export', () => {
const createdChannels: string[] = [];
let createE2EEChannel: CreateE2EEChannel;
test.use({ storageState: Users.admin.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
// Note: Using admin user, so no need for userE2EE cleanup
});
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await expect(await resetOwnE2EKey(ADMIN_CREDENTIALS)).toBeOK();
await loginPage.goto();
await loginPage.loginByUserState(Users.admin);
createE2EEChannel = new CreateE2EEChannel(page);
});
test.afterAll(async () => {
await deletePrivateRoomsByName(ADMIN_CREDENTIALS, createdChannels);
});
test('should display only the download file method when exporting messages in an e2ee room', async ({ page }) => {
const encryptedRoomPage = new EncryptedRoomPage(page);
const exportMessagesTab = new ExportMessagesFlexTab(page);
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(encryptedRoomPage.encryptedRoomHeaderIcon).toBeVisible();
await encryptedRoomPage.showExportMessagesTab();
await expect(exportMessagesTab.method).toContainClass('disabled'); // FIXME: looks like the component have an a11y issue
await expect(exportMessagesTab.method).toHaveAccessibleName('Download file Method');
});
test('should allow exporting messages as PDF in an encrypted room', async ({ page }) => {
const encryptedRoomPage = new EncryptedRoomPage(page);
const exportMessagesTab = new ExportMessagesFlexTab(page);
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(encryptedRoomPage.encryptedRoomHeaderIcon).toBeVisible();
await encryptedRoomPage.sendMessage('This is a message to export as PDF.');
await encryptedRoomPage.showExportMessagesTab();
await expect(exportMessagesTab.method).toHaveAccessibleName('Download file Method');
// Select Output format as PDF
await exportMessagesTab.setOutputFormat('PDF');
// select messages to be exported
await exportMessagesTab.selectAllMessages();
// Wait for download event and match format
const download = await exportMessagesTab.downloadMessages();
expect(download.suggestedFilename()).toMatch(/\.pdf$/);
});
});

View File

@@ -0,0 +1,111 @@
import { faker } from '@faker-js/faker';
import { DEFAULT_USER_CREDENTIALS, IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
import { HomeChannel } from '../page-objects';
import { CreateE2EEChannel } from '../page-objects/fragments/e2ee';
import { deletePrivateRoomsByName } from '../utils';
import { preserveSettings } from '../utils/preserveSettings';
import { test, expect } from '../utils/test';
const settingsList = [
'E2E_Enable',
'E2E_Allow_Unencrypted_Messages',
'E2E_Enabled_Default_DirectRooms',
'E2E_Enabled_Default_PrivateRooms',
];
preserveSettings(settingsList);
test.describe('E2EE Server Settings', () => {
const createdChannels: string[] = [];
let poHomeChannel: HomeChannel;
let createE2EEChannel: CreateE2EEChannel;
test.use({ storageState: Users.userE2EE.state });
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enabled_Default_DirectRooms', { value: false });
await api.post('/settings/E2E_Enabled_Default_PrivateRooms', { value: false });
await api.post('/im.delete', { username: 'user2' });
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
createE2EEChannel = new CreateE2EEChannel(page);
await poHomeChannel.goto();
});
test.afterAll(async () => {
await deletePrivateRoomsByName(
{ username: Users.userE2EE.data.username, password: DEFAULT_USER_CREDENTIALS.password },
createdChannels,
);
});
test('expect slash commands to be enabled in an e2ee room', async ({ page }) => {
test.skip(!IS_EE, 'Premium Only');
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await page.locator('[name="msg"]').type('/');
await expect(page.locator('#popup-item-contextualbar')).not.toHaveClass(/disabled/);
await page.locator('[name="msg"]').clear();
await poHomeChannel.content.dispatchSlashCommand('/contextualbar');
await expect(poHomeChannel.btnContextualbarClose).toBeVisible();
await poHomeChannel.btnContextualbarClose.click();
await expect(poHomeChannel.btnContextualbarClose).toBeHidden();
});
test.describe('un-encrypted messages not allowed in e2ee rooms', () => {
test.skip(!IS_EE, 'Premium Only');
let poHomeChannel: HomeChannel;
let createE2EEChannel: CreateE2EEChannel;
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
createE2EEChannel = new CreateE2EEChannel(page);
await poHomeChannel.goto();
});
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
});
test('expect slash commands to be disabled in an e2ee room', async ({ page }) => {
const channelName = faker.string.uuid();
await createE2EEChannel.createAndStore(channelName, createdChannels);
await expect(page).toHaveURL(`/group/${channelName}`);
await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();
await poHomeChannel.content.sendMessage('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await page.locator('[name="msg"]').pressSequentially('/');
await expect(page.locator('#popup-item-contextualbar')).toHaveClass(/disabled/);
});
});
});

View File

@@ -0,0 +1,28 @@
import { request as baseRequest } from '@playwright/test';
import { BASE_API_URL } from '../config/constants';
type Credentials = {
password: string;
username?: string;
email?: string;
};
export const resetOwnE2EKey = async (credentials: Credentials) => {
const request = await baseRequest.newContext();
try {
const loginResponse = await request.post(`${BASE_API_URL}/login`, { data: credentials });
const { data } = await loginResponse.json();
return await request.post(`${BASE_API_URL}/users.resetE2EKey`, {
data: { userId: data.userId },
headers: {
'X-Auth-Token': data.authToken,
'X-User-Id': data.userId,
},
});
} finally {
await request.dispose();
}
};

View File

@@ -0,0 +1,19 @@
import type { Page } from '@playwright/test';
import { SaveE2EEPasswordBanner } from '../page-objects/fragments/e2ee';
import { SaveE2EEPasswordModal } from '../page-objects/fragments/modals';
/**
* Click the banner to open the dialog to save the generated password
*/
export const setupE2EEPassword = async (page: Page) => {
const saveE2EEPasswordBanner = new SaveE2EEPasswordBanner(page);
const saveE2EEPasswordModal = new SaveE2EEPasswordModal(page);
await saveE2EEPasswordBanner.click();
const password = await saveE2EEPasswordModal.getPassword();
await saveE2EEPasswordModal.confirm();
await saveE2EEPasswordBanner.waitForDisappearance();
return password;
};

View File

@@ -0,0 +1,52 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { AdminEmailInboxes } from './page-objects';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('email-inboxes', () => {
let poAdminEmailInboxes: AdminEmailInboxes;
const email = faker.internet.email();
test.beforeEach(async ({ page }) => {
poAdminEmailInboxes = new AdminEmailInboxes(page);
await poAdminEmailInboxes.goto();
});
test('expect create an email inbox', async () => {
await poAdminEmailInboxes.btnNewEmailInbox.click();
const name = faker.person.firstName();
await poAdminEmailInboxes.inputName.type(name);
await poAdminEmailInboxes.inputEmail.type(email);
// SMTP
await poAdminEmailInboxes.inputSmtpServer.type(faker.internet.domainName());
await poAdminEmailInboxes.inputSmtpUsername.type(faker.internet.userName());
await poAdminEmailInboxes.inputSmtpPassword.type(faker.internet.password());
await poAdminEmailInboxes.inputSmtpSecure.click();
// IMAP
await poAdminEmailInboxes.inputImapServer.type(faker.internet.domainName());
await poAdminEmailInboxes.inputImapUsername.type(faker.internet.userName());
await poAdminEmailInboxes.inputImapPassword.type(faker.internet.password());
await poAdminEmailInboxes.inputImapSecure.click();
await poAdminEmailInboxes.btnSave.click();
await expect(poAdminEmailInboxes.itemRow(name)).toBeVisible();
});
test('expect delete an email inbox', async ({ page }) => {
await poAdminEmailInboxes.deleteEmailInboxByName(email);
// await poAdminEmailInboxes.itemRow(email).click();
// await poAdminEmailInboxes.btnDelete.click();
// await poUtils.btnModalConfirmDelete.click();
// await expect(poUtils.toastBarSuccess).toBeVisible();
await expect(page.locator('text=No results found')).toBeVisible();
});
});

View File

@@ -0,0 +1,137 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createDirectMessage, createTargetChannel, deleteChannel } from './utils';
import { test, expect } from './utils/test';
const embeddedLayoutURL = (pageUrl: string) => `${pageUrl}?layout=embedded`;
test.use({ storageState: Users.user1.state });
test.describe('embedded-layout', () => {
let poHomeChannel: HomeChannel;
let targetChannelId: string;
let notMemberChannelId: string;
let joinChannelId: string;
test.beforeAll(async ({ api }) => {
[targetChannelId, notMemberChannelId, joinChannelId] = await Promise.all([
createTargetChannel(api, { members: ['user1'] }),
createTargetChannel(api, { members: ['user2'] }),
createTargetChannel(api, { members: ['user2'] }),
]);
});
test.afterAll(async ({ api }) => {
await Promise.all([deleteChannel(api, targetChannelId), deleteChannel(api, notMemberChannelId), deleteChannel(api, joinChannelId)]);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
});
test.describe('Layout elements visibility', () => {
test('should hide primary navigation elements in embedded layout', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannelId);
await expect(poHomeChannel.roomHeaderToolbar).toBeVisible();
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.roomHeaderToolbar).not.toBeVisible();
await poHomeChannel.sidebar.waitForDismissal();
});
test.describe('should show room header toolbar when show top navbar setting is enabled', () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/UI_Show_top_navbar_embedded_layout', { value: true });
});
test.afterAll(async ({ api }) => {
await api.post('/settings/UI_Show_top_navbar_embedded_layout', { value: false });
});
test('should show room header toolbar as top navbar when setting enabled', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannelId);
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.roomHeaderToolbar).toBeVisible();
});
});
});
test.describe('Channel member functionality', () => {
test('should hide join button and enable messaging for members', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannelId);
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.composer.inputMessage).toBeVisible();
await expect(poHomeChannel.composer.inputMessage).toBeEnabled();
await expect(poHomeChannel.composer.btnJoinRoom).not.toBeVisible();
});
test('should allow sending and receiving messages', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannelId);
await page.goto(embeddedLayoutURL(page.url()));
const testMessage = `Embedded test message ${Date.now()}`;
await poHomeChannel.content.sendMessage(testMessage);
await expect(poHomeChannel.content.lastUserMessage).toContainText(testMessage);
});
test('should preserve message composer functionality', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannelId);
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.composer.inputMessage).toBeVisible();
await expect(poHomeChannel.composer.toolbarPrimaryActions).toBeVisible();
await poHomeChannel.composer.inputMessage.fill('Test message');
await expect(poHomeChannel.composer.inputMessage).toHaveValue('Test message');
await poHomeChannel.composer.inputMessage.fill('');
await expect(poHomeChannel.composer.inputMessage).toHaveValue('');
});
});
test.describe('Channel non-member functionality', () => {
test('should display join button and disable composer for non-members', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.navbar.openChat(notMemberChannelId);
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.composer.inputMessage).toBeDisabled();
await expect(poHomeChannel.composer.btnJoinRoom).toBeVisible();
});
test('should allow joining channel and enable messaging', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.navbar.openChat(joinChannelId);
await page.goto(embeddedLayoutURL(page.url()));
await poHomeChannel.composer.btnJoinRoom.click();
await expect(poHomeChannel.composer.btnJoinRoom).not.toBeVisible();
await expect(poHomeChannel.composer.inputMessage).toBeVisible();
await expect(poHomeChannel.composer.inputMessage).toBeEnabled();
const joinMessage = `Joined and sent message ${Date.now()}`;
await poHomeChannel.content.sendMessage(joinMessage);
await expect(poHomeChannel.content.lastUserMessage).toContainText(joinMessage);
});
});
test.describe('Direct message functionality', () => {
test('should allow sending direct messages', async ({ page, api }) => {
await createDirectMessage(api);
await poHomeChannel.goto();
await poHomeChannel.navbar.openChat(Users.user2.data.username);
await page.goto(embeddedLayoutURL(page.url()));
await expect(poHomeChannel.composer.inputMessage).toBeVisible();
await expect(poHomeChannel.composer.inputMessage).toBeEnabled();
await expect(poHomeChannel.composer.btnJoinRoom).not.toBeVisible();
const dmMessage = `Embedded DM test ${Date.now()}`;
await poHomeChannel.content.sendMessage(dmMessage);
await expect(poHomeChannel.content.lastUserMessage).toContainText(dmMessage);
});
});
});

View File

@@ -0,0 +1,106 @@
import { Users } from './fixtures/userStates';
import { HomeChannel, AdminEmoji } from './page-objects';
import { createTargetChannel } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('emoji', () => {
let poHomeChannel: HomeChannel;
let poAdminEmoji: AdminEmoji;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test('should display emoji picker properly', async ({ page }) => {
await poHomeChannel.composer.btnEmoji.click();
await test.step('should display scroller', async () => {
await expect(poHomeChannel.scrollerEmojiPicker).toBeVisible();
});
await test.step('should focus the active emoji tab category', async () => {
const activityEmojiTab = poHomeChannel.getEmojiPickerTabByName('Activity');
await activityEmojiTab.click();
//Picker search input can steal focus and cause failures here
await expect(activityEmojiTab).toHaveAttribute('aria-selected', 'true');
await poHomeChannel.composer.inputMessage.click(); // To close the emoji picker
});
await test.step('should pick and send grinning emoji', async () => {
await poHomeChannel.pickEmoji('grinning');
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserMessage).toContainText('😀');
});
});
test('expect send emoji via text', async ({ page }) => {
await poHomeChannel.content.sendMessage(':innocent:');
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserMessage).toContainText('😇');
});
test('expect render special characters and numbers properly', async () => {
await poHomeChannel.content.sendMessage('® © ™ # *');
await expect(poHomeChannel.content.lastUserMessage).toContainText('® © ™ # *');
});
test('should add a custom emoji, send it, rename it, and check render', async ({ page }) => {
const emojiName = 'customemoji';
const newEmojiName = 'renamedemoji';
const emojiUrl = './tests/e2e/fixtures/files/test-image.jpeg';
poAdminEmoji = new AdminEmoji(page);
await test.step('Add custom emoji', async () => {
await poHomeChannel.navbar.openManageMenuItem('Workspace');
await poAdminEmoji.sidebar.linkEmoji.click();
await poAdminEmoji.btnNew.click();
await poAdminEmoji.addEmojiFlexTab.inputName.fill(emojiName);
const [fileChooser] = await Promise.all([page.waitForEvent('filechooser'), page.locator('role=button[name="Custom Emoji"]').click()]);
await fileChooser.setFiles(emojiUrl);
await poAdminEmoji.addEmojiFlexTab.save();
await poAdminEmoji.sidebar.close();
await poHomeChannel.gotoChannel(targetChannel);
await poHomeChannel.content.sendMessage(`:${emojiName}:`);
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserMessage.getByTitle(`:${emojiName}:`)).toBeVisible();
});
await test.step('Rename custom emoji', async () => {
await poHomeChannel.navbar.openManageMenuItem('Workspace');
await poAdminEmoji.sidebar.linkEmoji.click();
await (await poAdminEmoji.findEmojiByName(emojiName)).click();
await poAdminEmoji.editEmojiFlexTab.inputName.fill(newEmojiName);
await poAdminEmoji.editEmojiFlexTab.save();
await poAdminEmoji.sidebar.close();
await poHomeChannel.gotoChannel(targetChannel);
await poHomeChannel.content.sendMessage(`:${newEmojiName}:`);
await page.keyboard.press('Enter');
await expect(poHomeChannel.content.lastUserMessage.getByTitle(`:${newEmojiName}:`)).toBeVisible();
});
await test.step('Delete custom emoji', async () => {
await poHomeChannel.navbar.openManageMenuItem('Workspace');
await poAdminEmoji.sidebar.linkEmoji.click();
await poAdminEmoji.deleteEmoji(newEmojiName);
await expect(await poAdminEmoji.findEmojiByName(newEmojiName)).not.toBeVisible();
});
});
});

View File

@@ -0,0 +1,106 @@
import { IS_EE } from './config/constants';
import { Users } from './fixtures/userStates';
import { HomeChannel, AccountSecurity } from './page-objects';
import { createCustomRole, deleteCustomRole } from './utils/custom-role';
import { setSettingValueById } from './utils/setSettingValueById';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe('enforce two factor authentication', () => {
test.skip(!IS_EE, 'Enterprise Only');
let poHomeChannel: HomeChannel;
let poAccountSecurity: AccountSecurity;
let customRoleId = '';
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
poAccountSecurity = new AccountSecurity(page);
});
test.beforeAll(async ({ api }) => {
const roleResponse = await createCustomRole(api, { name: 'enforce-2FA', mandatory2fa: true });
expect(roleResponse.status()).toBe(200);
const { role } = await roleResponse.json();
customRoleId = role._id;
const userUpdateRes = await api.post('/users.update', {
data: { roles: ['user', customRoleId, 'admin'] },
userId: 'rocketchat.internal.admin.test',
});
expect(userUpdateRes.status()).toBe(200);
const disableEmail2FA = await api.post('/users.2fa.disableEmail', {});
expect(disableEmail2FA.status()).toBe(200);
});
test.afterAll(async ({ api }) => {
const userUpdateRes = await api.post('/users.update', {
data: { roles: ['user', 'admin'] },
userId: 'rocketchat.internal.admin.test',
});
expect(userUpdateRes.status()).toBe(200);
const deleteRole = await deleteCustomRole(api, 'enforce-2FA');
expect(deleteRole.status()).toBe(200);
const enableEmail2FA = await api.post('/users.2fa.enableEmail', {});
expect(enableEmail2FA.status()).toBe(200);
});
test('should redirect to 2FA setup page and setup email 2FA', async () => {
await poHomeChannel.gotoExpecting(poAccountSecurity.required2faModalSetUpButton);
await poAccountSecurity.required2faModalSetUpButton.click();
await expect(poHomeChannel.navbar.btnHome).not.toBeVisible();
await expect(poAccountSecurity.securityHeader).toBeVisible();
await expect(poAccountSecurity.security2FASection).toHaveAttribute('aria-expanded', 'true');
await expect(poAccountSecurity.email2FASwitch).toBeVisible();
await poAccountSecurity.email2FASwitch.click();
await poHomeChannel.toastMessage.waitForDisplay();
await expect(poHomeChannel.navbar.btnHome).toBeVisible();
await expect(poAccountSecurity.securityHeader).not.toBeVisible();
});
test.describe('should still redirect to 2FA setup page when email 2FA is disabled', () => {
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', false);
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', true);
});
test('should redirect to 2FA setup page and show totp 2FA setup', async () => {
await poHomeChannel.gotoExpecting(poAccountSecurity.required2faModalSetUpButton);
await poAccountSecurity.required2faModalSetUpButton.click();
await expect(poHomeChannel.navbar.btnHome).not.toBeVisible();
await expect(poAccountSecurity.securityHeader).toBeVisible();
await expect(poAccountSecurity.security2FASection).toHaveAttribute('aria-expanded', 'true');
await expect(poAccountSecurity.totp2FASwitch).toBeVisible();
await expect(poAccountSecurity.email2FASwitch).not.toBeVisible();
});
});
test.describe('should not redirect to 2FA setup page when both email and totp 2FA are disabled', () => {
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', false);
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_TOTP_Enabled', false);
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', true);
await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_TOTP_Enabled', true);
});
test('should not redirect to 2FA setup page', async () => {
await poHomeChannel.goto();
await expect(poHomeChannel.navbar.btnHome).toBeVisible();
await expect(poAccountSecurity.securityHeader).not.toBeVisible();
});
});
});

View File

@@ -0,0 +1,164 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel, deleteChannel } from './utils';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
const uniqueMessage = (): string => `msg-${faker.string.uuid()}`;
test.describe('export-messages', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test.afterAll(async ({ api }) => {
await Promise.all([
api.post('/users.setPreferences', { userId: 'rocketchat.internal.admin.test', data: { hideFlexTab: false } }),
deleteChannel(api, targetChannel),
]);
});
test('should all export methods be available in targetChannel', async () => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await poHomeChannel.tabs.exportMessages.exposeMethods();
await expect(poHomeChannel.tabs.exportMessages.getMethodOptionByName('Send email')).toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getMethodOptionByName('Send file via email')).toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getMethodOptionByName('Download file')).toBeVisible();
});
test('should display export output format correctly depending on the selected method', async () => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
// TODO: Fix the base component to have a disabled statement and not only a class attribute
// Here we are checking for a button because the internal select element is not accessible
// and the higher component that is a button doesn't appear as disabled.
await expect(poHomeChannel.tabs.exportMessages.outputFormat).toContainClass('disabled');
await poHomeChannel.tabs.exportMessages.setMethod('Send file via email');
await poHomeChannel.tabs.exportMessages.exposeOutputFormats();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('html')).toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('json')).toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('pdf')).not.toBeVisible();
await poHomeChannel.tabs.exportMessages.setOutputFormat('html');
await poHomeChannel.tabs.exportMessages.setMethod('Download file');
await poHomeChannel.tabs.exportMessages.exposeOutputFormats();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('html')).not.toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('json')).toBeVisible();
await expect(poHomeChannel.tabs.exportMessages.getOutputFormatOptionByName('pdf')).toBeVisible();
});
test('when trying to send email without filling to users or to additional emails, should mark both fields as invalid', async () => {
const testMessage = uniqueMessage();
await poHomeChannel.content.sendMessage(testMessage);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await expect(poHomeChannel.btnContextualbarClose).toBeVisible();
await poHomeChannel.content.getMessageByText(testMessage).click();
await poHomeChannel.tabs.exportMessages.send();
const usersField = poHomeChannel.tabs.exportMessages.inputUsers;
const additionalEmailsField = poHomeChannel.tabs.exportMessages.inputAdditionalEmails;
await expect(usersField).toHaveAttribute('aria-invalid', 'true');
await expect(additionalEmailsField).toHaveAttribute('aria-invalid', 'true');
const errorMessages = poHomeChannel.tabs.exportMessages.errorMessage(
'You must select one or more users or provide one or more email addresses, separated by commas',
);
await expect(errorMessages).toHaveCount(2);
});
test('should display an error when trying to send email without selecting any message', async ({ page }) => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await poHomeChannel.tabs.exportMessages.setAdditionalEmail('mail@mail.com');
await poHomeChannel.tabs.exportMessages.send();
await expect(
page.locator('[role="alert"]', {
hasText: `You haven't selected any messages`,
}),
).toBeVisible();
});
test('should be able to send messages after closing export messages', async () => {
const message1 = uniqueMessage();
const message2 = uniqueMessage();
await poHomeChannel.content.sendMessage(message1);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await poHomeChannel.content.getMessageByText(message1).click();
await poHomeChannel.btnContextualbarClose.click();
await poHomeChannel.content.sendMessage(message2);
await expect(poHomeChannel.content.getMessageByText(message2)).toBeVisible();
});
test('should be able to select a single message to export', async () => {
const message1 = uniqueMessage();
const message2 = uniqueMessage();
await poHomeChannel.content.sendMessage(message1);
await poHomeChannel.content.sendMessage(message2);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await poHomeChannel.tabs.exportMessages.waitForDisplay();
await poHomeChannel.content.getMessageByText(message1).click();
await expect(poHomeChannel.tabs.exportMessages.getMessageCheckbox(message1)).toBeChecked();
await expect(poHomeChannel.tabs.exportMessages.getMessageCheckbox(message2)).not.toBeChecked();
await expect(poHomeChannel.tabs.exportMessages.clearSelectionButton).toBeEnabled();
await expect(poHomeChannel.tabs.exportMessages.sendButton).toBeEnabled();
});
test('should be able to select a single message to export with hide contextual bar preference enabled', async ({ api }) => {
await api.post('/users.setPreferences', {
userId: 'rocketchat.internal.admin.test',
data: { hideFlexTab: true },
});
const message1 = uniqueMessage();
const message2 = uniqueMessage();
await poHomeChannel.content.sendMessage(message1);
await poHomeChannel.content.sendMessage(message2);
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemExportMessages.click();
await poHomeChannel.tabs.exportMessages.waitForDisplay();
await poHomeChannel.content.getMessageByText(message1).click();
await expect(poHomeChannel.tabs.exportMessages.getMessageCheckbox(message1)).toBeChecked();
await expect(poHomeChannel.tabs.exportMessages.clearSelectionButton).toBeEnabled();
await expect(poHomeChannel.tabs.exportMessages.sendButton).toBeEnabled();
});
});

View File

@@ -0,0 +1,620 @@
import { faker } from '@faker-js/faker';
import { IS_EE } from './config/constants';
import { Users } from './fixtures/userStates';
import { AccountFeaturePreview, AdminInfo, HomeChannel, HomeDiscussion, HomeTeam } from './page-objects';
import { CreateNewChannelModal, CreateNewDiscussionModal } from './page-objects/fragments/modals';
import {
createTargetChannel,
createTargetTeam,
deleteChannel,
deleteTeam,
setSettingValueById,
createTargetDiscussion,
deleteRoom,
} from './utils';
import { setUserPreferences } from './utils/setUserPreferences';
import { test, expect } from './utils/test';
test.use({ storageState: Users.admin.state });
test.describe.serial('feature preview', () => {
let poHomeChannel: HomeChannel;
let poHomeTeam: HomeTeam;
let targetChannel: string;
let targetDiscussion: Record<string, string>;
let sidepanelTeam: string;
const targetChannelNameInTeam = `channel-from-team-${faker.number.int()}`;
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true);
targetChannel = await createTargetChannel(api, { members: ['user1'] });
targetDiscussion = await createTargetDiscussion(api);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: true,
},
],
});
});
test.afterAll(async ({ api }) => {
// await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false);
await deleteChannel(api, targetChannel);
await deleteRoom(api, targetDiscussion._id);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: false,
},
],
});
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
poHomeTeam = new HomeTeam(page);
});
test('should show "Navigation" feature section', async ({ page }) => {
await new AccountFeaturePreview(page).goto();
await expect(page.getByRole('main').getByRole('button', { name: 'Navigation' })).toBeVisible();
});
test.describe('Sidepanel', () => {
let newChannelModal: CreateNewChannelModal;
test.beforeEach(async ({ api }) => {
sidepanelTeam = await createTargetTeam(api);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: true,
},
{
name: 'secondarySidebar',
value: true,
},
],
});
});
test.afterEach(async ({ api }) => {
await deleteTeam(api, sidepanelTeam);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: false,
},
{
name: 'secondarySidebar',
value: false,
},
],
});
});
test('should display new channel from team on the sidepanel', async ({ page, api }) => {
newChannelModal = new CreateNewChannelModal(page);
await poHomeTeam.gotoGroup(sidepanelTeam);
await poHomeTeam.headerToolbar.openTeamChannels();
await poHomeTeam.tabs.channels.btnCreateNew.click();
await newChannelModal.inputName.fill(targetChannelNameInTeam);
await newChannelModal.checkboxPrivate.click();
await newChannelModal.btnCreate.click();
await expect(poHomeTeam.sidepanel.sidepanelList).toBeVisible();
await expect(poHomeTeam.sidepanel.getItemByName(targetChannelNameInTeam)).toBeVisible();
await deleteChannel(api, targetChannelNameInTeam);
});
test('should display sidepanel item with the same display preference as the sidebar', async () => {
await poHomeChannel.goto();
const message = 'hello world';
await poHomeChannel.navbar.openChat(sidepanelTeam);
await poHomeChannel.content.sendMessage(message);
await expect(poHomeChannel.sidepanel.getItemByName(sidepanelTeam)).toBeVisible();
});
test('should escape special characters on item subtitle', async () => {
await poHomeChannel.goto();
const message = 'hello > world';
const parsedWrong = 'hello &gt; world';
await poHomeChannel.navbar.openChat(sidepanelTeam);
await poHomeChannel.content.sendMessage(message);
await expect(poHomeChannel.sidepanel.getItemByName(sidepanelTeam)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(sidepanelTeam)).not.toHaveText(parsedWrong);
});
test('should show channel in sidepanel after adding existing one', async () => {
await poHomeTeam.gotoGroup(sidepanelTeam);
await poHomeTeam.headerToolbar.openTeamChannels();
await poHomeTeam.tabs.channels.addExistingChannel(targetChannel);
await expect(poHomeTeam.sidepanel.getItemByName(targetChannel)).toBeVisible();
});
test('should keep the main room on the top even if child has unread messages', async ({ page, browser }) => {
const user1Page = await browser.newPage({ storageState: Users.user1.state });
const user1Channel = new HomeChannel(user1Page);
await poHomeTeam.gotoGroup(sidepanelTeam);
await poHomeTeam.headerToolbar.openTeamChannels();
await poHomeTeam.tabs.channels.addExistingChannel(targetChannel);
const sidepanelTeamItem = poHomeTeam.sidepanel.getTeamItemByName(sidepanelTeam);
const targetChannelItem = poHomeTeam.sidepanel.getTeamItemByName(targetChannel);
await targetChannelItem.click();
expect(page.url()).toContain(`/channel/${targetChannel}`);
await poHomeTeam.content.sendMessage('hello channel');
await expect(async () => {
await sidepanelTeamItem.focus();
await sidepanelTeamItem.click();
expect(page.url()).toContain(`/group/${sidepanelTeam}`);
}).toPass();
await poHomeTeam.content.sendMessage('hello team');
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.openReplyInThread();
await user1Channel.content.toggleAlsoSendThreadToChannel(false);
await user1Channel.content.sendMessageInThread('hello thread');
const item = poHomeTeam.sidepanel.getTeamItemByName(targetChannel);
await expect(item.locator('..')).toHaveAttribute('data-item-index', '1');
await user1Page.close();
});
test('sidepanel should open the respective parent room filter if its a room filter', async ({ page }) => {
await poHomeChannel.gotoChannel(targetChannel);
await poHomeChannel.waitForRoomLoad();
await poHomeChannel.sidebar.getFilterItemByName(sidepanelTeam).click();
await poHomeChannel.content.waitForChannel();
await expect(page).toHaveURL(`/group/${sidepanelTeam}`);
await expect(poHomeChannel.sidepanel.getSidepanelHeader(sidepanelTeam)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(sidepanelTeam)).toHaveAttribute('aria-current', 'page');
});
test('sidepanel should not open the respective parent room filter if its not a room filter', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Favorites')).toBeVisible();
await expect(poHomeChannel.navbar.btnHome).toBeVisible();
await poHomeChannel.navbar.openChat(sidepanelTeam);
await poHomeChannel.content.waitForChannel();
await expect(page).toHaveURL(`/group/${sidepanelTeam}`);
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Favorites')).toBeVisible();
await expect(poHomeChannel.sidebar.favoritesTeamCollabFilter).toHaveAttribute('aria-selected', 'true');
});
test('should show all filters and tablist on sidepanel', async () => {
await poHomeChannel.goto();
await expect(poHomeChannel.sidebar.teamCollabFilters).toBeVisible();
await expect(poHomeChannel.sidebar.omnichannelFilters).toBeVisible();
await expect(poHomeChannel.sidebar.allTeamCollabFilter).toBeVisible();
await expect(poHomeChannel.sidebar.favoritesTeamCollabFilter).toBeVisible();
await expect(poHomeChannel.sidebar.discussionsTeamCollabFilter).toBeVisible();
});
test('should show favorite team on the sidepanel', async () => {
test.skip(IS_EE);
await poHomeChannel.gotoGroup(sidepanelTeam);
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.getTeamItemByName(sidepanelTeam)).not.toBeVisible();
await poHomeChannel.getRoomHeaderFavoriteBtn(IS_EE).click();
await expect(poHomeChannel.sidepanel.getTeamItemByName(sidepanelTeam)).toBeVisible();
});
test('should show discussion in discussions and all sidepanel filter, should remove after deleting discussion', async ({ page }) => {
const poHomeDiscussion = new HomeDiscussion(page);
await poHomeChannel.gotoGroup(sidepanelTeam);
const discussionName = faker.string.uuid();
await poHomeChannel.composer.btnMenuMoreActions.click();
await page.getByRole('menuitem', { name: 'Discussion' }).click();
const createDiscussionModal = new CreateNewDiscussionModal(page);
await createDiscussionModal.inputName.fill(discussionName);
await createDiscussionModal.create();
await expect(page.getByRole('heading', { name: discussionName })).toBeVisible();
await poHomeDiscussion.sidebar.discussionsTeamCollabFilter.click();
await expect(poHomeDiscussion.sidepanel.getItemByName(discussionName)).toBeVisible();
await poHomeDiscussion.sidebar.allTeamCollabFilter.click();
await expect(poHomeDiscussion.sidepanel.getItemByName(discussionName)).toBeVisible();
await poHomeDiscussion.roomToolbar.openRoomInfo();
await poHomeDiscussion.tabs.room.deleteRoom();
await poHomeDiscussion.sidebar.discussionsTeamCollabFilter.click();
await expect(poHomeDiscussion.sidepanel.getItemByName(discussionName)).not.toBeVisible();
await poHomeDiscussion.sidebar.allTeamCollabFilter.click();
await expect(poHomeDiscussion.sidepanel.getItemByName(discussionName)).not.toBeVisible();
});
test('should persist sidepanel state after page reload', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.sidebar.discussionsTeamCollabFilter.click();
await poHomeChannel.sidepanel.unreadToggleLabel.click({ force: true });
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Discussions')).toBeVisible();
await page.reload();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Discussions')).toBeVisible();
});
test('should show unread filter for thread messages', async ({ browser }) => {
const user1Page = await browser.newPage({ storageState: Users.user1.state });
const user1Channel = new HomeChannel(user1Page);
await test.step('mark all rooms as read', async () => {
await poHomeChannel.goto();
await poHomeChannel.content.markAllRoomsAsRead();
});
await poHomeChannel.gotoChannel(targetChannel);
await poHomeChannel.content.sendMessage('test thread message');
await poHomeChannel.navbar.btnHome.click();
await poHomeChannel.sidebar.allTeamCollabFilter.click();
await poHomeChannel.sidepanel.unreadToggleLabel.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await test.step('send a thread message from another user', async () => {
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.openReplyInThread();
await user1Channel.content.toggleAlsoSendThreadToChannel(false);
await user1Channel.content.sendMessageInThread('hello thread');
});
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible();
await expect(
poHomeChannel.sidepanel.getItemByName(targetChannel).getByRole('status', { name: '1 unread threaded message' }),
).toBeVisible();
await poHomeChannel.sidepanel.getItemByName(targetChannel).click();
await poHomeChannel.content.waitForChannel();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible();
await expect(
poHomeChannel.sidepanel.getItemByName(targetChannel).getByRole('status', { name: '1 unread threaded message' }),
).toBeVisible();
await poHomeChannel.content.openReplyInThread();
await user1Page.close();
});
test('unread mentions badges on filters', async ({ browser }) => {
test.skip(IS_EE);
const user1Page = await browser.newPage({ storageState: Users.user1.state });
const user1Channel = new HomeChannel(user1Page);
await test.step('mark all rooms as read', async () => {
await poHomeChannel.goto();
await poHomeChannel.content.markAllRoomsAsRead();
});
await test.step('should favorite the target channel', async () => {
await poHomeChannel.gotoChannel(targetChannel);
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).not.toBeVisible();
await poHomeChannel.getRoomHeaderFavoriteBtn(IS_EE).click();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible();
});
await test.step('unread state should be empty', async () => {
await poHomeChannel.sidebar.allTeamCollabFilter.click();
await poHomeChannel.sidepanel.unreadToggleLabel.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
});
await poHomeChannel.navbar.btnHome.click();
await test.step('send a mention message from another user', async () => {
await user1Channel.gotoChannel(targetChannel);
await user1Channel.content.sendMessage(`hello @${Users.admin.data.username}`);
});
await test.step('unread mentions badge should be visible on all filters', async () => {
await poHomeChannel.sidebar.allTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel).getByRole('status', { name: '1 mention' })).toBeVisible();
await expect(poHomeChannel.sidebar.allTeamCollabFilter.getByRole('status', { name: '1 mention from All' })).toBeVisible();
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(targetChannel).getByRole('status', { name: '1 mention' })).toBeVisible();
await expect(
poHomeChannel.sidebar.favoritesTeamCollabFilter.getByRole('status', { name: '1 mention from Favorites' }),
).toBeVisible();
});
await test.step('read the room', async () => {
await poHomeChannel.sidepanel.getItemByName(targetChannel).click();
await poHomeChannel.content.waitForChannel();
});
await test.step('unread mentions badge should not be visible on any filters', async () => {
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await poHomeChannel.sidebar.allTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
});
await user1Page.close();
});
test('should persist sidepanel state after switching admin panel', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.sidebar.discussionsTeamCollabFilter.click();
await poHomeChannel.sidepanel.unreadToggleLabel.click();
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Discussions')).toBeVisible();
await poHomeChannel.navbar.openAdminPanel();
await expect(page).toHaveURL(/\/admin/);
const adminPage = new AdminInfo(page);
await adminPage.sidebar.btnClose.click();
await expect(page).toHaveURL(/\/home/);
await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Discussions')).toBeVisible();
});
test.describe('sidebar and sidepanel in small viewport', () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize({ width: 640, height: 460 });
});
test('should show button to toggle sidebar/sidepanel', async () => {
await poHomeChannel.goto();
await poHomeChannel.sidebar.waitForDismissal();
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
await expect(poHomeChannel.navbar.btnSidebarToggler()).toBeVisible();
});
test('should toggle sidebar/sidepanel when clicking the button', async () => {
await poHomeChannel.goto();
await poHomeChannel.navbar.btnSidebarToggler().click();
await poHomeChannel.sidebar.waitForDismissal();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await poHomeChannel.navbar.btnSidebarToggler(true).click();
await poHomeChannel.sidebar.waitForDismissal();
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
});
test('toggle sidebar and sidepanel', async () => {
await poHomeChannel.goto();
await poHomeChannel.navbar.btnSidebarToggler().click();
await expect(poHomeChannel.sidepanel.sidepanelBackButton).toBeVisible();
await poHomeChannel.sidepanel.sidepanelBackButton.click();
await poHomeChannel.sidebar.waitForDisplay();
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
await poHomeChannel.sidebar.favoritesTeamCollabFilter.click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await expect(poHomeChannel.sidepanel.getSidepanelHeader('Favorites')).toBeVisible();
});
test('should close nav region when clicking outside of it', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.navbar.btnSidebarToggler().click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await page.click('main', { force: true });
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
await poHomeChannel.sidebar.waitForDismissal();
});
test('should close nav region when opening a room', async () => {
await poHomeChannel.goto();
await poHomeChannel.navbar.btnSidebarToggler().click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await poHomeChannel.sidepanel.getItemByName(targetChannel).click();
await poHomeChannel.content.waitForChannel();
await expect(poHomeChannel.content.channelHeader).toContainText(targetChannel);
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
await poHomeChannel.sidebar.waitForDismissal();
});
});
});
test.describe('Sidebar room filters', () => {
test.beforeAll(async ({ api }) => {
sidepanelTeam = await createTargetTeam(api);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: true,
},
{
name: 'secondarySidebar',
value: true,
},
],
});
});
test.afterAll(async ({ api }) => {
await deleteTeam(api, sidepanelTeam);
await setUserPreferences(api, {
featuresPreview: [
{
name: 'newNavigation',
value: false,
},
{
name: 'secondarySidebar',
value: false,
},
],
});
});
test('should open rooms when clicking on sidebar filter', async ({ page }) => {
await poHomeChannel.goto();
await expect(poHomeChannel.sidebar.channelsList).toBeVisible();
await poHomeChannel.sidebar.getFilterItemByName(sidepanelTeam).click({ force: true });
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await expect(poHomeChannel.sidepanel.getSidepanelHeader(sidepanelTeam)).toBeVisible();
await expect(poHomeChannel.sidepanel.getTeamItemByName(sidepanelTeam)).toBeVisible();
await expect(page).toHaveURL(`/group/${sidepanelTeam}`);
});
test('should open room when clicking on sidepanel item', async ({ page }) => {
await poHomeChannel.goto();
await poHomeChannel.sidebar.getFilterItemByName(sidepanelTeam).click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await expect(poHomeChannel.sidepanel.getSidepanelHeader(sidepanelTeam)).toBeVisible();
await poHomeChannel.sidepanel.getTeamItemByName(sidepanelTeam).click();
await poHomeChannel.content.waitForChannel();
await expect(page).toHaveURL(`/group/${sidepanelTeam}`);
});
test('should display rooms in direct message filter', async ({ page }) => {
const discussionName = faker.string.uuid();
await test.step('create a direct message with user1', async () => {
await poHomeChannel.goto();
await poHomeChannel.navbar.openChat(Users.user1.data.username);
await poHomeChannel.content.waitForChannel();
await expect(poHomeChannel.content.channelHeader).toContainText(Users.user1.data.username);
});
await test.step('create discussion in DM', async () => {
await poHomeChannel.composer.btnMenuMoreActions.click();
await page.getByRole('menuitem', { name: 'Discussion' }).click();
const createDiscussionModal = new CreateNewDiscussionModal(page);
await createDiscussionModal.inputName.fill(discussionName);
await createDiscussionModal.create();
await poHomeChannel.content.waitForChannel();
await poHomeChannel.content.sendMessage('hello');
await expect(page.getByRole('heading', { name: discussionName })).toBeVisible();
});
await test.step('open direct message sidebar filter', async () => {
await poHomeChannel.sidebar.teamsCollapser.click();
await poHomeChannel.sidebar.channelsCollapser.click();
await expect(poHomeChannel.sidebar.directMessagesCollapser).toBeVisible();
await expect(poHomeChannel.sidebar.directMessagesCollapser.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
await expect(poHomeChannel.sidebar.getFilterItemByName(Users.user1.data.username)).toBeVisible();
await poHomeChannel.sidebar.getFilterItemByName(Users.user1.data.username).click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
await expect(poHomeChannel.sidepanel.getSidepanelHeader(Users.user1.data.username)).toBeVisible();
await expect(poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).toBeVisible();
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).toContainText('hello');
await poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username).click();
await poHomeChannel.content.waitForChannel();
await poHomeChannel.content.sendMessage('hello DM');
await expect(poHomeChannel.content.channelHeader).toContainText(Users.user1.data.username);
await expect(page).toHaveURL(/direct/);
await expect(poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username)).toHaveAttribute('aria-current', 'page');
await expect(poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username)).toContainText('hello DM');
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).not.toHaveAttribute('aria-current');
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).toContainText('hello');
await poHomeChannel.sidepanel.getItemByName(discussionName).click();
await poHomeChannel.content.waitForChannel();
await expect(page).toHaveURL(/group/);
await expect(poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username)).not.toHaveAttribute('aria-current');
await expect(poHomeChannel.sidepanel.getMainRoomByName(Users.user1.data.username)).toContainText('hello DM');
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).toHaveAttribute('aria-current', 'page');
await expect(poHomeChannel.sidepanel.getItemByName(discussionName)).toContainText('hello');
await expect(poHomeChannel.sidepanel.getSidepanelHeader(Users.user1.data.username)).toBeVisible();
});
});
test('should not open rooms when clicking on sidebar filters in small viewport', async ({ page }) => {
await page.setViewportSize({ width: 640, height: 460 });
await poHomeChannel.goto();
await poHomeChannel.sidebar.waitForDismissal();
await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible();
await poHomeChannel.navbar.btnSidebarToggler().click();
await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible();
});
});
});

View File

@@ -0,0 +1,12 @@
# E2E Testing with playwright
## Running tests
```sh
$ RC_SERVER_1=http://localhost:3000 RC_SERVER_1_ADMIN_USER=test RC_SERVER_1_ADMIN_PASSWORD=test RC_SERVER_1_MATRIX_SERVER_NAME=my.matrix.server \
RC_SERVER_2=http://localhost:3000 RC_SERVER_2_ADMIN_USER=test2 RC_SERVER_2_ADMIN_PASSWORD=test RC_SERVER_2_MATRIX_SERVER_NAME=my2.matrix.server \
yarn run test:e2e:federation
```
## Important
Make sure to increase the rate limiter for the user register api under Admin => Rate Limiter => Feature limiting. This is necessary since we are registering new users programatically.

View File

@@ -0,0 +1,20 @@
export const RC_SERVER_1 = {
url: process.env.RC_SERVER_1 ?? 'http://localhost:3000',
username: process.env.RC_SERVER_1_ADMIN_USER ?? '',
password: process.env.RC_SERVER_1_ADMIN_PASSWORD ?? '',
matrixServerName: process.env.RC_SERVER_1_MATRIX_SERVER_NAME ?? '',
};
export const RC_SERVER_2 = {
url: process.env.RC_SERVER_2 ?? 'http://localhost:3000',
username: process.env.RC_SERVER_2_ADMIN_USER ?? '',
password: process.env.RC_SERVER_2_ADMIN_PASSWORD ?? '',
matrixServerName: process.env.RC_SERVER_2_MATRIX_SERVER_NAME ?? '',
};
export const RC_EXTRA_SERVER = {
url: process.env.RC_EXTRA_SERVER ?? 'http://localhost:3000',
username: process.env.RC_EXTRA_SERVER_ADMIN_USER ?? '',
password: process.env.RC_EXTRA_SERVER_ADMIN_PASSWORD ?? '',
matrixServerName: process.env.RC_EXTRA_SERVER_MATRIX_SERVER_NAME ?? '',
};

View File

@@ -0,0 +1,20 @@
export default async function (): Promise<void> {
const requiredEnvVars = [
'RC_SERVER_1',
'RC_SERVER_2',
'RC_SERVER_1_ADMIN_USER',
'RC_SERVER_1_ADMIN_PASSWORD',
'RC_SERVER_1_MATRIX_SERVER_NAME',
'RC_SERVER_2_ADMIN_USER',
'RC_SERVER_2_ADMIN_PASSWORD',
'RC_SERVER_2_MATRIX_SERVER_NAME',
'RC_EXTRA_SERVER',
'RC_EXTRA_SERVER_ADMIN_USER',
'RC_EXTRA_SERVER_ADMIN_PASSWORD',
'RC_EXTRA_SERVER_MATRIX_SERVER_NAME',
];
if (requiredEnvVars.some((envVar) => !process.env[envVar])) {
throw new Error(`Missing required environment variables: ${requiredEnvVars.filter((envVar) => !process.env[envVar]).join(', ')}`);
}
}

View File

@@ -0,0 +1,17 @@
import type { Locator, Page } from '@playwright/test';
export class FederationAccountProfile {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get inputName(): Locator {
return this.page.locator('//label[contains(text(), "Name")]/..//input');
}
get btnSubmit(): Locator {
return this.page.getByRole('button', { name: 'Save changes', exact: true });
}
}

View File

@@ -0,0 +1,78 @@
import type { Locator, Page } from '@playwright/test';
import { FederationAdminFlextab } from './fragments/admin-flextab';
export class FederationAdmin {
private readonly page: Page;
readonly tabs: FederationAdminFlextab;
constructor(page: Page) {
this.page = page;
this.tabs = new FederationAdminFlextab(page);
}
get inputSearchRooms(): Locator {
return this.page.locator('input[placeholder ="Search rooms"]');
}
get inputSearchUsers(): Locator {
return this.page.locator('input[placeholder="Search Users"]');
}
get inputSearchSettings(): Locator {
return this.page.locator('input[type=search]');
}
get roomsInputName(): Locator {
return this.page.locator('//label[text()="Name"]/following-sibling::span//input');
}
get roomsInputDescription(): Locator {
return this.page.locator('//label[text()="Description"]/following-sibling::span//textarea');
}
get roomsInputAnnouncement(): Locator {
return this.page.locator('//label[text()="Announcement"]/following-sibling::span//textarea');
}
get roomsInputTopic(): Locator {
return this.page.locator('//label[text()="Topic"]/following-sibling::span//textarea');
}
get roomsInputFavorite(): Locator {
return this.page.locator('//label[text()="Favorite"]/following-sibling::span//input/following-sibling::i');
}
get roomsInputPrivate(): Locator {
return this.page.locator('//label[text()="Private"]/following-sibling::span//input/following-sibling::i');
}
get roomsInputReadOnly(): Locator {
return this.page.locator('//label[text()="Read Only"]/following-sibling::span//input/following-sibling::i');
}
get roomsInputArchived(): Locator {
return this.page.locator('//label[text()="Archived"]/following-sibling::span//input/following-sibling::i');
}
get roomsInputDefault(): Locator {
return this.page.locator('//label[text()="Default"]/following-sibling::span//input/following-sibling::i');
}
get roomsInputFeatured(): Locator {
return this.page.locator('//label[text()="Featured"]/following-sibling::span//input/following-sibling::i');
}
get roomsBtnDelete(): Locator {
return this.page.locator('role=button[name="Delete"]');
}
get roomsBtnUploadAvatar(): Locator {
return this.page.locator('role=button[name="Upload"]');
}
get roomsBtnDefaultAvatar(): Locator {
return this.page.locator('button[title="Set Default Avatar"]');
}
}

View File

@@ -0,0 +1,94 @@
import type { Locator, Page } from '@playwright/test';
import { FederationHomeContent } from './fragments/home-content';
import { FederationHomeFlextab } from './fragments/home-flextab';
import { FederationSidenav } from './fragments/home-sidenav';
import { Navbar, RoomToolbar, ToastMessages } from '../../page-objects/fragments';
import { CreateNewChannelModal, CreateNewDMModal } from '../../page-objects/fragments/modals';
export class FederationChannel {
private readonly page: Page;
readonly content: FederationHomeContent;
readonly sidenav: FederationSidenav;
readonly navbar: Navbar;
readonly tabs: FederationHomeFlextab;
readonly roomToolbar: RoomToolbar;
readonly toastMessage: ToastMessages;
readonly newChannelModal: CreateNewChannelModal;
readonly newDMModal: CreateNewDMModal;
constructor(page: Page) {
this.page = page;
this.content = new FederationHomeContent(page);
this.sidenav = new FederationSidenav(page);
this.navbar = new Navbar(page);
this.tabs = new FederationHomeFlextab(page);
this.roomToolbar = new RoomToolbar(page);
this.toastMessage = new ToastMessages(page);
this.newChannelModal = new CreateNewChannelModal(page);
this.newDMModal = new CreateNewDMModal(page);
}
get btnContextualbarClose(): Locator {
return this.page.locator('[data-qa="ContextualbarActionClose"]');
}
async getFederationServerName(): Promise<string> {
return (await this.page.locator('[data-qa="federated-origin-server-name"]').locator('span').innerText()).substring(1).trim();
}
async createPublicChannelAndInviteUsersUsingCreationModal(channelName: string, usernamesToInvite: string[]) {
await this.navbar.createNew('Channel', channelName, {
private: false,
federated: true,
members: usernamesToInvite,
});
}
async createDiscussionSearchingForChannel(channelName: string) {
await this.navbar.openCreate('Discussion');
await this.page.locator('//label[text()="Parent channel or group"]/following-sibling::span//input').waitFor();
await this.page.locator('//label[text()="Parent channel or group"]/following-sibling::span//input').focus();
await this.page.locator('//label[text()="Parent channel or group"]/following-sibling::span//input').type(channelName, { delay: 100 });
}
async createTeam(teamName: string) {
await this.navbar.createNew('Team', teamName);
}
async createPrivateGroupAndInviteUsersUsingCreationModal(channelName: string, usernamesToInvite: string[]) {
await this.navbar.createNew('Channel', channelName, {
private: true,
federated: true,
members: usernamesToInvite,
});
}
async createDirectMessagesUsingModal(usernamesToInvite: string[]) {
await this.navbar.openCreate('Direct message');
for await (const username of usernamesToInvite) {
await this.newDMModal.inviteUserToDM(username);
}
await this.newDMModal.btnCreate.click();
}
async createNonFederatedPublicChannelAndInviteUsersUsingCreationModal(channelName: string, usernamesToInvite: string[]) {
await this.navbar.openCreate('Channel');
await this.newChannelModal.checkboxPrivate.click();
await this.newChannelModal.inputName.fill(channelName);
for await (const username of usernamesToInvite) {
await this.newChannelModal.inviteUserToChannel(username);
}
await this.newChannelModal.btnCreate.click();
}
}

View File

@@ -0,0 +1,46 @@
import type { Locator, Page } from '@playwright/test';
export class FederationAdminFlextabUsers {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get btnNew(): Locator {
return this.page.locator('role=button[name="New"]');
}
get btnEdit(): Locator {
return this.page.locator('role=button[name="Edit"]');
}
get btnSave(): Locator {
return this.page.locator('role=button[name="Save"]');
}
get inputName(): Locator {
return this.page.locator('//label[text()="Name"]/following-sibling::span//input');
}
get inputUserName(): Locator {
return this.page.locator('//label[text()="Username"]/following-sibling::span//input');
}
get inputEmail(): Locator {
return this.page.locator('//label[text()="Email"]/following-sibling::span//input').first();
}
get inputPassword(): Locator {
return this.page.locator('//label[text()="Password"]/following-sibling::span//input');
}
get checkboxVerified(): Locator {
return this.page.locator('//label[text()="Email"]/following-sibling::span//input/following-sibling::i');
}
async addRole(role: string): Promise<void> {
await this.page.locator('//label[text()="Roles"]/following-sibling::span//input').click();
await this.page.locator(`li[value=${role}]`).click();
}
}

View File

@@ -0,0 +1,11 @@
import type { Page } from '@playwright/test';
import { FederationAdminFlextabUsers } from './admin-flextab-users';
export class FederationAdminFlextab {
readonly users: FederationAdminFlextabUsers;
constructor(page: Page) {
this.users = new FederationAdminFlextabUsers(page);
}
}

View File

@@ -0,0 +1,244 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeContent {
protected readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get inputMessage(): Locator {
return this.page.locator('[name="msg"]');
}
get messagePopUpItems(): Locator {
return this.page.locator('role=menu[name="People"]');
}
get messageListItems(): Locator {
return this.page.locator('[role="listitem"][aria-roledescription="message"]');
}
get lastUserMessage(): Locator {
return this.messageListItems.last();
}
get lastUserMessageBody(): Locator {
return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]');
}
async sendMessage(text: string): Promise<void> {
await this.page.locator('[name="msg"]').type(text);
await this.page.locator('button[aria-label="Send"]').click();
}
async sendMessageUsingEnter(text: string): Promise<void> {
await this.page.locator('[name="msg"]').type(text);
await this.page.keyboard.press('Enter');
}
async editLastMessage(message: string): Promise<void> {
await this.openLastMessageMenu();
await this.btnOptionEditMessage.click();
await this.page.locator('[name="msg"]').fill(message);
await this.page.keyboard.press('Enter');
}
async deleteLastMessage(): Promise<void> {
await this.openLastMessageMenu();
await this.btnOptionDeleteMessage.click();
await this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger').click();
}
async starLastMessage(): Promise<void> {
await this.openLastMessageMenu();
await this.btnOptionStarMessage.waitFor();
await this.btnOptionStarMessage.click();
}
async replyInDm(message: string): Promise<void> {
await this.openLastMessageMenu();
await this.btnOptionReplyDirectly.click();
await this.page.waitForTimeout(2000);
await this.page.locator('[name="msg"]').type(message, { delay: 100 });
await this.page.waitForTimeout(2000);
await this.page.keyboard.press('Enter');
}
async sendAudioRecordedMessage(): Promise<void> {
await this.btnRecordAudio.click();
await this.page.waitForTimeout(3000);
await this.page.locator('.rc-message-box__icon.rc-message-box__audio-message-done').click();
await this.btnModalConfirm.click();
}
async sendAudioRecordedInThreadMessage(): Promise<void> {
await this.btnRecordAudio.nth(1).click();
await this.page.waitForTimeout(3000);
await this.page.locator('.rc-message-box__icon.rc-message-box__audio-message-done').click();
await this.btnModalConfirm.click();
}
async sendVideoRecordedMessage(): Promise<void> {
await this.btnVideoMessage.click();
await this.page.locator('.rcx-box.rcx-box--full.rcx-icon--name-video').click();
await this.page.waitForTimeout(3000);
await this.page.locator('.rcx-box.rcx-box--full.rcx-icon--name-stop-unfilled').click();
await this.page.locator('button >> text="Send"').click();
await this.page.waitForTimeout(3000);
await this.btnModalConfirm.click();
}
async dispatchSlashCommand(text: string): Promise<void> {
await this.page.locator('[name="msg"]').fill(text);
await this.page.locator('button[aria-label="Send"]').waitFor();
await this.page.locator('button[aria-label="Send"]').click();
}
get btnModalConfirm(): Locator {
return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--primary');
}
get lastMessageFileName(): Locator {
return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child');
}
async getLastFileMessageByFileName(filename: string): Promise<Locator> {
return this.page
.locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container')
.last()
.locator(`div[title="${filename}"]`);
}
async getLastFileThreadMessageByFileName(filename: string): Promise<Locator> {
return this.page
.locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container')
.last()
.locator(`div[title="${filename}"]`);
}
get lastFileMessage(): Locator {
return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container').last();
}
get waitForLastMessageTextAttachmentEqualsText(): Locator {
return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-attachment__details .rcx-message-body');
}
get waitForLastThreadMessageTextAttachmentEqualsText(): Locator {
return this.page
.locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]')
.last()
.locator('.rcx-attachment__details');
}
get btnOptionEditMessage(): Locator {
return this.page.locator('[data-qa-id="edit-message"]');
}
get btnOptionDeleteMessage(): Locator {
return this.page.locator('[data-qa-id="delete-message"]');
}
get btnOptionPinMessage(): Locator {
return this.page.locator('[data-qa-id="pin-message"]');
}
get btnOptionStarMessage(): Locator {
return this.page.locator('[data-qa-id="star-message"]');
}
get btnVideoMessage(): Locator {
return this.page.locator('[data-qa-id="video-message"]');
}
get btnRecordAudio(): Locator {
return this.page.locator('[data-qa-id="audio-record"]');
}
get btnMenuMoreActions() {
return this.page.locator('[data-qa-id="menu-more-actions"]');
}
get btnContactEdit(): Locator {
return this.page.locator('.rcx-vertical-bar button:has-text("Edit")');
}
get btnOptionReplyInThread(): Locator {
return this.page.locator('[data-qa-id="reply-in-thread"]');
}
get btnOptionStartDiscussion(): Locator {
return this.page.locator('[data-qa-id="start-discussion"]');
}
get btnOptionReplyDirectly(): Locator {
return this.page.locator('[data-qa-id="reply-directly"]');
}
get lastThreadMessageText(): Locator {
return this.page.locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]').last();
}
async sendFileMessage(fileName: string): Promise<void> {
await this.page.locator('input[type=file]').setInputFiles(`./tests/e2e/federation/files/${fileName}`);
}
async sendThreadMessage(message: string): Promise<void> {
await this.page.getByRole('dialog').locator('[name="msg"]').last().fill(message);
await this.page.keyboard.press('Enter');
}
async openLastMessageMenu(): Promise<void> {
await this.lastUserMessage.hover();
await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).waitFor();
await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).click();
}
threadSendToChannelAlso(): Locator {
return this.page.getByRole('dialog').locator('label', { hasText: 'Also send to channel' });
}
async quoteMessage(message: string): Promise<void> {
await this.openLastMessageMenu();
await this.page.locator('[data-qa-id="quote-message"]').click();
await this.page.locator('[name="msg"]').fill(message);
await this.page.keyboard.press('Enter');
}
async openLastThreadMessageMenu(): Promise<void> {
await this.lastThreadMessageText.hover();
await this.lastThreadMessageText.getByRole('button', { name: 'More', exact: true }).waitFor();
await this.lastThreadMessageText.getByRole('button', { name: 'More', exact: true }).click();
}
async quoteMessageInsideThread(message: string): Promise<void> {
await this.openLastThreadMessageMenu();
await this.page.locator('[data-qa-id="quote-message"]').click();
await this.sendThreadMessage(message);
}
async reactToMessage(emoji: string): Promise<void> {
await this.openLastMessageMenu();
await this.page.locator('[data-qa-id="reaction-message"]').click();
await this.page.locator('input.js-emojipicker-search').type(emoji);
await this.page.locator(`[data-emoji="${emoji}"]`).last().click();
}
async unreactLastMessage(): Promise<void> {
await this.lastUserMessage.locator('.rcx-message-reactions__reaction').nth(1).waitFor();
await this.lastUserMessage.locator('.rcx-message-reactions__reaction').nth(1).click();
}
async getSystemMessageByText(text: string): Promise<Locator> {
return this.page.locator('[role="document"][aria-roledescription="system message body"]', { hasText: text });
}
async getLastSystemMessageName(): Promise<Locator> {
return this.page.locator('[role="listitem"][aria-roledescription="system message"]').last().getByRole('button');
}
async getAllReactions(): Promise<Locator> {
return this.lastUserMessage.locator('.rcx-message-reactions__reaction');
}
}

View File

@@ -0,0 +1,21 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeFlextabChannels {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get btnAddExisting(): Locator {
return this.page.locator('button >> text="Add Existing"');
}
get inputChannels(): Locator {
return this.page.locator('#modal-root input').first();
}
get btnAdd(): Locator {
return this.page.locator('#modal-root button:has-text("Add")');
}
}

View File

@@ -0,0 +1,13 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeFlextabDirectMessageMember {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
getUserInfoUsername(username: string): Locator {
return this.page.locator(`//div[contains(@class, "rcx-box--full") and contains(@title, "${username}")]`);
}
}

View File

@@ -0,0 +1,57 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeFlextabMembers {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
getUserInList(username: string): Locator {
return this.page.locator(`[data-qa="MemberItem-${username}"]`);
}
get addUsersButton(): Locator {
return this.page.locator('role=button[name="Add"]');
}
get btnRemoveUserFromRoom(): Locator {
return this.page.locator('[value="removeUser"]');
}
get btnMenuUserInfo(): Locator {
return this.page.getByRole('dialog', { name: 'User Info', exact: true }).getByRole('button', { name: 'More', exact: true });
}
getKebabMenuForUser(username: string): Locator {
return this.page.locator(`[data-username="${username}"] [data-testid="menu"]`);
}
async getOptionFromKebabMenuForUser(optionName: string): Promise<Locator> {
return this.page.locator(`ol li[value="${optionName}"].rcx-option`);
}
async removeUserFromRoom(username: string): Promise<void> {
await this.getUserInList(username).hover();
await this.getKebabMenuForUser(username).click();
await (await this.getOptionFromKebabMenuForUser('removeUser')).click();
await this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger').click();
}
async addMultipleUsers(usernames: string[]) {
await this.addUsersButton.click();
for await (const username of usernames) {
await this.page.getByRole('textbox', { name: 'Choose users' }).click();
await this.page.getByRole('textbox', { name: 'Choose users' }).fill(username);
await this.page.getByRole('listbox').waitFor();
await this.page.getByRole('listbox').selectOption(username);
}
await this.addUsersButton.click();
}
async showAllUsers() {
await this.page.locator('.rcx-select >> text=Online').first().click();
await this.page.locator('.rcx-option:has-text("All")').first().click();
}
}

View File

@@ -0,0 +1,41 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeFlextabNotificationPreferences {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get btnSave(): Locator {
return this.page.locator('role=button[name="Save"]');
}
getPreferenceByDevice(device: string): Locator {
return this.page.locator(`//div[@id="${device}Alert"]`);
}
async selectDropdownById(text: string): Promise<void> {
await this.page.locator(`//div[@id="${text}"]`).click();
}
async selectOptionByLabel(text: string): Promise<void> {
await this.page.locator(`li.rcx-option >> text="${text}"`).click();
}
async selectDevice(text: string): Promise<void> {
await this.page.locator(`[data-qa-id="${text}-notifications"]`).click();
}
async updateDevicePreference(device: string): Promise<void> {
await this.selectDevice(device);
await this.selectDropdownById(`${device}Alert`);
await this.selectOptionByLabel('Mentions');
}
async updateAllNotificationPreferences(): Promise<void> {
await this.updateDevicePreference('Desktop');
await this.updateDevicePreference('Mobile');
await this.updateDevicePreference('Email');
}
}

View File

@@ -0,0 +1,49 @@
import type { Locator, Page } from '@playwright/test';
export class FederationHomeFlextabRoom {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get btnEdit(): Locator {
return this.page.locator('role=button[name="Edit"]');
}
get btnLeave(): Locator {
return this.page.locator('role=button[name="Leave"]');
}
get btnDelete(): Locator {
return this.page.locator('role=button[name="Delete"]');
}
get inputName(): Locator {
return this.page.getByRole('dialog').getByRole('textbox', { name: 'Name' });
}
get inputTopic(): Locator {
return this.page.getByRole('dialog').getByRole('textbox', { name: 'Topic' });
}
get inputAnnouncement(): Locator {
return this.page.getByRole('dialog').getByRole('textbox', { name: 'Announcement' });
}
get inputDescription(): Locator {
return this.page.getByRole('dialog').getByRole('textbox', { name: 'Description' });
}
get checkboxReadOnly(): Locator {
return this.page.locator('text=Read OnlyOnly authorized users can write new messages >> i');
}
get btnSave(): Locator {
return this.page.locator('role=button[name="Save"]');
}
get btnModalConfirm(): Locator {
return this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger');
}
}

View File

@@ -0,0 +1,44 @@
import type { Locator, Page } from '@playwright/test';
import { FederationHomeFlextabChannels } from './home-flextab-channels';
import { FederationHomeFlextabDirectMessageMember } from './home-flextab-dm-member';
import { FederationHomeFlextabMembers } from './home-flextab-members';
import { FederationHomeFlextabNotificationPreferences } from './home-flextab-notificationPreferences';
import { FederationHomeFlextabRoom } from './home-flextab-room';
export class FederationHomeFlextab {
private readonly page: Page;
readonly members: FederationHomeFlextabMembers;
readonly dmUserMember: FederationHomeFlextabDirectMessageMember;
readonly room: FederationHomeFlextabRoom;
readonly channels: FederationHomeFlextabChannels;
readonly notificationPreferences: FederationHomeFlextabNotificationPreferences;
constructor(page: Page) {
this.page = page;
this.members = new FederationHomeFlextabMembers(page);
this.room = new FederationHomeFlextabRoom(page);
this.channels = new FederationHomeFlextabChannels(page);
this.notificationPreferences = new FederationHomeFlextabNotificationPreferences(page);
this.dmUserMember = new FederationHomeFlextabDirectMessageMember(page);
}
get btnAddExistingChannelToTeam(): Locator {
return this.page.locator('role=button[name="Add Existing"]');
}
async searchForChannelOnAddChannelToTeam(channelName: string): Promise<void> {
await this.page.locator('//label[contains(text(), "Channels")]/..//input').focus();
await this.page.waitForTimeout(2000);
await this.page.locator('//label[contains(text(), "Channels")]/..//input').type(channelName);
}
get userInfoUsername(): Locator {
return this.page.locator('[data-qa="UserInfoUserName"]');
}
}

View File

@@ -0,0 +1,51 @@
import type { Locator, Page } from '@playwright/test';
export class FederationSidenav {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get checkboxFederatedChannel(): Locator {
return this.page.locator(
'//*[@id="modal-root"]//*[contains(@class, "rcx-field") and contains(text(), "Federated")]/../following-sibling::label/i',
);
}
async countFilteredChannelsOnDirectory(name: string): Promise<number> {
await this.page.locator('button[title="Directory"]').click();
await this.page.locator('button:has-text("Channels")').click();
await this.page.locator('input[placeholder ="Search Channels"]').focus();
await this.page.locator('input[placeholder ="Search Channels"]').type(name, { delay: 100 });
await this.page.waitForTimeout(5000);
return this.page.locator('table tbody tr').count();
}
async openChatWhenHaveMultipleWithTheSameName(name: string, item: number): Promise<void> {
await this.page.locator('role=navigation >> role=button[name=Search]').click();
await this.page.locator('role=search >> role=searchbox').focus();
await this.page.locator('role=search >> role=searchbox').type(name);
await this.page.waitForTimeout(2000);
await this.page.locator(`role=search >> role=listbox >> role=link >> text="${name}"`).nth(item).click({ force: true });
}
async countRoomsByNameOnSearch(name: string): Promise<number> {
await this.page.locator('role=navigation >> role=button[name=Search]').click();
await this.page.locator('role=search >> role=searchbox').focus();
await this.page.locator('role=search >> role=searchbox').type(name);
await this.page.locator(`role=search >> role=listbox >> role=link >> text="${name}"`).waitFor();
await this.page.waitForTimeout(2000);
return this.page.locator(`role=search >> role=listbox >> role=link >> text="${name}"`).count();
}
async openDMMultipleChat(name: string): Promise<void> {
await this.page.locator('role=navigation >> role=button[name=Search]').click();
await this.page.locator('role=search >> role=searchbox').focus();
await this.page.locator('role=search >> role=searchbox').type(name);
await this.page.locator(`role=search >> role=listbox >> role=link >> text="${name}"`).waitFor();
await this.page.locator(`.rcx-sidebar-item`).nth(1).click({ force: true });
}
}

View File

@@ -0,0 +1,81 @@
import { faker } from '@faker-js/faker';
import * as constants from '../../config/constants';
import { FederationAdmin } from '../../page-objects/admin';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createChannelUsingAPI } from '../../utils/channel';
import { formatIntoFullMatrixUsername } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe.parallel('Federation - Admin Panel - Rooms', () => {
let poFederationChannelServer1: FederationChannel;
let userFromServer2UsernameOnly: string;
const channelName = faker.string.uuid();
let poFederationAdmin: FederationAdmin;
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer2UsernameOnly = await registerUser(apiServer2);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
const page = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
await doLogin({
page,
server: constants.RC_SERVER_1,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await createChannelUsingAPI(apiServer2, channelName);
await poFederationChannelServer1.createPublicChannelAndInviteUsersUsingCreationModal(channelName, [fullUsernameFromServer2]);
await page.close();
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ page }) => {
poFederationAdmin = new FederationAdmin(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_2.url,
username: constants.RC_SERVER_2.username,
password: constants.RC_SERVER_2.password,
},
});
await page.goto(`${constants.RC_SERVER_2.url}/admin/rooms`);
});
test('expect to have the 2 rooms(with the same name) created and showing correctly', async ({ page }) => {
await poFederationAdmin.inputSearchRooms.type(channelName);
await page.waitForTimeout(5000);
await expect(await page.locator(`table tbody tr`).count()).toBe(2);
});
test('expect to be able to edit only (name, topic and favorite) when the room is a federated one', async ({ page }) => {
await poFederationAdmin.inputSearchRooms.type(channelName);
await page.waitForTimeout(5000);
await page.locator(`table tbody tr`).first().click();
await expect(poFederationAdmin.roomsInputName).not.toBeDisabled();
await expect(poFederationAdmin.roomsInputTopic).not.toBeDisabled();
await expect(poFederationAdmin.roomsInputFavorite).not.toBeDisabled();
await expect(poFederationAdmin.roomsInputDescription).not.toBeVisible();
await expect(poFederationAdmin.roomsInputAnnouncement).not.toBeVisible();
await expect(poFederationAdmin.roomsInputPrivate).not.toBeVisible();
await expect(poFederationAdmin.roomsInputReadOnly).not.toBeVisible();
await expect(poFederationAdmin.roomsInputArchived).not.toBeVisible();
await expect(poFederationAdmin.roomsInputDefault).toBeDisabled();
await expect(poFederationAdmin.roomsInputFeatured).toBeDisabled();
await expect(poFederationAdmin.roomsBtnDelete).toBeDisabled();
await expect(poFederationAdmin.roomsBtnUploadAvatar).toBeDisabled();
await expect(poFederationAdmin.roomsBtnDefaultAvatar).toBeDisabled();
});
});

View File

@@ -0,0 +1,128 @@
import * as constants from '../../config/constants';
import { FederationAdmin } from '../../page-objects/admin';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createChannelAndInviteRemoteUserToCreateLocalUser } from '../../utils/channel';
import { formatIntoFullMatrixUsername, formatUsernameAndDomainIntoMatrixFormat } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe.parallel('Federation - Admin Panel - Users', () => {
let poFederationChannelServer1: FederationChannel;
let userFromServer2UsernameOnly: string;
let usernameWithDomainFromServer2: string;
let poFederationAdmin: FederationAdmin;
test.beforeEach(async ({ page }) => {
poFederationAdmin = new FederationAdmin(page);
});
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer2UsernameOnly = await registerUser(apiServer2);
usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
const page = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
await createChannelAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ page }) => {
poFederationChannelServer1 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: constants.RC_SERVER_1.username,
password: constants.RC_SERVER_1.password,
},
});
await page.goto(`${constants.RC_SERVER_1.url}/admin/users`);
});
test('expect to not be able to edit federated users from the admin panel', async ({ page }) => {
await poFederationAdmin.inputSearchUsers.type(usernameWithDomainFromServer2);
await page.locator(`table tr`).locator(`figure[data-username="${usernameWithDomainFromServer2}"]`).click();
await expect(poFederationAdmin.tabs.users.btnEdit).toBeDisabled();
});
test('expect to not be able to edit federated users from the admin panel (trying to bypass by url)', async ({ page }) => {
await poFederationAdmin.inputSearchUsers.type(usernameWithDomainFromServer2);
await page.locator(`table tr`).locator(`figure[data-username="${usernameWithDomainFromServer2}"]`).click();
await expect(poFederationAdmin.tabs.users.btnEdit).toBeDisabled();
const currentUrl = await page.url();
await page.goto(`${currentUrl.replace('info', 'edit')}`);
await expect(page.locator('.rcx-box.rcx-box--full.rcx-callout__wrapper')).toBeVisible();
await expect(
page.locator('.rcx-box.rcx-box--full.rcx-callout__wrapper').locator('.rcx-box.rcx-box--full.rcx-callout__children'),
).toContainText('Not possible to edit a federated user');
});
test('expect not to have the kebab menu enabled, since there should not be any menu option enabled for federated users', async ({
page,
}) => {
await poFederationAdmin.inputSearchUsers.type(usernameWithDomainFromServer2);
await page.locator(`table tr`).locator(`figure[data-username="${usernameWithDomainFromServer2}"]`).click();
await expect(page.locator('[data-testid="menu"]')).not.toBeVisible();
});
test('expect to federated users (remote only) not being counted as a seat cap in an EE environment', async ({
browser,
page,
apiServer2,
}) => {
const before = parseInt((await page.locator('role=status').textContent())?.replace('Seats Available', '').trim() || '0');
const newUserFromServer2 = await registerUser(apiServer2);
const page2 = await browser.newPage();
const poFederationChannelServer1ForUser2 = new FederationChannel(page2);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(newUserFromServer2, constants.RC_SERVER_2.matrixServerName);
await createChannelAndInviteRemoteUserToCreateLocalUser({
page: page2,
poFederationChannelServer: poFederationChannelServer1ForUser2,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
const after = parseInt((await page.locator('role=status').textContent())?.replace('Seats Available', '').trim() || '0');
expect(before).toEqual(after);
await page2.close();
});
test('expect to federated users (but local ones) being counted as a seat cap in an EE environment', async ({
browser,
page,
apiServer1,
}) => {
const before = parseInt((await page.locator('role=status').textContent())?.replace('Seats Available', '').trim() || '0');
const newUserFromServer1 = await registerUser(apiServer1);
const page2 = await browser.newPage();
const poFederationChannelServer1ForUser2 = new FederationChannel(page2);
const fullUsernameFromServer1 = formatIntoFullMatrixUsername(newUserFromServer1, constants.RC_SERVER_1.matrixServerName);
await createChannelAndInviteRemoteUserToCreateLocalUser({
page: page2,
poFederationChannelServer: poFederationChannelServer1ForUser2,
fullUsernameFromServer: fullUsernameFromServer1,
server: constants.RC_SERVER_1,
});
await page.reload();
const after = parseInt((await page.locator('role=status').textContent())?.replace('Seats Available', '').trim() || '0');
expect(before).not.toEqual(after);
expect(before - 1).toEqual(after);
await page2.close();
});
});

View File

@@ -0,0 +1,88 @@
import { faker } from '@faker-js/faker';
import { CreateNewChannelModal } from '../../../page-objects/fragments/modals';
import * as constants from '../../config/constants';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createGroupAndInviteRemoteUserToCreateLocalUser } from '../../utils/channel';
import { formatIntoFullMatrixUsername } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe.parallel('Federation - CE version', () => {
let poFederationChannelServer2: FederationChannel;
let userFromServer2UsernameOnly: string;
let userFromServer1UsernameOnly: string;
let fullUsernameFromServer1: string;
let fullUsernameFromServer2: string;
let newChannelModal: CreateNewChannelModal;
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer1UsernameOnly = await registerUser(apiServer1);
userFromServer2UsernameOnly = await registerUser(apiServer2);
fullUsernameFromServer1 = formatIntoFullMatrixUsername(userFromServer1UsernameOnly, constants.RC_SERVER_1.matrixServerName);
fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
const page = await browser.newPage();
poFederationChannelServer2 = new FederationChannel(page);
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ page }) => {
poFederationChannelServer2 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_2.url,
username: constants.RC_SERVER_2.username,
password: constants.RC_SERVER_2.password,
},
});
await page.goto(`${constants.RC_SERVER_2.url}/home`);
});
test('expect to not be able to create channels from the UI', async ({ page }) => {
newChannelModal = new CreateNewChannelModal(page);
await poFederationChannelServer2.navbar.openChat('Channel');
await newChannelModal.advancedSettingsAccordion.click();
await expect(newChannelModal.checkboxFederated).toBeDisabled();
});
test('expect to not be able to create DMs from UI inviting external users (an user who does not exists on the server yet)', async () => {
await poFederationChannelServer2.createDirectMessagesUsingModal([fullUsernameFromServer1]);
await poFederationChannelServer2.toastMessage.waitForDisplay({ type: 'error' });
});
test('expect to not be able to create DMs from UI inviting external users (an user who already exists in the server)', async ({
browser,
}) => {
const page2 = await browser.newPage();
const poFederationChannelServer1 = new FederationChannel(page2);
await createGroupAndInviteRemoteUserToCreateLocalUser({
page: page2,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
await poFederationChannelServer2.createDirectMessagesUsingModal([fullUsernameFromServer1]);
await poFederationChannelServer2.toastMessage.waitForDisplay({ type: 'error' });
});
test('expect to not be able to invite federated users to non-federated rooms (using modal)', async () => {
const channelName = faker.string.uuid();
await poFederationChannelServer2.createNonFederatedPublicChannelAndInviteUsersUsingCreationModal(channelName, [
fullUsernameFromServer2,
]);
await poFederationChannelServer2.navbar.openChat(channelName);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer2.tabs.members.getUserInList(constants.RC_SERVER_2.username)).toBeVisible();
});
});

View File

@@ -0,0 +1,876 @@
import * as constants from '../../config/constants';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createChannelAndInviteRemoteUserToCreateLocalUser } from '../../utils/channel';
import { formatIntoFullMatrixUsername, formatUsernameAndDomainIntoMatrixFormat } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe.parallel('Federation - Direct Messages', () => {
let poFederationChannelServer1: FederationChannel;
let userFromServer2UsernameOnly: string;
let userFromServer1UsernameOnly: string;
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer1UsernameOnly = await registerUser(apiServer1);
userFromServer2UsernameOnly = await registerUser(apiServer2);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
const page = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
await createChannelAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ page }) => {
poFederationChannelServer1 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: constants.RC_SERVER_1.username,
password: constants.RC_SERVER_1.password,
},
});
await page.addInitScript(() => {
window.localStorage.setItem('fuselage-localStorage-members-list-type', JSON.stringify('online'));
});
});
test.describe('Direct Messages (DMs)', () => {
test.describe('Inviting users using the creation modal', () => {
test('expect to create a DM inviting an user from the Server B who does not exist in Server A yet', async ({
browser,
apiServer2,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
const usernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: usernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(usernameFromServer2, constants.RC_SERVER_2.matrixServerName);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
usernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.createDirectMessagesUsingModal([fullUsernameFromServer2]);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect to create a DM inviting an user from the Server B who already exist in Server A', async ({ browser, page }) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.createDirectMessagesUsingModal([userFromServer2UsernameOnly]);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test.describe('With multiple users (when the remote user does not exists in the server A yet)', () => {
let createdUsernameFromServer2: string;
test('expect to create a DM inviting an user from the Server B who does not exist in Server A yet + an user from Server A only (locally)', async ({
browser,
apiServer2,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
createdUsernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: createdUsernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(createdUsernameFromServer2, constants.RC_SERVER_2.matrixServerName);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const adminUsernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
const userCreatedWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer1UsernameOnly,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.createDirectMessagesUsingModal([fullUsernameFromServer2, userFromServer1UsernameOnly]);
await poFederationChannelServer1.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer1.roomToolbar.openMembersTab();
await poFederationChannelServer1.tabs.members.showAllUsers();
await poFederationChannelServer2.sidenav.openDMMultipleChat(userCreatedWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer1.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(createdUsernameFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(userCreatedWithDomainFromServer1)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(adminUsernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect the user from Server A (locally) is able to access the previous created group', async ({ browser }) => {
const page2 = await browser.newPage();
const poFederationChannel1ForUser2 = new FederationChannel(page2);
await doLogin({
page: page2,
server: {
url: constants.RC_SERVER_1.url,
username: userFromServer1UsernameOnly,
password: constants.RC_SERVER_1.password,
},
storeState: false,
});
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
await page2.goto(`${constants.RC_SERVER_1.url}/home`);
await poFederationChannel1ForUser2.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannel1ForUser2.roomToolbar.openMembersTab();
await poFederationChannel1ForUser2.tabs.members.showAllUsers();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await page2.close();
});
});
test.describe('With multiple users (when the user from Server B already exists in Server A)', () => {
test('expect to create a DM inviting an user from the Server B who already exist in Server A + an user from Server A only (locally)', async ({
browser,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
const usernameOriginalFromServer1OnlyWithDomain = formatUsernameAndDomainIntoMatrixFormat(
userFromServer1UsernameOnly,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.createDirectMessagesUsingModal([userFromServer2UsernameOnly, userFromServer1UsernameOnly]);
await poFederationChannelServer1.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer1.roomToolbar.openMembersTab();
await poFederationChannelServer1.tabs.members.showAllUsers();
await poFederationChannelServer2.sidenav.openDMMultipleChat(usernameOriginalFromServer1OnlyWithDomain);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer1.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(userFromServer2UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameOriginalFromServer1OnlyWithDomain)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect the user from Server A (locally) is able to access the previous created', async ({ browser }) => {
const page2 = await browser.newPage();
const poFederationChannel1ForUser2 = new FederationChannel(page2);
await doLogin({
page: page2,
server: {
url: constants.RC_SERVER_1.url,
username: userFromServer1UsernameOnly,
password: constants.RC_SERVER_1.password,
},
storeState: false,
});
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
await page2.goto(`${constants.RC_SERVER_1.url}/home`);
await poFederationChannel1ForUser2.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannel1ForUser2.roomToolbar.openMembersTab();
await poFederationChannel1ForUser2.tabs.members.showAllUsers();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await page2.close();
});
});
});
test.describe('Using Slash commands', () => {
test('expect to create a DM inviting an user from the Server B who does not exist in Server A yet', async ({
browser,
page,
apiServer2,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
const usernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: usernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(usernameFromServer2, constants.RC_SERVER_2.matrixServerName);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
usernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.navbar.openChat('general');
await poFederationChannelServer1.content.dispatchSlashCommand(`/federation dm ${fullUsernameFromServer2}`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect to create a DM inviting an user from the Server B who already exist in Server A', async ({
browser,
page,
apiServer2,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
const createdUsernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: createdUsernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(createdUsernameFromServer2, constants.RC_SERVER_2.matrixServerName);
await poFederationChannelServer1.navbar.logout();
await createChannelAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
closePageAfterCreation: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const adminUsernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.navbar.openChat('general');
await poFederationChannelServer1.content.dispatchSlashCommand(`/federation dm @${usernameWithDomainFromServer2}`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer2.navbar.openChat(adminUsernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(adminUsernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect to create a DM inviting an user from the Server A who does not exist in Server B yet', async ({
browser,
page,
apiServer1,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
const createdUsernameFromServer1 = await registerUser(apiServer1);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: constants.RC_SERVER_2.username,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const fullUsernameFromServer1 = formatIntoFullMatrixUsername(createdUsernameFromServer1, constants.RC_SERVER_1.matrixServerName);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer1,
constants.RC_SERVER_1.matrixServerName,
);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_2.username,
constants.RC_SERVER_2.matrixServerName,
);
await poFederationChannelServer2.navbar.openChat('general');
await poFederationChannelServer2.content.dispatchSlashCommand(`/federation dm ${fullUsernameFromServer1}`);
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await poFederationChannelServer2.content.sendMessage('hello world');
await poFederationChannelServer1.navbar.logout();
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: createdUsernameFromServer1,
password: constants.RC_SERVER_1.password,
},
storeState: false,
});
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer1)).toBeVisible();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await pageForServer2.close();
});
test('expect to create a DM inviting an user from the Server A who already exist in Server B', async ({
browser,
page,
apiServer2,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
const createdUsernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: createdUsernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(createdUsernameFromServer2, constants.RC_SERVER_2.matrixServerName);
await poFederationChannelServer1.navbar.logout();
await createChannelAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
closePageAfterCreation: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const adminUsernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer2.navbar.openChat('general');
await poFederationChannelServer2.content.dispatchSlashCommand(`/federation dm @${adminUsernameWithDomainFromServer1}`);
await page.waitForTimeout(2000);
await poFederationChannelServer2.navbar.openChat(adminUsernameWithDomainFromServer1);
await poFederationChannelServer2.roomToolbar.openUserInfo();
await poFederationChannelServer2.content.sendMessage('hello world');
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.roomToolbar.openUserInfo();
await expect(poFederationChannelServer2.tabs.dmUserMember.getUserInfoUsername(adminUsernameWithDomainFromServer1)).toBeVisible();
await expect(poFederationChannelServer1.tabs.dmUserMember.getUserInfoUsername(usernameWithDomainFromServer2)).toBeVisible();
await pageForServer2.close();
});
test.describe('With multiple users (when the remote user does not exists in the server A yet)', () => {
let createdUsernameFromServer2: string;
test('expect to create a DM, and invite an user from the Server B who does not exist in Server A yet + an user from Server A only (locally)', async ({
browser,
apiServer2,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
createdUsernameFromServer2 = await registerUser(apiServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: createdUsernameFromServer2,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(createdUsernameFromServer2, constants.RC_SERVER_2.matrixServerName);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
const usernameOriginalFromServer1OnlyWithDomain = formatUsernameAndDomainIntoMatrixFormat(
userFromServer1UsernameOnly,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.navbar.openChat('general');
await poFederationChannelServer1.content.dispatchSlashCommand(
`/federation dm ${fullUsernameFromServer2} @${userFromServer1UsernameOnly}`,
);
await poFederationChannelServer1.sidenav.openDMMultipleChat(userFromServer1UsernameOnly);
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer1.roomToolbar.openMembersTab();
await poFederationChannelServer1.tabs.members.showAllUsers();
await poFederationChannelServer2.sidenav.openDMMultipleChat(usernameOriginalFromServer1OnlyWithDomain);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer1.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(createdUsernameFromServer2)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameOriginalFromServer1OnlyWithDomain)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect the user from Server A (locally) is able to access the previous created group', async ({ browser }) => {
const page2 = await browser.newPage();
const poFederationChannel1ForUser2 = new FederationChannel(page2);
await doLogin({
page: page2,
server: {
url: constants.RC_SERVER_1.url,
username: userFromServer1UsernameOnly,
password: constants.RC_SERVER_1.password,
},
storeState: false,
});
await page2.goto(`${constants.RC_SERVER_1.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
createdUsernameFromServer2,
constants.RC_SERVER_2.matrixServerName,
);
await poFederationChannel1ForUser2.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannel1ForUser2.roomToolbar.openMembersTab();
await poFederationChannel1ForUser2.tabs.members.showAllUsers();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await page2.close();
});
});
test.describe('With multiple users (when the user from Server B already exists in Server A)', () => {
test('expect to create a DM, and invite an user from the Server B who already exist in Server A + an user from Server A only (locally)', async ({
browser,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
const usernameOriginalFromServer1OnlyWithDomain = formatUsernameAndDomainIntoMatrixFormat(
userFromServer1UsernameOnly,
constants.RC_SERVER_1.matrixServerName,
);
await poFederationChannelServer1.navbar.openChat('general');
await poFederationChannelServer1.content.dispatchSlashCommand(
`/federation dm @${usernameWithDomainFromServer2} @${userFromServer1UsernameOnly}`,
);
await poFederationChannelServer1.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await poFederationChannelServer1.roomToolbar.openMembersTab();
await poFederationChannelServer1.tabs.members.showAllUsers();
await poFederationChannelServer2.sidenav.openDMMultipleChat(usernameOriginalFromServer1OnlyWithDomain);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer1.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer1.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(userFromServer2UsernameOnly)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameOriginalFromServer1OnlyWithDomain)).toBeVisible();
await expect(poFederationChannelServer2.tabs.members.getUserInList(usernameWithDomainFromServer1)).toBeVisible();
await pageForServer2.close();
});
test('expect the user from Server A (locally) is able to access the previous created group', async ({ browser }) => {
const page2 = await browser.newPage();
const poFederationChannel1ForUser2 = new FederationChannel(page2);
await doLogin({
page: page2,
server: {
url: constants.RC_SERVER_1.url,
username: userFromServer1UsernameOnly,
password: constants.RC_SERVER_1.password,
},
storeState: false,
});
await page2.goto(`${constants.RC_SERVER_1.url}/home`);
const usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
await poFederationChannel1ForUser2.sidenav.openDMMultipleChat(usernameWithDomainFromServer2);
await poFederationChannel1ForUser2.roomToolbar.openMembersTab();
await poFederationChannel1ForUser2.tabs.members.showAllUsers();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(usernameWithDomainFromServer2)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(userFromServer1UsernameOnly)).toBeVisible();
await expect(poFederationChannel1ForUser2.tabs.members.getUserInList(constants.RC_SERVER_1.username)).toBeVisible();
await page2.close();
});
});
});
test.describe('DMs', () => {
let createdUsernameFromServer2: string;
let usernameWithDomainFromServer2: string;
let fullUsernameFromServer2: string;
const usernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
test.beforeAll(async ({ browser, apiServer2 }) => {
createdUsernameFromServer2 = await registerUser(apiServer2);
const page = await browser.newPage();
const poDmFederationServer1 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: constants.RC_SERVER_1.username,
password: constants.RC_SERVER_1.password,
},
});
fullUsernameFromServer2 = formatIntoFullMatrixUsername(createdUsernameFromServer2, constants.RC_SERVER_2.matrixServerName);
usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
await poDmFederationServer1.createDirectMessagesUsingModal([fullUsernameFromServer2]);
await page.close();
});
test.describe('Visual Elements', () => {
test('expect the calls button to be disabled', async ({ browser, page }) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await expect(poFederationChannelServer1.roomToolbar.btnVoiceCall).toBeDisabled();
await expect(poFederationChannelServer1.roomToolbar.btnVideoCall).toBeDisabled();
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await expect(poFederationChannelServer2.roomToolbar.btnVoiceCall).toBeDisabled();
await expect(poFederationChannelServer1.roomToolbar.btnVideoCall).toBeDisabled();
await pageForServer2.close();
});
test('expect the discussion button to be disabled', async ({ browser, page }) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await expect(poFederationChannelServer1.roomToolbar.btnDiscussion).toBeDisabled();
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await expect(poFederationChannelServer2.roomToolbar.btnDiscussion).toBeDisabled();
await pageForServer2.close();
});
});
test.describe('Owner rights', () => {
test('expect neither the owner nor the invitee to be able to edit the channel info (the channel info button should not be visible)', async ({
browser,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await expect(poFederationChannelServer1.roomToolbar.btnRoomInfo).not.toBeVisible();
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await expect(poFederationChannelServer2.roomToolbar.btnRoomInfo).not.toBeVisible();
await pageForServer2.close();
});
test('expect the owner nor the invitee to be able to add users to DMs (the button to add users should not be visible)', async ({
browser,
page,
}) => {
const pageForServer2 = await browser.newPage();
const poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(usernameWithDomainFromServer2);
await poFederationChannelServer1.content.sendMessage('hello world');
await expect(poFederationChannelServer1.roomToolbar.btnMembers).not.toBeVisible();
await poFederationChannelServer2.navbar.openChat(usernameWithDomainFromServer1);
await expect(poFederationChannelServer2.roomToolbar.btnMembers).not.toBeVisible();
await pageForServer2.close();
});
});
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,367 @@
import type { Page } from '@playwright/test';
import * as constants from '../../config/constants';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createGroupAndInviteRemoteUserToCreateLocalUser } from '../../utils/channel';
import { formatIntoFullMatrixUsername, formatUsernameAndDomainIntoMatrixFormat } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe('Federation - Threads', () => {
let poFederationChannelServer1: FederationChannel;
let poFederationChannelServer2: FederationChannel;
let userFromServer2UsernameOnly: string;
let createdGroupName: string;
let usernameWithDomainFromServer2: string;
const adminUsernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
let pageForServer2: Page;
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer2UsernameOnly = await registerUser(apiServer2);
usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
const page = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
createdGroupName = await createGroupAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ page, browser }) => {
pageForServer2 = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: constants.RC_SERVER_1.username,
password: constants.RC_SERVER_1.password,
},
});
poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.addInitScript(() => {
window.localStorage.setItem('fuselage-localStorage-members-list-type', JSON.stringify('online'));
});
});
test.afterEach(async ({ page }) => {
await page.close();
await pageForServer2.close();
});
test.describe('Messaging - Threads', () => {
test.describe('Create thread message', () => {
test('expect to send a thread message from Server B to Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer1.content.sendMessage('hello world from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await expect(pageForServer2).toHaveURL(/.*thread/);
await expect(poFederationChannelServer2.content.threadSendToChannelAlso()).not.toBeVisible();
await poFederationChannelServer2.content.sendThreadMessage('This is a thread message sent from server B');
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText('This is a thread message sent from server B');
await expect(poFederationChannelServer2.content.lastUserMessage).toContainText('This is a thread message sent from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await expect(page).toHaveURL(/.*thread/);
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText('This is a thread message sent from server B');
});
test('expect to send a thread message from Server A to Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer2.content.sendMessage('hello world from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await expect(page).toHaveURL(/.*thread/);
await expect(poFederationChannelServer1.content.threadSendToChannelAlso()).not.toBeVisible();
await poFederationChannelServer1.content.sendThreadMessage('This is a thread message sent from server A');
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText('This is a thread message sent from server A');
await expect(poFederationChannelServer1.content.lastUserMessage).toContainText('This is a thread message sent from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await expect(pageForServer2).toHaveURL(/.*thread/);
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText('This is a thread message sent from server A');
});
});
test.describe('Send "Special" messages', () => {
test('expect to send a thread message with emojis from Server A to Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer1.content.sendMessage('hello world from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await poFederationChannelServer2.content.sendThreadMessage('😀 😀 hello world 🌎 from server B with emojis 😀 😀');
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText(
'😀 😀 hello world 🌎 from server B with emojis 😀 😀',
);
await expect(poFederationChannelServer2.content.lastUserMessage).toContainText(
'😀 😀 hello world 🌎 from server B with emojis 😀 😀',
);
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText(
'😀 😀 hello world 🌎 from server B with emojis 😀 😀',
);
});
test('expect to send a thread message with emojis from Server B to Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer2.content.sendMessage('hello world from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await poFederationChannelServer1.content.sendThreadMessage('😀 😀 hello world 🌎 from server A with emojis 😀 😀');
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText(
'😀 😀 hello world 🌎 from server A with emojis 😀 😀',
);
await expect(poFederationChannelServer1.content.lastUserMessage).toContainText(
'😀 😀 hello world 🌎 from server A with emojis 😀 😀',
);
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText(
'😀 😀 hello world 🌎 from server A with emojis 😀 😀',
);
});
test('expect to send an audio message from Server A to Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer1.content.sendMessage('hello world from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await poFederationChannelServer2.content.sendAudioRecordedInThreadMessage();
await expect(
await (await poFederationChannelServer2.content.getLastFileThreadMessageByFileName('Audio record.mp3')).innerText(),
).toEqual('Audio record.mp3');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await expect(
await (await poFederationChannelServer1.content.getLastFileThreadMessageByFileName('Audio record.mp3')).innerText(),
).toEqual('Audio record.mp3');
});
test('expect to send an audio message from Server B to Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer2.content.sendMessage('hello world from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await poFederationChannelServer1.content.sendAudioRecordedInThreadMessage();
await expect(
await (await poFederationChannelServer1.content.getLastFileThreadMessageByFileName('Audio record.mp3')).innerText(),
).toEqual('Audio record.mp3');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await expect(
await (await poFederationChannelServer2.content.getLastFileThreadMessageByFileName('Audio record.mp3')).innerText(),
).toEqual('Audio record.mp3');
});
test('expect to send a thread message mentioning an user from Server A to Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer1.content.sendMessage('hello world from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await poFederationChannelServer2.content.sendThreadMessage(
`hello @${adminUsernameWithDomainFromServer1}, here's @${userFromServer2UsernameOnly} from Server B`,
);
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText(
`hello ${adminUsernameWithDomainFromServer1}, here's ${userFromServer2UsernameOnly} from Server B`,
);
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText(
`hello ${constants.RC_SERVER_1.username}, here's ${usernameWithDomainFromServer2} from Server B`,
);
});
test('expect to send a thread message mentioning an user Server B to Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await poFederationChannelServer2.content.sendMessage('hello world from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await poFederationChannelServer1.content.sendThreadMessage(
`hello @${usernameWithDomainFromServer2}, here's @${constants.RC_SERVER_1.username} from Server A`,
);
await expect(poFederationChannelServer1.content.lastThreadMessageText).toContainText(
`hello ${usernameWithDomainFromServer2}, here's ${constants.RC_SERVER_1.username} from Server A`,
);
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await expect(poFederationChannelServer2.content.lastThreadMessageText).toContainText(
`hello ${userFromServer2UsernameOnly}, here's ${adminUsernameWithDomainFromServer1} from Server A`,
);
});
});
test.describe('Message actions', () => {
test('expect to send a thread message quoting a thread message from Server A to Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
const message = `Message for quote - ${Date.now()}`;
await poFederationChannelServer1.content.sendMessage('hello world from server A');
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await poFederationChannelServer2.content.sendThreadMessage(message);
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await poFederationChannelServer1.content.quoteMessageInsideThread('this is a quote message');
await expect(poFederationChannelServer1.content.waitForLastThreadMessageTextAttachmentEqualsText).toContainText(message);
await expect(poFederationChannelServer2.content.waitForLastThreadMessageTextAttachmentEqualsText).toContainText(message);
});
test('expect to send a thread message quoting a thread message Server B to Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
const message = `Message for quote - ${Date.now()}`;
await poFederationChannelServer2.content.sendMessage('hello world from server B');
await poFederationChannelServer1.content.openLastMessageMenu();
await poFederationChannelServer1.content.btnOptionReplyInThread.click();
await poFederationChannelServer1.content.sendThreadMessage(message);
await poFederationChannelServer2.content.openLastMessageMenu();
await poFederationChannelServer2.content.btnOptionReplyInThread.click();
await poFederationChannelServer2.content.quoteMessageInsideThread('this is a quote message');
await expect(poFederationChannelServer1.content.waitForLastThreadMessageTextAttachmentEqualsText).toContainText(message);
await expect(poFederationChannelServer2.content.waitForLastThreadMessageTextAttachmentEqualsText).toContainText(message);
});
});
test.describe('Visual Elements', () => {
test('expect to see the thread list sent in the group on Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdGroupName);
await expect(poFederationChannelServer1.roomToolbar.btnThreads).toBeVisible();
});
test('expect to see the thread list sent in the group on Server B', async () => {
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer2.navbar.openChat(createdGroupName);
await expect(poFederationChannelServer2.roomToolbar.btnThreads).toBeVisible();
});
});
});
});

View File

@@ -0,0 +1,130 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import * as constants from '../../config/constants';
import { FederationAccountProfile } from '../../page-objects/account-profile';
import { FederationChannel } from '../../page-objects/channel';
import { doLogin } from '../../utils/auth';
import { createChannelAndInviteRemoteUserToCreateLocalUser } from '../../utils/channel';
import { formatIntoFullMatrixUsername, formatUsernameAndDomainIntoMatrixFormat } from '../../utils/format';
import { registerUser } from '../../utils/register-user';
import { test, expect, setupTesting, tearDownTesting } from '../../utils/test';
test.describe.parallel('Federation - User Account Pannel', () => {
let poFederationChannelServer1: FederationChannel;
let poFederationChannelServer2: FederationChannel;
let poFederationAccountProfileServer1: FederationAccountProfile;
let userFromServer2UsernameOnly: string;
let createdChannelName: string;
let pageForServer2: Page;
let usernameWithDomainFromServer2: string;
const adminUsernameWithDomainFromServer1 = formatUsernameAndDomainIntoMatrixFormat(
constants.RC_SERVER_1.username,
constants.RC_SERVER_1.matrixServerName,
);
test.beforeAll(async ({ apiServer1, apiServer2, browser }) => {
await setupTesting(apiServer1);
await setupTesting(apiServer2);
userFromServer2UsernameOnly = await registerUser(apiServer2);
const page = await browser.newPage();
poFederationChannelServer1 = new FederationChannel(page);
usernameWithDomainFromServer2 = formatUsernameAndDomainIntoMatrixFormat(
userFromServer2UsernameOnly,
constants.RC_SERVER_2.matrixServerName,
);
const fullUsernameFromServer2 = formatIntoFullMatrixUsername(userFromServer2UsernameOnly, constants.RC_SERVER_2.matrixServerName);
createdChannelName = await createChannelAndInviteRemoteUserToCreateLocalUser({
page,
poFederationChannelServer: poFederationChannelServer1,
fullUsernameFromServer: fullUsernameFromServer2,
server: constants.RC_SERVER_1,
});
});
test.afterAll(async ({ apiServer1, apiServer2 }) => {
await tearDownTesting(apiServer1);
await tearDownTesting(apiServer2);
});
test.beforeEach(async ({ browser, page }) => {
pageForServer2 = await browser.newPage();
poFederationAccountProfileServer1 = new FederationAccountProfile(page);
poFederationChannelServer1 = new FederationChannel(page);
await doLogin({
page,
server: {
url: constants.RC_SERVER_1.url,
username: constants.RC_SERVER_1.username,
password: constants.RC_SERVER_1.password,
},
});
poFederationChannelServer2 = new FederationChannel(pageForServer2);
await doLogin({
page: pageForServer2,
server: {
url: constants.RC_SERVER_2.url,
username: userFromServer2UsernameOnly,
password: constants.RC_SERVER_2.password,
},
storeState: false,
});
await page.addInitScript(() => {
window.localStorage.setItem('fuselage-localStorage-members-list-type', JSON.stringify('online'));
});
});
test.afterEach(async ({ page }) => {
await poFederationChannelServer1.navbar.logout();
await page.close();
});
test('expect to be able to edit a name in Server A and it must be reflected on Server B', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const newName = faker.person.fullName();
await page.goto(`${constants.RC_SERVER_1.url}/account/profile`);
await poFederationAccountProfileServer1.inputName.fill(newName);
await poFederationAccountProfileServer1.btnSubmit.click();
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdChannelName);
await poFederationChannelServer2.navbar.openChat(createdChannelName);
await poFederationChannelServer2.roomToolbar.openMembersTab();
await poFederationChannelServer2.tabs.members.showAllUsers();
await expect(poFederationChannelServer2.tabs.members.getUserInList(adminUsernameWithDomainFromServer1)).toContainText(newName);
await pageForServer2.close();
});
test('expect to be able to edit a name in Server B and it must be reflected on Server A', async ({ page }) => {
await page.goto(`${constants.RC_SERVER_1.url}/home`);
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
const poFederationAccountProfileServer2 = new FederationAccountProfile(pageForServer2);
const newName = faker.person.fullName();
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/account/profile`);
await poFederationAccountProfileServer2.inputName.fill(newName);
await poFederationAccountProfileServer2.btnSubmit.click();
await pageForServer2.goto(`${constants.RC_SERVER_2.url}/home`);
await poFederationChannelServer1.navbar.openChat(createdChannelName);
await poFederationChannelServer2.navbar.openChat(createdChannelName);
await poFederationChannelServer1.roomToolbar.openMembersTab();
await poFederationChannelServer1.tabs.members.showAllUsers();
await expect(poFederationChannelServer1.tabs.members.getUserInList(usernameWithDomainFromServer2)).toContainText(newName);
await pageForServer2.close();
});
});

View File

@@ -0,0 +1,28 @@
import type { Page } from '@playwright/test';
export const doLogin = async ({
page,
server,
storageNamePrefix,
storeState,
}: {
page: Page;
server: {
username: string;
password: string;
url: string;
};
storageNamePrefix?: string;
storeState?: boolean;
}) => {
await page.goto(`${server.url}/login`);
await page.locator('role=textbox[name=/username/i]').type(server.username);
await page.locator('[name=password]').type(server.password);
await page.locator('role=button[name="Login"]').click();
await page.waitForTimeout(1000);
if (storeState) {
await page.context().storageState({ path: `${storageNamePrefix}-session.json` });
}
};

View File

@@ -0,0 +1,85 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { doLogin } from './auth';
import type { API } from './test';
import type { FederationChannel } from '../page-objects/channel';
const doLoginAndGoToHome = async (
page: Page,
server: {
url: string;
username: string;
password: string;
},
): Promise<void> => {
await doLogin({
page,
server,
});
await page.goto(`${server.url}/home`);
};
export const createChannelAndInviteRemoteUserToCreateLocalUser = async ({
page,
poFederationChannelServer,
fullUsernameFromServer,
server,
closePageAfterCreation = true,
}: {
page: Page;
poFederationChannelServer: FederationChannel;
fullUsernameFromServer: string;
server: {
url: string;
username: string;
password: string;
};
closePageAfterCreation?: boolean;
}): Promise<string> => {
const channelName = faker.string.uuid();
await doLoginAndGoToHome(page, server);
await poFederationChannelServer.createPublicChannelAndInviteUsersUsingCreationModal(channelName, [fullUsernameFromServer]);
if (closePageAfterCreation) {
await page.close();
}
return channelName;
};
export const createGroupAndInviteRemoteUserToCreateLocalUser = async ({
page,
poFederationChannelServer,
fullUsernameFromServer,
server,
}: {
page: Page;
poFederationChannelServer: FederationChannel;
fullUsernameFromServer: string;
server: {
url: string;
username: string;
password: string;
matrixServerName: string;
};
}): Promise<string> => {
const groupName = faker.string.uuid();
await doLoginAndGoToHome(page, server);
await poFederationChannelServer.createPrivateGroupAndInviteUsersUsingCreationModal(groupName, [fullUsernameFromServer]);
await page.close();
return groupName;
};
export const createGroupUsingAPI = async (api: API, name: string) => {
await api.post('/groups.create', { name });
};
export const createChannelUsingAPI = async (api: API, name: string) => {
await api.post('/channels.create', { name });
};

View File

@@ -0,0 +1,4 @@
export const formatUsernameAndDomainIntoMatrixFormat = (username: string, domain: string): string => `${username}:${domain}`;
export const formatIntoFullMatrixUsername = (username: string, domain: string): string =>
`@${formatUsernameAndDomainIntoMatrixFormat(username, domain)}`;

View File

@@ -0,0 +1,17 @@
import { faker } from '@faker-js/faker';
import type { API } from './test';
import * as constants from '../config/constants';
export async function registerUser(api: API): Promise<string> {
const username = faker.string.uuid();
await api.post('/users.register', {
username,
email: `${username}@test-rc.com`,
pass: constants.RC_SERVER_2.password,
name: username,
});
return username;
}

View File

@@ -0,0 +1,84 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { APIResponse } from '@playwright/test';
import { test as baseTest } from '@playwright/test';
import { API_PREFIX } from '../../config/constants';
import * as constants from '../config/constants';
export type AnyObj = { [key: string]: any };
export type API = {
get(uri: string, prefix?: string): Promise<APIResponse>;
post(uri: string, data: AnyObj, prefix?: string): Promise<APIResponse>;
put(uri: string, data: AnyObj, prefix?: string): Promise<APIResponse>;
delete(uri: string, prefix?: string): Promise<APIResponse>;
};
export type BaseTest = {
apiServer1: API;
apiServer2: API;
};
const api = async (request: any, use: any, user: string, password: string, baseUrl: string) => {
const resp = await request.post(`${baseUrl + API_PREFIX}/login`, { data: { user, password } });
const json = await resp.json();
const headers = {
'X-Auth-Token': json.data.authToken,
'X-User-Id': json.data.userId,
};
await use({
get(uri: string, prefix = API_PREFIX) {
return request.get(baseUrl + prefix + uri, { headers });
},
post(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.post(baseUrl + prefix + uri, { headers, data });
},
put(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.put(baseUrl + prefix + uri, { headers, data });
},
delete(uri: string, prefix = API_PREFIX) {
return request.delete(baseUrl + prefix + uri, { headers });
},
});
};
export const test = baseTest.extend<BaseTest>({
apiServer1: async ({ request }, use) =>
api(request, use, constants.RC_SERVER_1.username, constants.RC_SERVER_1.password, constants.RC_SERVER_1.url),
apiServer2: async ({ request }, use) =>
api(request, use, constants.RC_SERVER_2.username, constants.RC_SERVER_2.password, constants.RC_SERVER_2.url),
});
export const { expect } = test;
export const setupTesting = async (api: API) => {
await api.post('/settings/Message_AudioRecorderEnabled', { value: true });
await api.post('/settings/Accounts_ManuallyApproveNewUsers', { value: false });
await api.post('/settings/API_Enable_Rate_Limiter', { value: false });
await api.post('/settings/DDP_Rate_Limit_IP_Enabled', { value: false });
await api.post('/settings/DDP_Rate_Limit_User_Enabled', { value: false });
await api.post('/settings/DDP_Rate_Limit_Connection_Enabled', { value: false });
await api.post('/settings/DDP_Rate_Limit_Connection_Interval_Time', { value: false });
await api.post('/settings/DDP_Rate_Limit_User_By_Method_Enabled', { value: false });
await api.post('/settings/DDP_Rate_Limit_Connection_By_Method_Enabled', { value: false });
await api.post('/settings/Accounts_RegistrationForm', { value: 'Public' });
await api.post('/settings/UI_Use_Real_Name', { value: false });
await api.post('/settings/Rate_Limiter_Limit_RegisterUser', { value: 10 });
await api.post('/settings/Hide_System_Messages', { value: [] });
await api.post('/permissions.update', { permissions: [{ _id: 'force-delete-message', roles: ['admin', 'user'] }] });
};
export const tearDownTesting = async (api: API) => {
await api.post('/settings/Accounts_ManuallyApproveNewUsers', { value: true });
await api.post('/settings/API_Enable_Rate_Limiter', { value: true });
await api.post('/settings/DDP_Rate_Limit_IP_Enabled', { value: true });
await api.post('/settings/DDP_Rate_Limit_User_Enabled', { value: true });
await api.post('/settings/DDP_Rate_Limit_Connection_Enabled', { value: true });
await api.post('/settings/DDP_Rate_Limit_Connection_Interval_Time', { value: true });
await api.post('/settings/DDP_Rate_Limit_User_By_Method_Enabled', { value: true });
await api.post('/settings/DDP_Rate_Limit_Connection_By_Method_Enabled', { value: true });
await api.post('/settings/Rate_Limiter_Limit_RegisterUser', { value: 1 });
await api.post('/settings/Accounts_RegistrationForm', { value: 'Disabled' });
};

View File

@@ -0,0 +1,248 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { FileUploadWarningModal } from './page-objects/fragments/modals';
import { createTargetChannel } from './utils';
import { setSettingValueById } from './utils/setSettingValueById';
import { expect, test } from './utils/test';
test.use({ storageState: Users.user1.state });
const TEST_FILE_TXT = 'any_file.txt';
const TEST_FILE_LST = 'lst-test.lst';
const TEST_FILE_DRAWIO = 'diagram.drawio';
const TEST_EMPTY_FILE = 'empty_file.txt';
test.describe.serial('file-upload', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml');
targetChannel = await createTargetChannel(api, { members: ['user1'] });
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml');
expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200);
});
test('should cancel uploaded file attached to message composer', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT);
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible();
});
test('should send file with name updated', async () => {
const updatedFileName = `edited_${TEST_FILE_TXT}`;
await poHomeChannel.content.dragAndDropTxtFile();
await test.step('update file name and send', async () => {
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill(updatedFileName);
await poHomeChannel.content.btnUpdateFileUpload.click();
await expect(poHomeChannel.composer.getFileByName(updatedFileName)).toBeVisible();
await poHomeChannel.composer.btnSend.click();
});
await expect(poHomeChannel.content.getLastMessageByFileName(updatedFileName)).toContainText(updatedFileName);
});
test('should attach multiple files and send one per message', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.dragAndDropLstFile();
await expect(poHomeChannel.composer.inputMessage).toBeFocused();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toBeVisible();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)).toBeVisible();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessageDownloadLink).toHaveCount(1);
});
test('should not be able to attach files when editing a message', async () => {
await poHomeChannel.content.sendMessage('message to be edited');
await poHomeChannel.content.openLastMessageMenu();
await poHomeChannel.content.btnOptionEditMessage.click();
await poHomeChannel.content.dragAndDropTxtFile({ waitForResponse: false });
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible();
});
test('should send lst file successfully', async () => {
await poHomeChannel.content.dragAndDropLstFile();
await poHomeChannel.composer.inputMessage.fill('lst_description');
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description');
await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_LST)).toBeVisible();
});
test('should send drawio (unknown media type) file successfully', async ({ page }) => {
await page.reload();
await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO);
await poHomeChannel.composer.inputMessage.fill('drawio_description');
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description');
await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_DRAWIO)).toBeVisible();
});
test('should not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => {
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream');
await page.reload();
await poHomeChannel.content.sendFileMessage(TEST_FILE_DRAWIO, { waitForResponse: false });
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_DRAWIO)).toHaveAttribute('readonly');
});
test('should be able to remove file from composer before sending', async () => {
await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT);
await poHomeChannel.content.sendFileMessage(TEST_FILE_LST);
await poHomeChannel.composer.removeFileByName(TEST_FILE_TXT);
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_LST)).toBeVisible();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.lastUserMessage).not.toContainText(TEST_FILE_TXT);
await expect(poHomeChannel.content.lastUserMessage).toContainText(TEST_FILE_LST);
});
test('should respect the maximum number of files allowed per message: 10', async () => {
const files = new Array(10).fill('number1.png');
await Promise.all(files.map((file) => poHomeChannel.content.sendFileMessage(file)));
await poHomeChannel.content.dragAndDropTxtFile({ waitForResponse: false });
await expect(poHomeChannel.composer.getFilesInComposer()).toHaveCount(10);
await expect(poHomeChannel.composer.getFileByName('any_file.txt')).not.toBeVisible();
});
test('should upload file in composer after recording video message', async ({ context }) => {
await context.grantPermissions(['camera', 'microphone']);
await poHomeChannel.gotoChannel(targetChannel);
await test.step('should be able to record a video with text content in composer ', async () => {
await poHomeChannel.composer.inputMessage.fill('this is a message with video message');
await expect(poHomeChannel.composer.btnVideoMessage).toBeEnabled();
});
await poHomeChannel.composer.btnVideoMessage.click();
await poHomeChannel.composer.videoRecorderPopup.record();
await expect(poHomeChannel.composer.getFileByName('Video record.webm')).toBeVisible();
});
test.describe.serial('thread multiple file upload', () => {
test('should be able to remove file from thread composer before sending', async () => {
await poHomeChannel.content.sendMessage('this is a message for thread reply');
await poHomeChannel.content.openReplyInThread();
await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_TXT);
await poHomeChannel.content.sendFileMessageToThread(TEST_FILE_LST);
await poHomeChannel.threadComposer.removeFileByName(TEST_FILE_LST);
await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_TXT)).toBeVisible();
await expect(poHomeChannel.threadComposer.getFileByName(TEST_FILE_LST)).not.toBeVisible();
});
});
test.describe.serial('file upload fails', () => {
let fileUploadWarningModal: FileUploadWarningModal;
test.beforeAll(async ({ api }) => {
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream');
});
test.afterAll(async ({ api }) => {
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml');
});
test('should open warning modal when all file uploads fail', async ({ page }) => {
fileUploadWarningModal = new FileUploadWarningModal(page.getByRole('dialog', { name: 'Warning' }));
await poHomeChannel.content.sendMultipleFilesMessage([TEST_EMPTY_FILE, TEST_FILE_DRAWIO], { waitForResponse: false });
await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toHaveAttribute('readonly');
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_DRAWIO)).toHaveAttribute('readonly');
await poHomeChannel.composer.btnSend.click();
await fileUploadWarningModal.waitForDisplay();
await expect(fileUploadWarningModal.getContent('2 files failed to upload')).toBeVisible();
await expect(fileUploadWarningModal.btnOk).toBeVisible();
await expect(fileUploadWarningModal.btnSendAnyway).not.toBeVisible();
});
test('should handle multiple files with one failing upload', async ({ page }) => {
fileUploadWarningModal = new FileUploadWarningModal(page.getByRole('dialog', { name: 'Are you sure' }));
await test.step('should only mark as "Upload failed" the specific file that failed to upload', async () => {
await poHomeChannel.content.sendMultipleFilesMessage([TEST_FILE_TXT, TEST_EMPTY_FILE], { waitForResponse: false });
await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toHaveAttribute('readonly');
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toBeVisible();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toHaveAttribute('readonly');
});
await test.step('should open warning modal', async () => {
await poHomeChannel.composer.btnSend.click();
await fileUploadWarningModal.waitForDisplay();
await expect(fileUploadWarningModal.getContent('1 file failed to upload')).toBeVisible();
});
await test.step('should close modal when clicking "Cancel" button', async () => {
await fileUploadWarningModal.cancel();
await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).toBeVisible();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).toBeVisible();
});
await test.step('should send message with the valid file when confirming "Send anyway"', async () => {
await poHomeChannel.composer.btnSend.click();
await fileUploadWarningModal.confirmSend();
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible();
await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).toContainText(TEST_FILE_TXT);
await expect(poHomeChannel.composer.getFileByName(TEST_EMPTY_FILE)).not.toBeVisible();
});
});
});
});
test.describe('file-upload-not-member', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api);
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test.afterAll(async ({ api }) => {
expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200);
});
test('should not be able to upload if not a member', async () => {
await poHomeChannel.content.dragAndDropTxtFile({ waitForResponse: false });
await expect(poHomeChannel.composer.getFileByName(TEST_FILE_TXT)).not.toBeVisible();
});
});

View File

@@ -0,0 +1,54 @@
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel } from './utils';
import { expect, test } from './utils/test';
test.use({ storageState: Users.user1.state });
const TEST_FILE_TXT = 'any_file.txt';
test.describe.serial('files-management', () => {
let poHomeChannel: HomeChannel;
let targetChannel: string;
test.beforeAll(async ({ api }) => {
targetChannel = await createTargetChannel(api, { members: ['user1'] });
});
test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await poHomeChannel.gotoChannel(targetChannel);
});
test.afterAll(async ({ api }) => {
expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200);
});
test('should send a file and manage it in the list', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.composer.btnSend.click();
await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).toBeVisible();
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemFiles.click();
await expect(poHomeChannel.tabs.files.getFileByName(TEST_FILE_TXT)).toBeVisible();
await test.step('should open download dialog when clicking on the file in the files tab', async () => {
const [download] = await Promise.all([
poHomeChannel.page.waitForEvent('download'),
poHomeChannel.tabs.files.getFileByName(TEST_FILE_TXT).click(),
]);
expect(download).toBeDefined();
expect(download.suggestedFilename()).toBe(TEST_FILE_TXT);
});
await test.step('should delete the file from the list', async () => {
await poHomeChannel.tabs.files.deleteFile(TEST_FILE_TXT);
await expect(poHomeChannel.tabs.files.getFileByName(TEST_FILE_TXT)).toHaveCount(0);
await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).not.toBeVisible();
});
});
});

View File

@@ -0,0 +1,21 @@
import { request } from '@playwright/test';
import { Users } from './userStates';
import { BASE_API_URL } from '../config/constants';
export default async function addCustomOAuth(): Promise<void> {
const api = await request.newContext();
const headers = {
'X-Auth-Token': Users.admin.data.loginToken,
'X-User-Id': Users.admin.data._id,
};
await api.post(`${BASE_API_URL}/settings.addCustomOAuth`, { data: { name: 'Test' }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test`, { data: { value: false }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-url`, { data: { value: 'https://rocket.chat' }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-login_style`, { data: { value: 'redirect' }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-id`, { data: { value: 'abc' }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-secret`, { data: { value: 'def' }, headers });
await api.post(`${BASE_API_URL}/settings/Accounts_OAuth_Custom-Test-authorize_path`, { data: { value: '/authorize' }, headers });
}

View File

@@ -0,0 +1,53 @@
import { faker } from '@faker-js/faker';
import type { IUser } from '@rocket.chat/core-typings';
import { DEFAULT_USER_CREDENTIALS } from '../../config/constants';
import type { IUserState } from '../userStates';
type UserFixture = IUser & {
username: string;
__rooms: string[];
};
export function createUserFixture(user: IUserState): UserFixture {
const { username, hashedToken, loginExpire, e2e } = user.data;
return {
_id: `${username}`,
type: 'user',
active: true,
emails: [{ address: `${username}@email.com`, verified: true }],
roles: ['user'],
name: username,
lastLogin: new Date(),
statusConnection: 'offline',
utcOffset: -3,
username,
services: {
password: { bcrypt: DEFAULT_USER_CREDENTIALS.bcrypt },
email2fa: { enabled: true, changedAt: new Date() },
email: {
verificationTokens: [
{
token: faker.string.uuid(),
address: `${username}@email.com`,
when: new Date(),
},
],
},
resume: {
loginTokens: [
{
when: loginExpire,
hashedToken,
},
],
},
emailCode: { code: '', attempts: 0, expire: new Date() },
},
createdAt: new Date(),
_updatedAt: new Date(),
__rooms: ['GENERAL'],
e2e,
};
}

View File

@@ -0,0 +1,19 @@
import type { Browser, Page } from '@playwright/test';
import type { IUserState } from './userStates';
export const createAuxContext = async (
browser: Browser,
userState: IUserState,
route = '/',
waitForMainContent = true,
): Promise<{ page: Page }> => {
const page = await browser.newPage({ storageState: userState.state });
await page.goto(route);
if (waitForMainContent) {
await page.locator('#main-content').waitFor();
}
return { page };
};

View File

@@ -0,0 +1 @@
another_file

Some files were not shown because too many files have changed in this diff Show More