Merge branch 'livekit' into valere/async_error_show_boundary
This commit is contained in:
33
.github/workflows/playwright.yml
vendored
Normal file
33
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches: [livekit, full-mesh]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
cache: "yarn"
|
||||||
|
node-version-file: ".node-version"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Run backend components
|
||||||
|
run: |
|
||||||
|
docker compose -f playwright-backend-docker-compose.yml up -d
|
||||||
|
docker ps
|
||||||
|
- name: Copy config file
|
||||||
|
run: cp config/config.devenv.json public/config.json
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: yarn playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 3
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,3 +10,9 @@ public/config.json
|
|||||||
backend/synapse_tmp/*
|
backend/synapse_tmp/*
|
||||||
/coverage
|
/coverage
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -20,7 +20,7 @@ utilizes
|
|||||||
**[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)**
|
**[MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)**
|
||||||
with **[LiveKit](https://livekit.io/)** as its backend.
|
with **[LiveKit](https://livekit.io/)** as its backend.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
You can find the latest development version continuously deployed to
|
You can find the latest development version continuously deployed to
|
||||||
[call.element.dev](https://call.element.dev/).
|
[call.element.dev](https://call.element.dev/).
|
||||||
@@ -189,6 +189,64 @@ yarn backend
|
|||||||
# podman-compose -f dev-backend-docker-compose.yml up
|
# podman-compose -f dev-backend-docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Playwright tests
|
||||||
|
|
||||||
|
Our Playwright tests run automatically as part of our CI along with our other tests,
|
||||||
|
on every pull request.
|
||||||
|
|
||||||
|
You may need to follow instructions to set up your development environment for running
|
||||||
|
Playwright by following <https://playwright.dev/docs/browsers#install-browsers> and
|
||||||
|
<https://playwright.dev/docs/browsers#install-system-dependencies>.
|
||||||
|
|
||||||
|
However the Playwright tests are run, an element-call instance must be running on
|
||||||
|
https://localhost:3000 (this is configured in `playwright.config.ts`) - this is what will
|
||||||
|
be tested.
|
||||||
|
|
||||||
|
The local backend environment should be running for the test to work:
|
||||||
|
`yarn backend`
|
||||||
|
|
||||||
|
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn run test:playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
This will run the Playwright tests once, non-interactively.
|
||||||
|
|
||||||
|
There is a more user-friendly way to run the tests in interactive mode:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn run test:playwright:open
|
||||||
|
```
|
||||||
|
|
||||||
|
The easiest way to develop new test is to use the codegen feature of Playwright:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx playwright codegen
|
||||||
|
```
|
||||||
|
|
||||||
|
This will record your action and write the test code for you. Use the tool bar to test visibility, text content,
|
||||||
|
clicking.
|
||||||
|
|
||||||
|
##### Investigate a failed test from the CI
|
||||||
|
|
||||||
|
In the failed action page, click on the failed job, then scroll down to the `upload-artifact` step.
|
||||||
|
You will find a link to download the zip report, as per:
|
||||||
|
|
||||||
|
```
|
||||||
|
Artifact playwright-report has been successfully uploaded! Final size is 1360358 bytes. Artifact ID is 2746265841
|
||||||
|
Artifact download URL: https://github.com/element-hq/element-call/actions/runs/13837660687/artifacts/2746265841
|
||||||
|
```
|
||||||
|
|
||||||
|
Unzip the report then use this command to open the report in your browser:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx playwright show-report ~/Downloads/playwright-report/
|
||||||
|
```
|
||||||
|
|
||||||
|
Under the failed test there is a small icon looking like "3 columns" (next to test name file name),
|
||||||
|
click on it to see the live screenshots/console output.
|
||||||
|
|
||||||
### Test Coverage
|
### Test Coverage
|
||||||
|
|
||||||
<img src="https://codecov.io/github/element-hq/element-call/graphs/tree.svg?token=O6CFVKK6I1"></img>
|
<img src="https://codecov.io/github/element-hq/element-call/graphs/tree.svg?token=O6CFVKK6I1"></img>
|
||||||
|
|||||||
67
backend/playwright_homeserver.yaml
Normal file
67
backend/playwright_homeserver.yaml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
server_name: "synapse.localhost"
|
||||||
|
public_baseurl: http://synapse.localhost:8008/
|
||||||
|
|
||||||
|
pid_file: /data/homeserver.pid
|
||||||
|
|
||||||
|
listeners:
|
||||||
|
- port: 8008
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
resources:
|
||||||
|
- names: [client, federation, openid]
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
database:
|
||||||
|
name: sqlite3
|
||||||
|
args:
|
||||||
|
database: /data/homeserver.db
|
||||||
|
|
||||||
|
media_store_path: /data/media_store
|
||||||
|
signing_key_path: "/data/SERVERNAME.signing.key"
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: "matrix.org"
|
||||||
|
|
||||||
|
experimental_features:
|
||||||
|
# MSC3266: Room summary API. Used for knocking over federation
|
||||||
|
msc3266_enabled: true
|
||||||
|
# MSC4222 needed for syncv2 state_after. This allow clients to
|
||||||
|
# correctly track the state of the room.
|
||||||
|
msc4222_enabled: true
|
||||||
|
|
||||||
|
# The maximum allowed duration by which sent events can be delayed, as
|
||||||
|
# per MSC4140. Must be a positive value if set. Defaults to no
|
||||||
|
# duration (null), which disallows sending delayed events.
|
||||||
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
|
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||||
|
#
|
||||||
|
# Each ratelimiting configuration is made of two parameters:
|
||||||
|
# - per_second: number of requests a client can send per second.
|
||||||
|
# - burst_count: number of requests a client can send before being throttled.
|
||||||
|
|
||||||
|
rc_message:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_login:
|
||||||
|
address:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
account:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
failed_attempts:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
rc_registration:
|
||||||
|
per_second: 10000
|
||||||
|
burst_count: 10000
|
||||||
|
|
||||||
|
# Required for Element Call in Single Page Mode due to on-the-fly user registration
|
||||||
|
enable_registration: true
|
||||||
|
enable_registration_without_verification: true
|
||||||
|
|
||||||
|
report_stats: false
|
||||||
|
serve_server_wellknown: true
|
||||||
@@ -46,10 +46,15 @@ experimental_features:
|
|||||||
max_event_delay_duration: 24h
|
max_event_delay_duration: 24h
|
||||||
|
|
||||||
rc_message:
|
rc_message:
|
||||||
# This needs to match at least the heart-beat frequency plus a bit of headroom
|
# This needs to match at least e2ee key sharing frequency plus a bit of headroom
|
||||||
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
# Note key sharing events are bursty
|
||||||
per_second: 0.5
|
per_second: 0.5
|
||||||
burst_count: 30
|
burst_count: 30
|
||||||
|
# This needs to match at least the heart-beat frequency plus a bit of headroom
|
||||||
|
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
|
||||||
|
rc_delayed_event_mgmt:
|
||||||
|
per_second: 1
|
||||||
|
burst_count: 20
|
||||||
```
|
```
|
||||||
|
|
||||||
### MatrixRTC Backend
|
### MatrixRTC Backend
|
||||||
@@ -84,7 +89,7 @@ to implement
|
|||||||
{
|
{
|
||||||
"type": "another_foci",
|
"type": "another_foci",
|
||||||
"props_for_another_foci": "val"
|
"props_for_another_foci": "val"
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"i18n:check": "i18next --fail-on-warnings --fail-on-update",
|
"i18n:check": "i18next --fail-on-warnings --fail-on-update",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"backend": "docker-compose -f dev-backend-docker-compose.yml up"
|
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
|
||||||
|
"test:playwright": "playwright test",
|
||||||
|
"test:playwright:open": "yarn test:playwright --ui"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
||||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.25.1",
|
"@opentelemetry/semantic-conventions": "^1.25.1",
|
||||||
|
"@playwright/test": "^1.51.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
"@radix-ui/react-visually-hidden": "^1.0.3",
|
"@radix-ui/react-visually-hidden": "^1.0.3",
|
||||||
|
|||||||
86
playwright-backend-docker-compose.yml
Normal file
86
playwright-backend-docker-compose.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
networks:
|
||||||
|
ecbackend:
|
||||||
|
|
||||||
|
services:
|
||||||
|
auth-service:
|
||||||
|
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
|
||||||
|
hostname: auth-server
|
||||||
|
environment:
|
||||||
|
- LK_JWT_PORT=8080
|
||||||
|
- LIVEKIT_URL=ws://localhost:7880
|
||||||
|
- LIVEKIT_KEY=devkey
|
||||||
|
- LIVEKIT_SECRET=secret
|
||||||
|
# If the configured homeserver runs on localhost, it'll probably be using
|
||||||
|
# a self-signed certificate
|
||||||
|
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- 8009:8080
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
|
livekit:
|
||||||
|
image: livekit/livekit-server:latest
|
||||||
|
command: --dev --config /etc/livekit.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
# The SFU seems to work far more reliably when we let it share the host
|
||||||
|
# network rather than opening specific ports (but why?? we're not missing
|
||||||
|
# any…)
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- 7880:7880/tcp
|
||||||
|
- 7881:7881/tcp
|
||||||
|
- 7882:7882/tcp
|
||||||
|
- 50100-50200:50100-50200/udp
|
||||||
|
volumes:
|
||||||
|
- ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6-alpine
|
||||||
|
command: redis-server /etc/redis.conf
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- 6379:6379
|
||||||
|
volumes:
|
||||||
|
- ./backend/redis.conf:/etc/redis.conf:Z
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
|
synapse:
|
||||||
|
hostname: homeserver
|
||||||
|
image: docker.io/matrixdotorg/synapse:latest
|
||||||
|
environment:
|
||||||
|
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
|
||||||
|
# Needed for rootless podman-compose such that the uid/gid mapping does
|
||||||
|
# fit local user uid. If the container runs as root (uid 0) it is fine as
|
||||||
|
# it actually maps to your non-root user on the host (e.g. 1000).
|
||||||
|
# Otherwise uid mapping will not match your non-root user.
|
||||||
|
- UID=0
|
||||||
|
- GID=0
|
||||||
|
volumes:
|
||||||
|
- ./backend/synapse_tmp:/data:Z
|
||||||
|
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost"
|
||||||
|
hostname: synapse.localhost
|
||||||
|
image: nginx:latest
|
||||||
|
volumes:
|
||||||
|
- ./backend/tls_localhost_nginx.conf:/etc/nginx/conf.d/default.conf:Z
|
||||||
|
- ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z
|
||||||
|
- ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z
|
||||||
|
ports:
|
||||||
|
# HOST_PORT:CONTAINER_PORT
|
||||||
|
- "8008:80"
|
||||||
|
- "4443:443"
|
||||||
|
depends_on:
|
||||||
|
- synapse
|
||||||
|
networks:
|
||||||
|
- ecbackend
|
||||||
81
playwright.config.ts
Normal file
81
playwright.config.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./playwright",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "https://localhost:3000",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
permissions: [
|
||||||
|
"clipboard-write",
|
||||||
|
"clipboard-read",
|
||||||
|
"microphone",
|
||||||
|
"camera",
|
||||||
|
],
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
launchOptions: {
|
||||||
|
args: [
|
||||||
|
"--use-fake-ui-for-media-stream",
|
||||||
|
"--use-fake-device-for-media-stream",
|
||||||
|
"--mute-audio",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Firefox"],
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
launchOptions: {
|
||||||
|
firefoxUserPrefs: {
|
||||||
|
"permissions.default.microphone": 1,
|
||||||
|
"permissions.default.camera": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// No safari for now, until I find a solution to fix `Not allowed to request resource` due to calling
|
||||||
|
// clear http to the homeserver
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "yarn dev",
|
||||||
|
url: "https://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
131
playwright/access.spec.ts
Normal file
131
playwright/access.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("Sign up a new account, then login, then logout", async ({ browser }) => {
|
||||||
|
const userId = `test_user-id_${Date.now()}`;
|
||||||
|
|
||||||
|
const newUserContext = await browser.newContext();
|
||||||
|
const newUserPage = await newUserContext.newPage();
|
||||||
|
await newUserPage.goto("/");
|
||||||
|
|
||||||
|
await expect(newUserPage.getByTestId("home_register")).toBeVisible();
|
||||||
|
await newUserPage.getByTestId("home_register").click();
|
||||||
|
|
||||||
|
await newUserPage.getByTestId("register_username").click();
|
||||||
|
await newUserPage.getByTestId("register_username").fill(userId);
|
||||||
|
|
||||||
|
await newUserPage.getByTestId("register_password").click();
|
||||||
|
await newUserPage.getByTestId("register_password").fill("password1!");
|
||||||
|
await newUserPage.getByTestId("register_confirm_password").click();
|
||||||
|
await newUserPage.getByTestId("register_confirm_password").fill("password1!");
|
||||||
|
await newUserPage.getByTestId("register_register").click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
newUserPage.getByRole("heading", { name: "Start new call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Now use a new page to login this account
|
||||||
|
const returningUserContext = await browser.newContext();
|
||||||
|
const returningUserPage = await returningUserContext.newPage();
|
||||||
|
await returningUserPage.goto("/");
|
||||||
|
|
||||||
|
await expect(returningUserPage.getByTestId("home_login")).toBeVisible();
|
||||||
|
await returningUserPage.getByTestId("home_login").click();
|
||||||
|
await returningUserPage.getByTestId("login_username").click();
|
||||||
|
await returningUserPage.getByTestId("login_username").fill(userId);
|
||||||
|
await returningUserPage.getByTestId("login_password").click();
|
||||||
|
await returningUserPage.getByTestId("login_password").fill("password1!");
|
||||||
|
await returningUserPage.getByTestId("login_login").click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
returningUserPage.getByRole("heading", { name: "Start new call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// logout
|
||||||
|
await returningUserPage.getByTestId("usermenu_open").click();
|
||||||
|
await returningUserPage.locator('[data-test-id="usermenu_logout"]').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
returningUserPage.getByRole("link", { name: "Log In" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(returningUserPage.getByTestId("home_login")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("As a guest, create a call, share link and other join", async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// Use reduce motion to disable animations that are making the tests a bit flaky
|
||||||
|
const creatorContext = await browser.newContext({ reducedMotion: "reduce" });
|
||||||
|
const creatorPage = await creatorContext.newPage();
|
||||||
|
|
||||||
|
await creatorPage.goto("/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ARRANGE: The first user creates a call as guest, join it, then click the invite button to copy the invite link
|
||||||
|
// ========
|
||||||
|
await creatorPage.getByTestId("home_callName").click();
|
||||||
|
await creatorPage.getByTestId("home_callName").fill("Welcome");
|
||||||
|
await creatorPage.getByTestId("home_displayName").click();
|
||||||
|
await creatorPage.getByTestId("home_displayName").fill("Inviter");
|
||||||
|
await creatorPage.getByTestId("home_go").click();
|
||||||
|
await expect(creatorPage.locator("video")).toBeVisible();
|
||||||
|
|
||||||
|
// join
|
||||||
|
await creatorPage.getByTestId("lobby_joinCall").click();
|
||||||
|
// Spotlight mode to make checking the test visually clearer
|
||||||
|
await creatorPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// Get the invite link
|
||||||
|
await creatorPage.getByRole("button", { name: "Invite" }).click();
|
||||||
|
await expect(
|
||||||
|
creatorPage.getByRole("heading", { name: "Invite to this call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(creatorPage.getByRole("img", { name: "QR Code" })).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await creatorPage.getByTestId("modal_inviteLink").click();
|
||||||
|
|
||||||
|
let inviteLink = (await creatorPage.evaluate(
|
||||||
|
"navigator.clipboard.readText()",
|
||||||
|
)) as string;
|
||||||
|
expect(inviteLink).toContain("room/#/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ACT: The other user use the invite link to join the call as a guest
|
||||||
|
// ========
|
||||||
|
const guestInviteeContext = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
|
});
|
||||||
|
const guestPage = await guestInviteeContext.newPage();
|
||||||
|
|
||||||
|
await guestPage.goto(inviteLink);
|
||||||
|
await guestPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||||
|
await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||||
|
await guestPage.getByTestId("joincall_joincall").click();
|
||||||
|
await guestPage.getByTestId("lobby_joinCall").click();
|
||||||
|
await guestPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ASSERT: check that there are two members in the call
|
||||||
|
// ========
|
||||||
|
|
||||||
|
// There should be two participants now
|
||||||
|
await expect(
|
||||||
|
guestPage.getByTestId("roomHeader_participants_count"),
|
||||||
|
).toContainText("2");
|
||||||
|
expect(await guestPage.getByTestId("videoTile").count()).toBe(2);
|
||||||
|
|
||||||
|
// Same in creator page
|
||||||
|
await expect(
|
||||||
|
creatorPage.getByTestId("roomHeader_participants_count"),
|
||||||
|
).toContainText("2");
|
||||||
|
expect(await creatorPage.getByTestId("videoTile").count()).toBe(2);
|
||||||
|
|
||||||
|
// XXX check the display names on the video tiles
|
||||||
|
});
|
||||||
55
playwright/create-call.spec.ts
Normal file
55
playwright/create-call.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("Start a new call then leave and show the feedback screen", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.getByTestId("home_callName").click();
|
||||||
|
await page.getByTestId("home_callName").fill("HelloCall");
|
||||||
|
await page.getByTestId("home_displayName").click();
|
||||||
|
await page.getByTestId("home_displayName").fill("John Doe");
|
||||||
|
await page.getByTestId("home_go").click();
|
||||||
|
|
||||||
|
await expect(page.locator("video")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||||
|
|
||||||
|
// Check the button toolbar
|
||||||
|
// await expect(page.getByRole('button', { name: 'Mute microphone' })).toBeVisible();
|
||||||
|
// await expect(page.getByRole('button', { name: 'Stop video' })).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("button", { name: "End call" })).toBeVisible();
|
||||||
|
|
||||||
|
// Join the call
|
||||||
|
await page.getByTestId("lobby_joinCall").click();
|
||||||
|
|
||||||
|
// Ensure that the call is connected
|
||||||
|
await page
|
||||||
|
.locator("div")
|
||||||
|
.filter({ hasText: /^HelloCall$/ })
|
||||||
|
.click();
|
||||||
|
// Check the number of participants
|
||||||
|
await expect(page.locator("div").filter({ hasText: /^1$/ })).toBeVisible();
|
||||||
|
// The tooltip with the name should be visible
|
||||||
|
await expect(page.getByTestId("name_tag")).toContainText("John Doe");
|
||||||
|
|
||||||
|
// leave the call
|
||||||
|
await page.getByTestId("incall_leave").click();
|
||||||
|
await expect(page.getByRole("heading")).toContainText(
|
||||||
|
"John Doe, your call has ended. How did it go?",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("main")).toContainText(
|
||||||
|
"Why not finish by setting up a password to keep your account?",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { name: "Not now, return to home screen" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
30
playwright/landing.spec.ts
Normal file
30
playwright/landing.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test("has title", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/Element Call/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Landing page", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// There should be a login button in the header
|
||||||
|
await expect(page.getByRole("link", { name: "Log In" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Start new call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("home_callName")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("home_displayName")).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("home_go")).toBeVisible();
|
||||||
|
});
|
||||||
@@ -161,7 +161,12 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
|||||||
height={20}
|
height={20}
|
||||||
aria-label={t("header_participants_label")}
|
aria-label={t("header_participants_label")}
|
||||||
/>
|
/>
|
||||||
<Text as="span" size="sm" weight="medium">
|
<Text
|
||||||
|
as="span"
|
||||||
|
size="sm"
|
||||||
|
weight="medium"
|
||||||
|
data-testid="roomHeader_participants_count"
|
||||||
|
>
|
||||||
{t("participant_count", { count: participantCount ?? 0 })}
|
{t("participant_count", { count: participantCount ?? 0 })}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default defineConfig((configEnv) =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: ["src/vitest.setup.ts"],
|
setupFiles: ["src/vitest.setup.ts"],
|
||||||
|
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ["html", "json"],
|
reporter: ["html", "json"],
|
||||||
include: ["src/"],
|
include: ["src/"],
|
||||||
@@ -21,6 +22,7 @@ export default defineConfig((configEnv) =>
|
|||||||
"src/utils/test.ts",
|
"src/utils/test.ts",
|
||||||
"src/utils/test-viewmodel.ts",
|
"src/utils/test-viewmodel.ts",
|
||||||
"src/utils/test-fixtures.ts",
|
"src/utils/test-fixtures.ts",
|
||||||
|
"playwright/**",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@@ -2137,6 +2137,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||||
|
|
||||||
|
"@playwright/test@^1.51.0":
|
||||||
|
version "1.51.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.0.tgz#8d5c8400b465a0bfdbcf993e390ceecb903ea6d2"
|
||||||
|
integrity sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==
|
||||||
|
dependencies:
|
||||||
|
playwright "1.51.0"
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||||
@@ -6995,6 +7002,20 @@ picomatch@^4.0.1, picomatch@^4.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
|
||||||
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
|
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
|
||||||
|
|
||||||
|
playwright-core@1.51.0:
|
||||||
|
version "1.51.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.0.tgz#bb23ea6bb6298242d088ae5e966ffcf8dc9827e8"
|
||||||
|
integrity sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==
|
||||||
|
|
||||||
|
playwright@1.51.0:
|
||||||
|
version "1.51.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.0.tgz#9ba154497ba62bc6dc199c58ee19295eb35a4707"
|
||||||
|
integrity sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==
|
||||||
|
dependencies:
|
||||||
|
playwright-core "1.51.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "2.3.2"
|
||||||
|
|
||||||
pluralize@^8.0.0:
|
pluralize@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||||
|
|||||||
Reference in New Issue
Block a user