snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
410
apps/meteor/tests/e2e/README.md
Normal file
410
apps/meteor/tests/e2e/README.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# E2E Testing with playwright
|
||||
|
||||
## Running tests
|
||||
The application must be started with `TEST_MODE=true`
|
||||
|
||||
```sh
|
||||
$ TEST_MODE=true yarn dev
|
||||
```
|
||||
|
||||
Then we can run a single suite with
|
||||
|
||||
```sh
|
||||
$ yarn test:e2e ./tests/e2e/administration.spec.ts
|
||||
```
|
||||
|
||||
Or all the tests with
|
||||
|
||||
```sh
|
||||
$ yarn test:e2e
|
||||
```
|
||||
|
||||
We can also provide some env vars to `test:e2e` script:
|
||||
- `BASE_URL=<any_url>` Run the tests to the given url
|
||||
- `PWDEBUG=1` Control the test execution
|
||||
|
||||
## Page Objects
|
||||
- Any locator name must start with of one the following prefixes: `btn`, `link`, `input`, `select`, `checkbox`, `text`
|
||||
|
||||
## Important links
|
||||
- [playwright docs](https://playwright.dev/docs/intro)
|
||||
|
||||
## Assertions and waiting
|
||||
|
||||
- Prefer Playwright web-first assertions such as `toBeVisible()` and `toHaveText()`. They retry until the expected condition is met.
|
||||
- Use Playwright's `expect` matchers instead of Node-style `assert` statements.
|
||||
- Rely on Playwright's built-in auto-waiting whenever possible.
|
||||
- When an explicit wait is necessary, wait for a specific condition with APIs such as `locator.waitFor()`, `page.waitForURL()`, or `page.waitForResponse()`.
|
||||
- Prefer waiting for observable conditions over fixed delays. If a fixed delay is unavoidable, document why no observable condition can be awaited.
|
||||
|
||||
```ts
|
||||
await expect(anyElement).toBeVisible();
|
||||
```
|
||||
|
||||
# Playwright Locator Best Practices
|
||||
|
||||
## Preferred locator types:
|
||||
|
||||
### 1. By role `.getByRole()`:
|
||||
This is the most recommended locator type.
|
||||
- It ensures our element is accessible to screen readers and other assistive technologies.
|
||||
- It ensures that the element is uniquely identifiable.
|
||||
- It's recommended to use the `exact` option to ensure that the locator doesn't match any other element with the same role
|
||||
|
||||
|
||||
```ts
|
||||
await page.getByRole('button', { name: 'Save', exact: true });
|
||||
|
||||
// Without the `exact` option, Playwright would match buttons with 'Save' and 'Save changes' labels.
|
||||
```
|
||||
|
||||
|
||||
### 2. By text `.getByText()` or `.getByLabel()`:
|
||||
- Use this when locating by role is not possible or not sufficient.
|
||||
- Be sure to restrict the scope of the locator to the element you are looking for.
|
||||
|
||||
### 3. Locator with `has`:
|
||||
Our input elements hide the native input and render a custom component.
|
||||
|
||||
To target the input and trigger changes, you should locate the label that wraps the input and use the `has` locator.
|
||||
```ts
|
||||
page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Private' }) });
|
||||
```
|
||||
|
||||
## Locator types to avoid (at all costs):
|
||||
If you are not able to find the element by role, label or text, it's a sign that the element is not accessible to the user.
|
||||
The component should be refactored to allow a more accessible locator.
|
||||
### 1. By `data-qa-id` or testId `.getByTestId()`:
|
||||
```ts
|
||||
// DON'T ❌
|
||||
page.locator('[data-qa-id="menu-more-actions"]');
|
||||
|
||||
// DO ✅
|
||||
page.getByRole('menu', {name: 'More actions', exact: true });
|
||||
```
|
||||
|
||||
### 2. By elements and class names `.locator('div.class-name')`:
|
||||
The HTML structure and/or class names can easily change over time, so it's better to use the locator by role and name.
|
||||
|
||||
```ts
|
||||
// DON'T ❌
|
||||
page.locator('#modal-root .rcx-button-group--align-end .rcx-button--primary');
|
||||
|
||||
// #modal-root was used to locate the injected portal element for the modal.
|
||||
// This can be better located by role 'dialog' and making sure the modal has the proper name attribute.
|
||||
|
||||
// DO ✅
|
||||
page.getByRole('dialog', name: 'Modal name example').getByRole('button', { name: 'Confirm', exact: true });
|
||||
```
|
||||
|
||||
### 3. By position:
|
||||
Using `nth-child` or similar selectors is not recommended as it can easily change.
|
||||
It's a fragile locator and it's hard to maintain.
|
||||
|
||||
### 4. Parent/Child relationships:
|
||||
A change in the DOM structure can break the test.
|
||||
|
||||
## Use our page-objects:
|
||||
> apps/meteor/tests/e2e/page-objects
|
||||
- page-objects are a great way to reuse locators across tests using `getters` and `methods`.
|
||||
- They make it easier to write tests that are more readable and maintainable.
|
||||
- Always make sure to use the most restricted scope possible - not the whole page to avoid multiple matches.
|
||||
- Page objects own navigation - see [Navigation](#navigation-goto-owns-the-wait).
|
||||
|
||||
If you are writing a new test, make sure to look at the existing page-objects to see if there is a suitable one for your use case.
|
||||
|
||||
If not, evaluate if creating a reusable getter/method is worth it and create the ones you need in the respective context.
|
||||
|
||||
E.g.:
|
||||
Writing a new test for a sidebar behavior.
|
||||
- check `apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts` if the fragments you need are already there.
|
||||
- If not, create new getters and/or methods in this file.
|
||||
|
||||
Usage example:
|
||||
|
||||
```ts
|
||||
// apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
|
||||
// ...
|
||||
|
||||
get sidebar(): Locator {
|
||||
return this.page.getByRole('navigation', { name: 'sidebar' });
|
||||
}
|
||||
|
||||
get channelsList(): Locator {
|
||||
return this.sidebar.getByRole('list', { name: 'Channels' });
|
||||
}
|
||||
|
||||
// Restricted scope: inside navigation > sidebar > list named Channels > link with name
|
||||
getSearchRoomByName(name: string) {
|
||||
return this.channelsList.getByRole('link', { name });
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// test.spec.ts
|
||||
|
||||
test('should display sidebar items', async ({ page }) => {
|
||||
poHomeChannel = new HomeChannel(page);
|
||||
await poHomeChannel.goto();
|
||||
const targetChannel = 'channel-test';
|
||||
|
||||
await expect(poHomeChannel.sidebar.getSearchRoomByName(targetChannel)).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Navigation: `goto()` owns the wait
|
||||
|
||||
`page.goto()` resolves as soon as the document loads, long before the SPA has rendered - the next
|
||||
locator then races the app. So page objects own navigation: `goto()` navigates *and* waits.
|
||||
|
||||
```ts
|
||||
poAdminUsers = new AdminUsers(page);
|
||||
await poAdminUsers.goto(); // the next line can act on the page
|
||||
```
|
||||
|
||||
Never call `page.goto()` from a test. If the page you need has no page object, add one - under
|
||||
`Admin`, `Account` or `OmnichannelAdmin` it is usually two lines:
|
||||
|
||||
```ts
|
||||
export class AdminThirdPartyLogin extends Admin {
|
||||
protected readonly route = AdminSectionsHref.thirdPartyLogin;
|
||||
|
||||
protected readonly title = 'Third-party login';
|
||||
}
|
||||
```
|
||||
|
||||
`title` is the page's heading, matched with `exact: true` - copy it from
|
||||
`packages/i18n/src/locales/en.i18n.json` rather than guessing the casing. It also scopes
|
||||
`pageContent`, the `main` region to hang the page's other locators off.
|
||||
|
||||
Anything else extends `RoutedPage` directly and implements `waitForReady()`. Override it when the
|
||||
heading is not enough - `AdminUsers` waits for its search box, `OmnichannelAdmin` for its table.
|
||||
|
||||
Two more hooks:
|
||||
|
||||
- `gotoExpecting(until)` - the route settles on something else, e.g. an upsell dialog on CE.
|
||||
- `navigateTo(route, until?)` - build extra `gotoSomething()` methods when a page object owns more
|
||||
than one route (`gotoGeneral()`, `gotoChannel(name)`). `until` takes a `Locator` or an async waiter.
|
||||
|
||||
Auth screens (`Login`, `Registration`, `Authenticated`) take the url to navigate to, since an
|
||||
anonymous visitor asking for `/home` lands somewhere else.
|
||||
|
||||
A raw `page.goto()` is still right for external urls, urls derived from `page.url()`, and tests that
|
||||
must not wait for the route to settle.
|
||||
|
||||
## Cleanup after tests
|
||||
- Remember to delete all users, channels, rooms, etc, created during the tests.
|
||||
- Reset settings to their default values after the tests, if changed.
|
||||
- Close all new pages opened during the tests.
|
||||
- You can use the `test.afterAll()` or `test.afterEach()` methods to clean up after the tests.
|
||||
|
||||
|
||||
```ts
|
||||
// test.spec.ts
|
||||
|
||||
test.describe.serial('feature example', ({ api}) => {
|
||||
let targetChannel;
|
||||
let targetDiscussion;
|
||||
|
||||
test.beforeAll(async ({ api }) => {
|
||||
// change setting value
|
||||
await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true);
|
||||
|
||||
// create channel
|
||||
targetChannel = await createTargetChannel(api, { members: ['user1'] });
|
||||
|
||||
// create discussion
|
||||
targetDiscussion = await createTargetDiscussion(api);
|
||||
});
|
||||
test.afterAll(async ({ api }) => {
|
||||
// reset setting value
|
||||
await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false);
|
||||
|
||||
// delete channel
|
||||
await deleteChannel(api, targetChannel);
|
||||
|
||||
// delete discussion
|
||||
await deleteRoom(api, targetDiscussion._id);
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
## General recommendations
|
||||
|
||||
### Use descriptive test names
|
||||
|
||||
Test names should clearly describe the behavior and expected outcome being verified.
|
||||
|
||||
### Use `test.describe` for grouping tests
|
||||
It can be used to group related tests into a test suite. It provides a way to organize tests logically and improve the readability and maintainability of your test code.
|
||||
```ts
|
||||
test.describe('Feature Test', () => {
|
||||
test('should display the feature heading after navigation', async ({ api}) => {
|
||||
// do some tests
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Use `test.step` for grouping steps
|
||||
Enhances test readability and provides more detailed information in test reports
|
||||
```ts
|
||||
test.describe('Feature Test', () => {
|
||||
test('should display the feature heading after navigation', async ({ page }) => {
|
||||
await test.step('open the feature page', async () => {
|
||||
const poFeature = new FeaturePage(page);
|
||||
await poFeature.goto();
|
||||
await expect(page.getByRole('heading', { name: 'Feature' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Big test files should not be `.serial`:
|
||||
- Ok for tests with very few steps, when steps have dependencies on each other, you can simulate the user flow sequentially.
|
||||
- Avoid chaining big tests with `test.serial` - dependant steps make it harder to debug and/or make small changes
|
||||
> If you are changing something on the 34th step, you would have to run the whole test suite multiple times during development, instead of olny running the test step in question.
|
||||
|
||||
## Performance patterns
|
||||
|
||||
E2E tests are the most expensive tests we run. Two patterns, applied together, typically cut a suite's runtime by ~80% and remove the most common sources of flake. They are the standard for new suites and the target for migrating existing ones.
|
||||
|
||||
### Pattern 1 — seed state through the API, not the UI
|
||||
|
||||
Use the browser **only for what the test actually verifies**. Everything else — creating rooms, DMs, discussions, threads, sending setup messages — goes through REST.
|
||||
|
||||
Why:
|
||||
- A REST call costs 30–150ms; the equivalent UI flow (menu → modal → input → submit → wait for nav) costs 2–8s per step.
|
||||
- API errors are deterministic and synchronous. UI errors manifest as timeouts on overloaded CI, which is a slow and ambiguous signal.
|
||||
- The test fails only when the feature under test breaks, not when an unrelated setup UI regresses. A broken "create discussion" modal should not take down every test that needs a discussion as fixture.
|
||||
- Removes the most flake-prone interactions: hover-revealed menus, modal animations, search debounces.
|
||||
|
||||
Do **not** apply when the setup UI is the actual subject of the test (`create-direct.spec.ts`, `create-discussion.spec.ts`, `create-channel.spec.ts`, etc.). There, keep one test per creation flow going through the UI; unrelated tests rely on the API path.
|
||||
|
||||
### Pattern 2 — share the browser context across a serial suite
|
||||
|
||||
For suites already using `test.describe.serial`, move `browser.newContext(), pageObject.goto()` and the initial room navigation from `beforeEach` to `beforeAll`.
|
||||
|
||||
Why: bootstrapping a Playwright context plus hydrating the Meteor app costs ~1.5–2.5s per test. In a serial suite the isolation cost is already paid — collecting the speed benefit is free.
|
||||
|
||||
Preconditions:
|
||||
- Suite is `test.describe.serial`.
|
||||
- All tests share the same user / storage state.
|
||||
- Each test leaves the page in a known "home position" (base room open, no modals, threads, or side panels lingering).
|
||||
|
||||
Do **not** apply when:
|
||||
- Tests need different users or storage states.
|
||||
- Suite covers auth, 2FA, or session lifecycle — the browser state is the subject.
|
||||
- Suite has more than ~15 tests: the debug cost of shared state outgrows the speed win (see "Big test files should not be `.serial`" above).
|
||||
|
||||
### Pattern 3 — navigate to a known room by URL, not by search
|
||||
|
||||
When a test already knows the room it needs (a channel created via API, `general`, etc.), open it directly with `poHomeChannel.gotoChannel(name)` instead of going home and searching for it.
|
||||
|
||||
```ts
|
||||
// ❌ fragile — three independent things must align before the combobox `fill` resolves
|
||||
await page.goto('/home');
|
||||
await poHomeChannel.navbar.openChat(targetChannel);
|
||||
|
||||
// ✅ deterministic — navigate straight to the room, then wait for it to be ready
|
||||
await poHomeChannel.gotoChannel(targetChannel);
|
||||
```
|
||||
|
||||
Why: searching the navbar couples the navigation to (1) the app shell hydrating, (2) the **search index** having picked up the room — a real lag for a channel created moments earlier via API — and (3) the listbox rendering and the option becoming clickable. Any one stalling makes `searchInput.fill` hang until the test timeout, surfacing as the misleading `Target page, context or browser has been closed`. `gotoChannel` loads `/channel/<name>` and then `waitForChannel()` (main region, heading, message list, `aria-busy` cleared), so it has a proper readiness wait without the search dependency.
|
||||
|
||||
Do **not** apply when:
|
||||
- The navbar search is the actual subject of the test.
|
||||
- Navigating to a **DM** (`/direct/<username>`) or a **discussion** — `gotoChannel` builds a `/channel/` URL only.
|
||||
- Reaching a channel the user is **not a member of** — the direct URL hits a different preview/join flow; verify the behavior before switching.
|
||||
|
||||
## API helpers for state seeding
|
||||
|
||||
Prefer these helpers in `beforeAll` / `beforeEach` and in setup `test.step`s. All live under `apps/meteor/tests/e2e/utils/`.
|
||||
|
||||
| Intent | Helper | REST endpoint |
|
||||
| ------------------------------------ | --------------------------------------------------------- | ------------------------- |
|
||||
| Create public channel | `createTargetChannel(api)` | `/channels.create` |
|
||||
| Create public channel (full room) | `createTargetChannelAndReturnFullRoom(api)` | `/channels.create` |
|
||||
| Create private channel | `createTargetPrivateChannel(api)` | `/groups.create` |
|
||||
| Create private group (full room) | `createTargetGroupAndReturnFullRoom(api)` | `/groups.create` |
|
||||
| Create team | `createTargetTeam(api)` | `/teams.create` |
|
||||
| Create discussion (fresh parent) | `createTargetDiscussion(api)` | `/rooms.createDiscussion` |
|
||||
| Create discussion on existing msg | `createDiscussion(api, parentRoomId, parentMsgId, name)` | `/rooms.createDiscussion` |
|
||||
| Create DM room (get id back) | `createDirectMessageRoom(api, username)` | `/im.create` |
|
||||
| Send message to a room | `sendMessage(api, roomId, msg)` | `/chat.sendMessage` |
|
||||
| Send message inside a thread | `sendMessage(api, roomId, msg, parentMsgId)` | `/chat.sendMessage` |
|
||||
| Send message as a specific user | `sendMessageFromUser(request, user, rid, msg)` | `/chat.postMessage` |
|
||||
| Delete channel (by name) | `deleteChannel(api, roomName)` | `/channels.delete` |
|
||||
| Delete room (by id) | `deleteRoom(api, roomId)` | `/rooms.delete` |
|
||||
| Delete team | `deleteTeam(api, teamName)` | `/teams.delete` |
|
||||
|
||||
If the helper you need is missing, add it under `utils/` and re-export it from `utils/index.ts` rather than inlining the REST call in the spec.
|
||||
|
||||
## Template: optimized `.serial` suite
|
||||
|
||||
Copy-paste starting point for a new suite that applies both patterns.
|
||||
|
||||
```ts
|
||||
import type { Page, BrowserContext } from 'playwright-core';
|
||||
|
||||
import { Users } from './fixtures/userStates';
|
||||
import { HomeChannel } from './page-objects';
|
||||
import { createTargetChannel, sendMessage } from './utils';
|
||||
import { expect, test } from './utils/test';
|
||||
|
||||
test.use({ storageState: Users.admin.state });
|
||||
|
||||
test.describe.serial('Feature X', () => {
|
||||
let poHomeChannel: HomeChannel;
|
||||
let targetChannel: string;
|
||||
let targetChannelId: string;
|
||||
let page: Page;
|
||||
let context: BrowserContext;
|
||||
|
||||
test.beforeAll(async ({ browser, api }) => {
|
||||
targetChannel = await createTargetChannel(api);
|
||||
const info = await (await api.get(`/channels.info?roomName=${targetChannel}`)).json();
|
||||
targetChannelId = info.channel._id;
|
||||
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
poHomeChannel = new HomeChannel(page);
|
||||
|
||||
await poHomeChannel.gotoChannel(targetChannel);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ api }) => {
|
||||
await api.post('/channels.delete', { roomName: targetChannel });
|
||||
await page.close();
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('exercises the feature', async ({ api }) => {
|
||||
const messageText = 'seed';
|
||||
await sendMessage(api, targetChannelId, messageText);
|
||||
await expect(poHomeChannel.content.lastUserMessage).toContainText(messageText);
|
||||
// assertions for the feature under test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating an existing suite
|
||||
|
||||
Recipe for a single spec. Keep PRs to at most 5 files so reviews stay tractable.
|
||||
|
||||
1. **Baseline**. Run the spec three times locally and record the median per-test time. Paste it in the PR body.
|
||||
2. **Map setup**. For every test, mark each `test.step` and every `beforeEach` call that only prepares state (creates rooms, messages, threads, opens menus). These are candidates for API seeding.
|
||||
3. **Apply Pattern 1**. Replace the marked UI setup with helpers from the table above. If a helper is missing, add it under `utils/` and export it from `utils/index.ts`.
|
||||
4. **Apply Pattern 2** — only if the suite is `.serial` and the preconditions hold. Move context, page, navigation to `beforeAll`. Close page and context in `afterAll`. Ensure every test returns the page to the home position.
|
||||
5. **Check coverage when consolidating**. If you merge tests (e.g. four formatting tests into one), confirm the merged test still asserts every behavior the originals covered. List the merges in the PR body.
|
||||
6. **Compare**. Run three times again, record the new median. If the improvement is under 30%, revisit — the coupling may not be worth it.
|
||||
7. **PR body must include**: before/after timings, the list of UI setups moved to API, and an explicit reason if you chose not to apply one of the patterns.
|
||||
|
||||
## Anti-patterns to flag in review
|
||||
|
||||
- `poHomeChannel.content.sendMessage(...)` used inside `beforeEach` or `beforeAll` — that is setup, should be `sendMessage(api, ...)`.
|
||||
- Opening meatball menus or modals purely to create a discussion, thread, or DM as a setup step.
|
||||
- `test.describe.serial` that builds a fresh context and navigates in `beforeEach` — both should be shared in `beforeAll`.
|
||||
- Any raw `page.goto(...)` in a spec — use the page object's `goto()` (see [Navigation](#navigation-goto-owns-the-wait)).
|
||||
- `goto()` + `navbar.openChat(name)` to reach a room the test already knows — use `poHomeChannel.gotoChannel(name)` (see Pattern 3). Searching a just-created channel races the search index and hangs until timeout.
|
||||
- New non-serial suites whose tests still each carry >3s of UI setup.
|
||||
- Inline `api.post('/im.create', …)` / `api.post('/chat.sendMessage', …)` in a spec instead of extending the helpers in `utils/`.
|
||||
120
apps/meteor/tests/e2e/abac-classification-banner.spec.ts
Normal file
120
apps/meteor/tests/e2e/abac-classification-banner.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
110
apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts
Normal file
110
apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
44
apps/meteor/tests/e2e/account-login.spec.ts
Normal file
44
apps/meteor/tests/e2e/account-login.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
66
apps/meteor/tests/e2e/account-manage-devices.spec.ts
Normal file
66
apps/meteor/tests/e2e/account-manage-devices.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
161
apps/meteor/tests/e2e/account-profile.spec.ts
Normal file
161
apps/meteor/tests/e2e/account-profile.spec.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
114
apps/meteor/tests/e2e/account-security.spec.ts
Normal file
114
apps/meteor/tests/e2e/account-security.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
93
apps/meteor/tests/e2e/account-totp.spec.ts
Normal file
93
apps/meteor/tests/e2e/account-totp.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
118
apps/meteor/tests/e2e/admin-device-management.spec.ts
Normal file
118
apps/meteor/tests/e2e/admin-device-management.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
94
apps/meteor/tests/e2e/admin-room.spec.ts
Normal file
94
apps/meteor/tests/e2e/admin-room.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
157
apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts
Normal file
157
apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
66
apps/meteor/tests/e2e/admin-users-role-management.spec.ts
Normal file
66
apps/meteor/tests/e2e/admin-users-role-management.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
80
apps/meteor/tests/e2e/admin-users-status-management.spec.ts
Normal file
80
apps/meteor/tests/e2e/admin-users-status-management.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
66
apps/meteor/tests/e2e/admin-users.spec.ts
Normal file
66
apps/meteor/tests/e2e/admin-users.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
101
apps/meteor/tests/e2e/administration-settings.spec.ts
Normal file
101
apps/meteor/tests/e2e/administration-settings.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
469
apps/meteor/tests/e2e/administration.spec.ts
Normal file
469
apps/meteor/tests/e2e/administration.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
46
apps/meteor/tests/e2e/anonymous-user.spec.ts
Normal file
46
apps/meteor/tests/e2e/anonymous-user.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
72
apps/meteor/tests/e2e/apps/app-modal-interaction.spec.ts
Normal file
72
apps/meteor/tests/e2e/apps/app-modal-interaction.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
36
apps/meteor/tests/e2e/apps/apps-contextualbar.spec.ts
Normal file
36
apps/meteor/tests/e2e/apps/apps-contextualbar.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
44
apps/meteor/tests/e2e/apps/apps-modal.spec.ts
Normal file
44
apps/meteor/tests/e2e/apps/apps-modal.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
116
apps/meteor/tests/e2e/apps/private-apps-upload.spec.ts
Normal file
116
apps/meteor/tests/e2e/apps/private-apps-upload.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
288
apps/meteor/tests/e2e/apps/uikit-interactions.spec.ts
Normal file
288
apps/meteor/tests/e2e/apps/uikit-interactions.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
60
apps/meteor/tests/e2e/auto-translate.spec.ts
Normal file
60
apps/meteor/tests/e2e/auto-translate.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
98
apps/meteor/tests/e2e/avatar-settings.ts
Normal file
98
apps/meteor/tests/e2e/avatar-settings.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
71
apps/meteor/tests/e2e/calendar.spec.ts
Normal file
71
apps/meteor/tests/e2e/calendar.spec.ts
Normal 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 };
|
||||
}
|
||||
355
apps/meteor/tests/e2e/channel-management.spec.ts
Normal file
355
apps/meteor/tests/e2e/channel-management.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
24
apps/meteor/tests/e2e/config/constants.ts
Normal file
24
apps/meteor/tests/e2e/config/constants.ts
Normal 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;
|
||||
11
apps/meteor/tests/e2e/config/global-setup.ts
Normal file
11
apps/meteor/tests/e2e/config/global-setup.ts
Normal 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();
|
||||
}
|
||||
7
apps/meteor/tests/e2e/config/global.d.ts
vendored
Normal file
7
apps/meteor/tests/e2e/config/global.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare namespace PlaywrightTest {
|
||||
interface Matchers<R> {
|
||||
hasAttribute(a: string): Promise<R>;
|
||||
toBeInvalid(): Promise<R>;
|
||||
toBeBusy(): Promise<R>;
|
||||
}
|
||||
}
|
||||
36
apps/meteor/tests/e2e/containers/saml/Dockerfile
Normal file
36
apps/meteor/tests/e2e/containers/saml/Dockerfile
Normal 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
|
||||
21
apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt
Normal file
21
apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt
Normal 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-----
|
||||
@@ -0,0 +1,9 @@
|
||||
Listen 8080
|
||||
|
||||
<IfModule ssl_module>
|
||||
Listen 8443
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 8443
|
||||
</IfModule>
|
||||
@@ -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-----
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -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'),
|
||||
);
|
||||
@@ -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',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
7
apps/meteor/tests/e2e/containers/saml/docker-compose.yml
Normal file
7
apps/meteor/tests/e2e/containers/saml/docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
testsamlidp_idp:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8443:8443"
|
||||
33
apps/meteor/tests/e2e/create-channel.spec.ts
Normal file
33
apps/meteor/tests/e2e/create-channel.spec.ts
Normal 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}`);
|
||||
});
|
||||
});
|
||||
20
apps/meteor/tests/e2e/create-direct.spec.ts
Normal file
20
apps/meteor/tests/e2e/create-direct.spec.ts
Normal 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\/.*/);
|
||||
});
|
||||
});
|
||||
26
apps/meteor/tests/e2e/create-discussion.spec.ts
Normal file
26
apps/meteor/tests/e2e/create-discussion.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
104
apps/meteor/tests/e2e/delete-account.spec.ts
Normal file
104
apps/meteor/tests/e2e/delete-account.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
53
apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts
Normal file
53
apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
100
apps/meteor/tests/e2e/e2e-encryption/e2ee-legacy-format.spec.ts
Normal file
100
apps/meteor/tests/e2e/e2e-encryption/e2ee-legacy-format.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
90
apps/meteor/tests/e2e/e2e-encryption/e2ee-pdf-export.spec.ts
Normal file
90
apps/meteor/tests/e2e/e2e-encryption/e2ee-pdf-export.spec.ts
Normal 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$/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
apps/meteor/tests/e2e/e2e-encryption/resetOwnE2EKey.ts
Normal file
28
apps/meteor/tests/e2e/e2e-encryption/resetOwnE2EKey.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
19
apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts
Normal file
19
apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts
Normal 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;
|
||||
};
|
||||
52
apps/meteor/tests/e2e/email-inboxes.spec.ts
Normal file
52
apps/meteor/tests/e2e/email-inboxes.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
137
apps/meteor/tests/e2e/embedded-layout.spec.ts
Normal file
137
apps/meteor/tests/e2e/embedded-layout.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
106
apps/meteor/tests/e2e/emojis.spec.ts
Normal file
106
apps/meteor/tests/e2e/emojis.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
106
apps/meteor/tests/e2e/enforce-2FA.spec.ts
Normal file
106
apps/meteor/tests/e2e/enforce-2FA.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
164
apps/meteor/tests/e2e/export-messages.spec.ts
Normal file
164
apps/meteor/tests/e2e/export-messages.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
620
apps/meteor/tests/e2e/feature-preview.spec.ts
Normal file
620
apps/meteor/tests/e2e/feature-preview.spec.ts
Normal 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 > 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
12
apps/meteor/tests/e2e/federation/README.md
Normal file
12
apps/meteor/tests/e2e/federation/README.md
Normal 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.
|
||||
20
apps/meteor/tests/e2e/federation/config/constants.ts
Normal file
20
apps/meteor/tests/e2e/federation/config/constants.ts
Normal 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 ?? '',
|
||||
};
|
||||
20
apps/meteor/tests/e2e/federation/config/global-setup.ts
Normal file
20
apps/meteor/tests/e2e/federation/config/global-setup.ts
Normal 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(', ')}`);
|
||||
}
|
||||
}
|
||||
BIN
apps/meteor/tests/e2e/federation/files/test_pdf_file.pdf
Normal file
BIN
apps/meteor/tests/e2e/federation/files/test_pdf_file.pdf
Normal file
Binary file not shown.
@@ -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 });
|
||||
}
|
||||
}
|
||||
78
apps/meteor/tests/e2e/federation/page-objects/admin.ts
Normal file
78
apps/meteor/tests/e2e/federation/page-objects/admin.ts
Normal 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"]');
|
||||
}
|
||||
}
|
||||
94
apps/meteor/tests/e2e/federation/page-objects/channel.ts
Normal file
94
apps/meteor/tests/e2e/federation/page-objects/channel.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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")');
|
||||
}
|
||||
}
|
||||
@@ -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}")]`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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"]');
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
81
apps/meteor/tests/e2e/federation/tests/admin/rooms.spec.ts
Normal file
81
apps/meteor/tests/e2e/federation/tests/admin/rooms.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
128
apps/meteor/tests/e2e/federation/tests/admin/users.spec.ts
Normal file
128
apps/meteor/tests/e2e/federation/tests/admin/users.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
88
apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts
Normal file
88
apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
876
apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts
Normal file
876
apps/meteor/tests/e2e/federation/tests/channel/dm.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1252
apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts
Normal file
1252
apps/meteor/tests/e2e/federation/tests/channel/private.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
1360
apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts
Normal file
1360
apps/meteor/tests/e2e/federation/tests/channel/public.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
1361
apps/meteor/tests/e2e/federation/tests/messaging/dm.spec.ts
Normal file
1361
apps/meteor/tests/e2e/federation/tests/messaging/dm.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
1100
apps/meteor/tests/e2e/federation/tests/messaging/private.spec.ts
Normal file
1100
apps/meteor/tests/e2e/federation/tests/messaging/private.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
1094
apps/meteor/tests/e2e/federation/tests/messaging/public.spec.ts
Normal file
1094
apps/meteor/tests/e2e/federation/tests/messaging/public.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
367
apps/meteor/tests/e2e/federation/tests/messaging/threads.spec.ts
Normal file
367
apps/meteor/tests/e2e/federation/tests/messaging/threads.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
130
apps/meteor/tests/e2e/federation/tests/user-account/user.spec.ts
Normal file
130
apps/meteor/tests/e2e/federation/tests/user-account/user.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
28
apps/meteor/tests/e2e/federation/utils/auth.ts
Normal file
28
apps/meteor/tests/e2e/federation/utils/auth.ts
Normal 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` });
|
||||
}
|
||||
};
|
||||
85
apps/meteor/tests/e2e/federation/utils/channel.ts
Normal file
85
apps/meteor/tests/e2e/federation/utils/channel.ts
Normal 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 });
|
||||
};
|
||||
4
apps/meteor/tests/e2e/federation/utils/format.ts
Normal file
4
apps/meteor/tests/e2e/federation/utils/format.ts
Normal 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)}`;
|
||||
17
apps/meteor/tests/e2e/federation/utils/register-user.ts
Normal file
17
apps/meteor/tests/e2e/federation/utils/register-user.ts
Normal 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;
|
||||
}
|
||||
84
apps/meteor/tests/e2e/federation/utils/test.ts
Normal file
84
apps/meteor/tests/e2e/federation/utils/test.ts
Normal 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' });
|
||||
};
|
||||
248
apps/meteor/tests/e2e/file-upload.spec.ts
Normal file
248
apps/meteor/tests/e2e/file-upload.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
54
apps/meteor/tests/e2e/files-management.spec.ts
Normal file
54
apps/meteor/tests/e2e/files-management.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
21
apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts
Normal file
21
apps/meteor/tests/e2e/fixtures/addCustomOAuth.ts
Normal 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 });
|
||||
}
|
||||
53
apps/meteor/tests/e2e/fixtures/collections/users.ts
Normal file
53
apps/meteor/tests/e2e/fixtures/collections/users.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
19
apps/meteor/tests/e2e/fixtures/createAuxContext.ts
Normal file
19
apps/meteor/tests/e2e/fixtures/createAuxContext.ts
Normal 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 };
|
||||
};
|
||||
1
apps/meteor/tests/e2e/fixtures/files/another_file.txt
Normal file
1
apps/meteor/tests/e2e/fixtures/files/another_file.txt
Normal file
@@ -0,0 +1 @@
|
||||
another_file
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user