mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-21 02:42:25 +00:00
Merge branch 'livekit' into toger5/dont-trap-in-invalid-config
This commit is contained in:
@@ -99,6 +99,13 @@ module.exports = {
|
||||
"jsdoc/require-param-description": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["playwright/**"],
|
||||
rules: {
|
||||
// Playwright as a `use` function that has nothing to do with React hooks.
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -3,7 +3,7 @@ networks:
|
||||
|
||||
services:
|
||||
auth-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:pr_139
|
||||
image: ghcr.io/element-hq/lk-jwt-service:sha-f8ddd00
|
||||
pull_policy: always
|
||||
hostname: auth-server
|
||||
environment:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
- ecbackend
|
||||
|
||||
auth-service-1:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:pr_139
|
||||
image: ghcr.io/element-hq/lk-jwt-service:sha-f8ddd00
|
||||
pull_policy: always
|
||||
hostname: auth-server-1
|
||||
environment:
|
||||
|
||||
@@ -6,15 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Browser,
|
||||
type Page,
|
||||
test,
|
||||
expect,
|
||||
type JSHandle,
|
||||
} from "@playwright/test";
|
||||
import { type Page, test, expect, type JSHandle } from "@playwright/test";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk";
|
||||
import { TestHelpers } from "../widget/test-helpers.ts";
|
||||
|
||||
export type UserBaseFixture = {
|
||||
mxId: string;
|
||||
@@ -31,10 +26,11 @@ export type BaseWidgetSetup = {
|
||||
export interface MyFixtures {
|
||||
asWidget: BaseWidgetSetup;
|
||||
callType: "room" | "dm";
|
||||
addUser: (
|
||||
username: string /**, homeserver: string*/,
|
||||
) => Promise<UserBaseFixture>;
|
||||
}
|
||||
|
||||
const PASSWORD = "foobarbaz1!";
|
||||
|
||||
// Minimal config.json for the local element-web instance
|
||||
const CONFIG_JSON = {
|
||||
default_server_config: {
|
||||
@@ -68,85 +64,6 @@ const CONFIG_JSON = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
|
||||
*/
|
||||
const setDevToolElementCallDevUrl = process.env.USE_DOCKER
|
||||
? async (page: Page): Promise<void> => {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"http://localhost:8080/room",
|
||||
);
|
||||
});
|
||||
}
|
||||
: async (page: Page): Promise<void> => {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"https://localhost:3000/room",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a new user and returns page, clientHandle and mxId.
|
||||
*/
|
||||
async function registerUser(
|
||||
browser: Browser,
|
||||
username: string,
|
||||
): Promise<{ page: Page; clientHandle: JSHandle<MatrixClient>; mxId: string }> {
|
||||
const userContext = await browser.newContext({
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const page = await userContext.newPage();
|
||||
await page.goto("http://localhost:8081/#/welcome");
|
||||
await page.getByRole("link", { name: "Create Account" }).click();
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page
|
||||
.getByRole("textbox", { name: "Password", exact: true })
|
||||
.fill(PASSWORD);
|
||||
await page.getByRole("textbox", { name: "Confirm password" }).click();
|
||||
await page.getByRole("textbox", { name: "Confirm password" }).fill(PASSWORD);
|
||||
await page.getByRole("button", { name: "Register" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: `Welcome ${username}` }),
|
||||
).toBeVisible();
|
||||
|
||||
const browserUnsupportedToast = page
|
||||
.getByText("Element does not support this browser")
|
||||
.locator("..")
|
||||
.locator("..");
|
||||
|
||||
// Dismiss incompatible browser toast
|
||||
const dismissButton = browserUnsupportedToast.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
});
|
||||
try {
|
||||
await expect(dismissButton).toBeVisible({ timeout: 700 });
|
||||
await dismissButton.click();
|
||||
} catch {
|
||||
// dismissButton not visible, continue as normal
|
||||
}
|
||||
|
||||
await setDevToolElementCallDevUrl(page);
|
||||
|
||||
const clientHandle = await page.evaluateHandle(() =>
|
||||
window.mxMatrixClientPeg.get(),
|
||||
);
|
||||
const mxId = (await clientHandle.evaluate(
|
||||
(cli: MatrixClient) => cli.getUserId(),
|
||||
clientHandle,
|
||||
))!;
|
||||
|
||||
return { page, clientHandle, mxId };
|
||||
}
|
||||
|
||||
export const widgetTest = test.extend<MyFixtures>({
|
||||
// allow per-test override: `widgetTest.use({ callType: "dm" })`
|
||||
callType: ["room", { option: true }],
|
||||
@@ -163,25 +80,16 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
page: ewPage1,
|
||||
clientHandle: brooksClientHandle,
|
||||
mxId: brooksMxId,
|
||||
} = await registerUser(browser, brooksDisplayName);
|
||||
} = await TestHelpers.registerUser(browser, brooksDisplayName);
|
||||
const {
|
||||
page: ewPage2,
|
||||
clientHandle: whistlerClientHandle,
|
||||
mxId: whistlerMxId,
|
||||
} = await registerUser(browser, whistlerDisplayName);
|
||||
} = await TestHelpers.registerUser(browser, whistlerDisplayName);
|
||||
|
||||
// Invite the second user
|
||||
await ewPage1
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
|
||||
if (callType === "room") {
|
||||
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
|
||||
await ewPage1.getByRole("button", { name: "Create room" }).click();
|
||||
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
|
||||
await expect(ewPage1.getByText("Encryption enabled")).toBeVisible();
|
||||
await TestHelpers.createRoom("Welcome Room", ewPage1);
|
||||
|
||||
await ewPage1
|
||||
.getByRole("button", { name: "Invite to this room", exact: true })
|
||||
@@ -211,6 +119,11 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
.getByRole("heading", { name: "Welcome Room" }),
|
||||
).toBeVisible();
|
||||
} else if (callType === "dm") {
|
||||
await ewPage1
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
|
||||
await ewPage1.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Search" }).click();
|
||||
await ewPage1.getByRole("textbox", { name: "Search" }).fill(whistlerMxId);
|
||||
@@ -253,4 +166,28 @@ export const widgetTest = test.extend<MyFixtures>({
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide a way to add additional users within a test.
|
||||
* The returned user will be registered on the default homeserver, the name will be made unique by appending a timestamp.
|
||||
*/
|
||||
addUser: async ({ browser }, use) => {
|
||||
await use(
|
||||
async (
|
||||
username: string /**, homeserver?: string*/,
|
||||
): Promise<UserBaseFixture> => {
|
||||
const uniqueSuffix = Date.now();
|
||||
const { page, clientHandle, mxId } = await TestHelpers.registerUser(
|
||||
browser,
|
||||
`${username.toLowerCase()}_${uniqueSuffix}`,
|
||||
);
|
||||
return {
|
||||
mxId,
|
||||
displayName: username,
|
||||
page,
|
||||
clientHandle,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
160
playwright/widget/huddle-call.test.ts
Normal file
160
playwright/widget/huddle-call.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations 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";
|
||||
|
||||
import { widgetTest } from "../fixtures/widget-user.ts";
|
||||
import { TestHelpers } from "./test-helpers.ts";
|
||||
|
||||
widgetTest("Create and join a group call", async ({ addUser, browserName }) => {
|
||||
test.skip(
|
||||
browserName === "firefox",
|
||||
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
|
||||
);
|
||||
|
||||
test.slow(); // We are registering multiple users here, give it more time
|
||||
|
||||
const valere = await addUser("Valere");
|
||||
const timo = await addUser("Timo");
|
||||
const robin = await addUser("Robin");
|
||||
const halfshot = await addUser("Halfshot");
|
||||
const florian = await addUser("florian");
|
||||
|
||||
const roomName = "Group Call Room";
|
||||
await TestHelpers.createRoom(roomName, valere.page, [
|
||||
timo.mxId,
|
||||
robin.mxId,
|
||||
halfshot.mxId,
|
||||
florian.mxId,
|
||||
]);
|
||||
|
||||
for (const user of [timo, robin, halfshot, florian]) {
|
||||
// Accept the invite
|
||||
// This isn't super stable to get this as this super generic locator,
|
||||
// but it works for now.
|
||||
await expect(
|
||||
user.page.getByRole("option", { name: roomName }),
|
||||
).toBeVisible();
|
||||
await user.page.getByRole("option", { name: roomName }).click();
|
||||
await user.page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
await expect(
|
||||
user.page.getByRole("main").getByRole("heading", { name: roomName }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// Start the call as Valere
|
||||
await TestHelpers.startCallInCurrentRoom(valere.page, false);
|
||||
await expect(
|
||||
valere.page.locator('iframe[title="Element Call"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
valere.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall"),
|
||||
).toBeVisible();
|
||||
|
||||
await valere.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByTestId("lobby_joinCall")
|
||||
.click();
|
||||
|
||||
for (const user of [timo, robin, halfshot, florian]) {
|
||||
// THis is the header button that notifies about an ongoing call
|
||||
await expect(user.page.getByText("Video call started")).toBeVisible();
|
||||
await expect(user.page.getByRole("button", { name: "Join" })).toBeVisible();
|
||||
await user.page.getByRole("button", { name: "Join" }).click();
|
||||
}
|
||||
|
||||
for (const user of [timo, robin, halfshot, florian]) {
|
||||
const frame = user.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
|
||||
// No lobby, should start with video on
|
||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
||||
const videoButton = frame.getByTestId("incall_videomute");
|
||||
await expect(videoButton).toBeVisible();
|
||||
// video should be off by default in a voice call
|
||||
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
|
||||
}
|
||||
|
||||
// We should see 5 video tiles everywhere now
|
||||
for (const user of [valere, timo, robin, halfshot, florian]) {
|
||||
const frame = user.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
await expect(frame.getByTestId("videoTile")).toHaveCount(5);
|
||||
for (const participant of [valere, timo, robin, halfshot, florian]) {
|
||||
// Check the names are correct
|
||||
await expect(frame.getByText(participant.displayName)).toBeVisible();
|
||||
}
|
||||
|
||||
// There is no other options than to wait for all media to be ready?
|
||||
// Or it is too flaky :/
|
||||
await user.page.waitForTimeout(5000);
|
||||
// No one should be waiting for media
|
||||
await expect(frame.getByText("Waiting for media...")).not.toBeVisible();
|
||||
|
||||
// There should be 5 video elements, visible and autoplaying
|
||||
const videoElements = await frame.locator("video").all();
|
||||
expect(videoElements.length).toBe(5);
|
||||
await expect(frame.locator("video[autoplay]")).toHaveCount(5);
|
||||
|
||||
const blockDisplayCount = await frame
|
||||
.locator("video")
|
||||
.evaluateAll(
|
||||
(videos: Element[]) =>
|
||||
videos.filter(
|
||||
(v: Element) => window.getComputedStyle(v).display === "block",
|
||||
).length,
|
||||
);
|
||||
expect(blockDisplayCount).toBe(5);
|
||||
}
|
||||
|
||||
// Quickly test muting one participant to see it reflects and that our asserts works
|
||||
const florianFrame = florian.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
const florianMuteButton = florianFrame.getByTestId("incall_videomute");
|
||||
await florianMuteButton.click();
|
||||
// Now the button should indicate we can start video
|
||||
await expect(florianMuteButton).toHaveAttribute(
|
||||
"aria-label",
|
||||
/^Start video$/,
|
||||
);
|
||||
|
||||
// wait a bit for the state to propagate
|
||||
await valere.page.waitForTimeout(3000);
|
||||
{
|
||||
const frame = valere.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
|
||||
const videoElements = await frame.locator("video").all();
|
||||
expect(videoElements.length).toBe(5);
|
||||
|
||||
const blockDisplayCount = await frame
|
||||
.locator("video")
|
||||
.evaluateAll(
|
||||
(videos: Element[]) =>
|
||||
videos.filter(
|
||||
(v: Element) => window.getComputedStyle(v).display === "block",
|
||||
).length,
|
||||
);
|
||||
|
||||
// out of 5 ONLY 4 are visible (display:block) !!
|
||||
// XXX we need to be better at our HTML markup and accessibility, it would make
|
||||
// this kind of stuff way easier to test if we could look out for aria attributes.
|
||||
expect(blockDisplayCount).toBe(4);
|
||||
}
|
||||
|
||||
await valere.page.pause();
|
||||
});
|
||||
156
playwright/widget/test-helpers.ts
Normal file
156
playwright/widget/test-helpers.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Browser,
|
||||
expect,
|
||||
type JSHandle,
|
||||
type Page,
|
||||
} from "@playwright/test";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
const PASSWORD = "foobarbaz1!";
|
||||
|
||||
export class TestHelpers {
|
||||
public static async startCallInCurrentRoom(
|
||||
page: Page,
|
||||
voice: boolean = false,
|
||||
): Promise<void> {
|
||||
const buttonName = voice ? "Voice call" : "Video call";
|
||||
await expect(page.getByRole("button", { name: buttonName })).toBeVisible();
|
||||
await page.getByRole("button", { name: buttonName }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Element Call" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user and returns page, clientHandle and mxId.
|
||||
*/
|
||||
public static async registerUser(
|
||||
browser: Browser,
|
||||
username: string,
|
||||
): Promise<{
|
||||
page: Page;
|
||||
clientHandle: JSHandle<MatrixClient>;
|
||||
mxId: string;
|
||||
}> {
|
||||
const userContext = await browser.newContext({
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const page = await userContext.newPage();
|
||||
await page.goto("http://localhost:8081/#/welcome");
|
||||
await page.getByRole("link", { name: "Create Account" }).click();
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page
|
||||
.getByRole("textbox", { name: "Password", exact: true })
|
||||
.fill(PASSWORD);
|
||||
await page.getByRole("textbox", { name: "Confirm password" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Confirm password" })
|
||||
.fill(PASSWORD);
|
||||
await page.getByRole("button", { name: "Register" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: `Welcome ${username}` }),
|
||||
).toBeVisible({
|
||||
// Increase timeout as registration can be slow :/
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
const browserUnsupportedToast = page
|
||||
.getByText("Element does not support this browser")
|
||||
.locator("..")
|
||||
.locator("..");
|
||||
|
||||
// Dismiss incompatible browser toast
|
||||
const dismissButton = browserUnsupportedToast.getByRole("button", {
|
||||
name: "Dismiss",
|
||||
});
|
||||
try {
|
||||
await expect(dismissButton).toBeVisible({ timeout: 700 });
|
||||
await dismissButton.click();
|
||||
} catch {
|
||||
// dismissButton not visible, continue as normal
|
||||
}
|
||||
|
||||
await TestHelpers.setDevToolElementCallDevUrl(page);
|
||||
|
||||
const clientHandle = await page.evaluateHandle(() =>
|
||||
window.mxMatrixClientPeg.get(),
|
||||
);
|
||||
const mxId = (await clientHandle.evaluate(
|
||||
(cli: MatrixClient) => cli.getUserId(),
|
||||
clientHandle,
|
||||
))!;
|
||||
|
||||
return { page, clientHandle, mxId };
|
||||
}
|
||||
|
||||
public static async createRoom(
|
||||
name: string,
|
||||
page: Page,
|
||||
andInvite: string[] = [],
|
||||
): Promise<void> {
|
||||
await page.pause();
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
|
||||
await page.getByRole("menuitem", { name: "New Room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill(name);
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
await expect(page.getByText("You created this room.")).toBeVisible();
|
||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||
|
||||
// Invite users if any
|
||||
if (andInvite.length > 0) {
|
||||
await page
|
||||
.getByRole("button", { name: "Invite to this room", exact: true })
|
||||
.click();
|
||||
|
||||
const inviteInput = page.getByRole("dialog").getByRole("textbox");
|
||||
for (const mxId of andInvite) {
|
||||
await inviteInput.focus();
|
||||
await inviteInput.fill(mxId);
|
||||
await inviteInput.press("Enter");
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Invite" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current Element Web app to use the dev Element Call URL.
|
||||
* @param page - The EW page
|
||||
*/
|
||||
public static async setDevToolElementCallDevUrl(page: Page): Promise<void> {
|
||||
if (process.env.USE_DOCKER) {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"http://localhost:8080/room",
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await page.evaluate(() => {
|
||||
window.mxSettingsStore.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
"device",
|
||||
"https://localhost:3000/room",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { widgetTest } from "../fixtures/widget-user.ts";
|
||||
import { TestHelpers } from "./test-helpers.ts";
|
||||
|
||||
widgetTest.use({ callType: "dm" });
|
||||
|
||||
@@ -23,16 +24,7 @@ widgetTest(
|
||||
|
||||
const { brooks, whistler } = asWidget;
|
||||
|
||||
await expect(
|
||||
brooks.page.getByRole("button", { name: "Voice call" }),
|
||||
).toBeVisible();
|
||||
await brooks.page.getByRole("button", { name: "Voice call" }).click();
|
||||
|
||||
await expect(
|
||||
brooks.page.getByRole("menuitem", { name: "Element Call" }),
|
||||
).toBeVisible();
|
||||
|
||||
await brooks.page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
await TestHelpers.startCallInCurrentRoom(brooks.page, true);
|
||||
|
||||
await expect(
|
||||
brooks.page.locator('iframe[title="Element Call"]'),
|
||||
@@ -123,16 +115,7 @@ widgetTest(
|
||||
|
||||
const { brooks, whistler } = asWidget;
|
||||
|
||||
await expect(
|
||||
brooks.page.getByRole("button", { name: "Video call" }),
|
||||
).toBeVisible();
|
||||
await brooks.page.getByRole("button", { name: "Video call" }).click();
|
||||
|
||||
await expect(
|
||||
brooks.page.getByRole("menuitem", { name: "Element Call" }),
|
||||
).toBeVisible();
|
||||
|
||||
await brooks.page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
await TestHelpers.startCallInCurrentRoom(brooks.page, false);
|
||||
|
||||
await expect(
|
||||
brooks.page.locator('iframe[title="Element Call"]'),
|
||||
@@ -210,3 +193,48 @@ widgetTest(
|
||||
await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
widgetTest(
|
||||
"Decline a new video call in DM as widget",
|
||||
async ({ asWidget, browserName }) => {
|
||||
test.skip(
|
||||
browserName === "firefox",
|
||||
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
|
||||
);
|
||||
|
||||
test.slow(); // Triples the timeout
|
||||
|
||||
const { brooks, whistler } = asWidget;
|
||||
|
||||
await TestHelpers.startCallInCurrentRoom(brooks.page, false);
|
||||
|
||||
await expect(
|
||||
brooks.page.locator('iframe[title="Element Call"]'),
|
||||
).toBeVisible();
|
||||
|
||||
const brooksFrame = brooks.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
|
||||
// We should show a ringing overlay, let's check for that
|
||||
await expect(
|
||||
brooksFrame.getByText(`Waiting for ${whistler.displayName} to join…`),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(whistler.page.getByText("Incoming video call")).toBeVisible();
|
||||
await whistler.page.getByRole("button", { name: "Decline" }).click();
|
||||
|
||||
await expect(
|
||||
whistler.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
|
||||
// The widget should be closed and the timeline should be back on screen
|
||||
await expect(
|
||||
brooks.page.locator('iframe[title="Element Call"]'),
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
brooks.page.getByText("This is the beginning of your"),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -842,6 +842,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
.getConnections()
|
||||
.map((connectionItem) => ({
|
||||
room: connectionItem.livekitRoom,
|
||||
livekitAlias: connectionItem.livekitAlias,
|
||||
// TODO compute is local or tag it in the livekit room items already
|
||||
isLocal: undefined,
|
||||
url: connectionItem.transport.livekit_service_url,
|
||||
|
||||
@@ -36,7 +36,7 @@ function createMockLivekitRoom(
|
||||
wsUrl: string,
|
||||
serverInfo: object,
|
||||
metadata: string,
|
||||
): { isLocal: boolean; url: string; room: LivekitRoom } {
|
||||
): { isLocal: boolean; url: string; room: LivekitRoom; livekitAlias: string } {
|
||||
const mockRoom = {
|
||||
serverInfo,
|
||||
metadata,
|
||||
@@ -49,6 +49,7 @@ function createMockLivekitRoom(
|
||||
isLocal: true,
|
||||
url: wsUrl,
|
||||
room: mockRoom,
|
||||
livekitAlias: "TestAlias",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ describe("DeveloperSettingsTab", () => {
|
||||
room: LivekitRoom;
|
||||
url: string;
|
||||
isLocal?: boolean;
|
||||
livekitAlias: string;
|
||||
}[] = [
|
||||
createMockLivekitRoom(
|
||||
"wss://local-sfu.example.org",
|
||||
@@ -80,6 +82,7 @@ describe("DeveloperSettingsTab", () => {
|
||||
),
|
||||
{
|
||||
isLocal: false,
|
||||
livekitAlias: "TestAlias2",
|
||||
url: "wss://remote-sfu.example.org",
|
||||
room: {
|
||||
localParticipant: { identity: "localParticipantIdentity" },
|
||||
|
||||
@@ -51,7 +51,12 @@ import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomId?: string;
|
||||
livekitRooms?: { room: LivekitRoom; url: string; isLocal?: boolean }[];
|
||||
livekitRooms?: {
|
||||
room: LivekitRoom;
|
||||
url: string;
|
||||
isLocal?: boolean;
|
||||
livekitAlias?: string;
|
||||
}[];
|
||||
env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -343,6 +348,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
url: livekitRoom.url || "unknown",
|
||||
})}
|
||||
</h4>
|
||||
<p>LivekitAlias: {livekitRoom.livekitAlias}</p>
|
||||
{livekitRoom.isLocal && <p>ws-url: {localSfuUrl?.href}</p>}
|
||||
<p>
|
||||
{t("developer_mode.livekit_server_info")}(
|
||||
|
||||
@@ -355,6 +355,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
<h4>
|
||||
LiveKit SFU: wss://local-sfu.example.org
|
||||
</h4>
|
||||
<p>
|
||||
LivekitAlias:
|
||||
TestAlias
|
||||
</p>
|
||||
<p>
|
||||
ws-url:
|
||||
wss://local-sfu.example.org/
|
||||
@@ -393,6 +397,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
<h4>
|
||||
LiveKit SFU: wss://remote-sfu.example.org
|
||||
</h4>
|
||||
<p>
|
||||
LivekitAlias:
|
||||
TestAlias2
|
||||
</p>
|
||||
<p>
|
||||
LiveKit Server Info
|
||||
(
|
||||
|
||||
@@ -124,9 +124,9 @@ import {
|
||||
} from "./remoteMembers/ConnectionManager.ts";
|
||||
import {
|
||||
createMatrixLivekitMembers$,
|
||||
type TaggedParticipant,
|
||||
type LocalMatrixLivekitMember,
|
||||
type RemoteMatrixLivekitMember,
|
||||
type MatrixLivekitMember,
|
||||
} from "./remoteMembers/MatrixLivekitMembers.ts";
|
||||
import {
|
||||
type AutoLeaveReason,
|
||||
@@ -717,65 +717,38 @@ export function createCallViewModel$(
|
||||
matrixLivekitMembers,
|
||||
duplicateTiles,
|
||||
]) {
|
||||
let localUserMediaId: string | undefined = undefined;
|
||||
// add local member if available
|
||||
if (localMatrixLivekitMember) {
|
||||
const computeMediaId = (m: MatrixLivekitMember): string =>
|
||||
`${m.userId}:${m.membership$.value.deviceId}`;
|
||||
|
||||
const localUserMediaId = localMatrixLivekitMember
|
||||
? computeMediaId(localMatrixLivekitMember)
|
||||
: undefined;
|
||||
|
||||
const localAsArray = localMatrixLivekitMember
|
||||
? [localMatrixLivekitMember]
|
||||
: [];
|
||||
const remoteWithoutLocal = matrixLivekitMembers.value.filter(
|
||||
(m) => computeMediaId(m) !== localUserMediaId,
|
||||
);
|
||||
const allMatrixLivekitMembers = [
|
||||
...localAsArray,
|
||||
...remoteWithoutLocal,
|
||||
];
|
||||
|
||||
for (const matrixLivekitMember of allMatrixLivekitMembers) {
|
||||
const { userId, participant, connection$, membership$ } =
|
||||
localMatrixLivekitMember;
|
||||
|
||||
localUserMediaId = `${userId}:${membership$.value.deviceId}`;
|
||||
const rtcBackendIdentity = membership$.value.rtcBackendIdentity;
|
||||
matrixLivekitMember;
|
||||
const rtcId = membership$.value.rtcBackendIdentity; // rtcBackendIdentity
|
||||
const mediaId = computeMediaId(matrixLivekitMember);
|
||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||
yield {
|
||||
keys: [
|
||||
dup,
|
||||
localUserMediaId,
|
||||
userId,
|
||||
participant satisfies TaggedParticipant as TaggedParticipant, // Widen the type safely
|
||||
connection$,
|
||||
rtcBackendIdentity,
|
||||
],
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
// add remote members that are available
|
||||
for (const {
|
||||
userId,
|
||||
participant,
|
||||
connection$,
|
||||
membership$,
|
||||
} of matrixLivekitMembers.value) {
|
||||
const userMediaId = `${userId}:${membership$.value.deviceId}`;
|
||||
const rtcBackendIdentity = membership$.value.rtcBackendIdentity;
|
||||
// skip local user as we added them manually before
|
||||
if (userMediaId === localUserMediaId) continue;
|
||||
|
||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||
yield {
|
||||
keys: [
|
||||
dup,
|
||||
userMediaId,
|
||||
userId,
|
||||
participant,
|
||||
connection$,
|
||||
rtcBackendIdentity,
|
||||
],
|
||||
keys: [dup, mediaId, userId, participant, connection$, rtcId],
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
(
|
||||
scope,
|
||||
_data$,
|
||||
dup,
|
||||
userMediaId,
|
||||
userId,
|
||||
participant,
|
||||
connection$,
|
||||
rtcBackendIdentity,
|
||||
) => {
|
||||
(scope, _, dup, mediaId, userId, participant, connection$, rtcId) => {
|
||||
const livekitRoom$ = scope.behavior(
|
||||
connection$.pipe(map((c) => c?.livekitRoom)),
|
||||
);
|
||||
@@ -790,9 +763,9 @@ export function createCallViewModel$(
|
||||
|
||||
return new UserMedia(
|
||||
scope,
|
||||
`${userMediaId}:${dup}`,
|
||||
`${mediaId}:${dup}`,
|
||||
userId,
|
||||
rtcBackendIdentity,
|
||||
rtcId,
|
||||
participant,
|
||||
options.encryptionSystem,
|
||||
livekitRoom$,
|
||||
@@ -801,8 +774,8 @@ export function createCallViewModel$(
|
||||
localMembership.reconnecting$,
|
||||
displayName$,
|
||||
matrixMemberMetadataStore.createAvatarUrlBehavior$(userId),
|
||||
handsRaised$.pipe(map((v) => v[userMediaId]?.time ?? null)),
|
||||
reactions$.pipe(map((v) => v[userMediaId] ?? undefined)),
|
||||
handsRaised$.pipe(map((v) => v[mediaId]?.time ?? null)),
|
||||
reactions$.pipe(map((v) => v[mediaId] ?? undefined)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -68,6 +68,27 @@ export enum JwtEndpointVersion {
|
||||
Matrix_2_0 = "matrix_2_0",
|
||||
}
|
||||
|
||||
// TODO livekit_alias-cleanup
|
||||
// 1. We need to move away from transports map to connections!!!
|
||||
//
|
||||
// 2. We need to stop sending livekit_alias all together
|
||||
//
|
||||
//
|
||||
// 1.
|
||||
// Transports are just the jwt service adress but do not contain the information which room on this transport to use.
|
||||
// That requires slot and roomId.
|
||||
//
|
||||
// We need one connection per room on the transport.
|
||||
//
|
||||
// We need an object that contains:
|
||||
// transport
|
||||
// roomId
|
||||
// slotId
|
||||
//
|
||||
// To map to the connections. Prosposal: `ConnectionIdentifier`
|
||||
//
|
||||
// 2.
|
||||
// We need to make sure we do not sent livekit_alias in sticky events and that we drop all code for sending state events!
|
||||
export interface LocalTransportWithSFUConfig {
|
||||
transport: LivekitTransport;
|
||||
sfuConfig: SFUConfig;
|
||||
@@ -250,7 +271,18 @@ async function makeTransport(
|
||||
transport: {
|
||||
type: "livekit",
|
||||
livekit_service_url: url,
|
||||
livekit_alias: sfuConfig.livekitAlias,
|
||||
// WARNING PLS READ ME!!!
|
||||
// This looks unintuitive especially considering that `sfuConfig.livekitAlias` exists.
|
||||
// Why do we not use: `livekit_alias: sfuConfig.livekitAlias`
|
||||
//
|
||||
// - This is going to be used for sending our state event transport (focus_preferred)
|
||||
// - In sticky events it is expected to NOT send this field at all. The transport is only the `type`, `livekit_service_url`
|
||||
// - If we set it to the hased alias we get from the jwt, we will end up using the hashed alias as the body.roomId field
|
||||
// in v0.16.0. (It will use oldest member transport. It is using the transport.livekit_alias as the body.roomId)
|
||||
//
|
||||
// TLDR this is a temporal field that allow for comaptibilty but the spec expects it to not exists. (but its existance also does not break anything)
|
||||
// It is just named poorly: It was intetended to be the actual alias. But now we do pseudonymys ids so we use a hashed alias.
|
||||
livekit_alias: roomId,
|
||||
},
|
||||
sfuConfig,
|
||||
};
|
||||
|
||||
@@ -117,6 +117,14 @@ export class Connection {
|
||||
*/
|
||||
public readonly remoteParticipants$: Behavior<RemoteParticipant[]>;
|
||||
|
||||
/**
|
||||
* The alias of the LiveKit room.
|
||||
*/
|
||||
public get livekitAlias(): string | undefined {
|
||||
return this._livekitAlias;
|
||||
}
|
||||
private _livekitAlias?: string;
|
||||
|
||||
/**
|
||||
* Whether the connection has been stopped.
|
||||
* @see Connection.stop
|
||||
@@ -144,9 +152,10 @@ export class Connection {
|
||||
this._state$.next(ConnectionState.FetchingConfig);
|
||||
// We should already have this information after creating the localTransport.
|
||||
// only call getSFUConfigWithOpenID for connections where we do not have a token yet. (existingJwtTokenData === undefined)
|
||||
const { url, jwt } =
|
||||
const { url, jwt, livekitAlias } =
|
||||
this.existingSFUConfig ??
|
||||
(await this.getSFUConfigForRemoteConnection());
|
||||
this._livekitAlias = livekitAlias;
|
||||
// If we were stopped while fetching the config, don't proceed to connect
|
||||
if (this.stopped) return;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export type TaggedParticipant =
|
||||
| LocalTaggedParticipant
|
||||
| RemoteTaggedParticipant;
|
||||
|
||||
interface MatrixLivekitMember {
|
||||
export interface MatrixLivekitMember {
|
||||
membership$: Behavior<CallMembership>;
|
||||
connection$: Behavior<Connection | null>;
|
||||
// participantId: string; We do not want a participantId here since it will be generated by the jwt
|
||||
@@ -143,9 +143,14 @@ export function areLivekitTransportsEqual<T extends LivekitTransport>(
|
||||
t1: T | null,
|
||||
t2: T | null,
|
||||
): boolean {
|
||||
if (t1 && t2) return t1.livekit_service_url === t2.livekit_service_url;
|
||||
// In case we have different lk rooms in the same SFU (depends on the livekit authorization service)
|
||||
// It is only needed in case the livekit authorization service is not behaving as expected (or custom implementation)
|
||||
if (t1 && t2)
|
||||
return (
|
||||
t1.livekit_service_url === t2.livekit_service_url &&
|
||||
// In case we have different lk rooms in the same SFU (depends on the livekit authorization service)
|
||||
// It is only needed in case the livekit authorization service is not behaving as expected (or custom implementation)
|
||||
// Also LivekitTransport is planned to become a `ConnectionIdentifier` which moves this equal somewhere else.
|
||||
t1.livekit_alias === t2.livekit_alias
|
||||
);
|
||||
if (!t1 && !t2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
44
yarn.lock
44
yarn.lock
@@ -2903,16 +2903,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/dom@npm:1.6.13, @floating-ui/dom@npm:^1.0.0":
|
||||
version: 1.6.13
|
||||
resolution: "@floating-ui/dom@npm:1.6.13"
|
||||
dependencies:
|
||||
"@floating-ui/core": "npm:^1.6.0"
|
||||
"@floating-ui/utils": "npm:^0.2.9"
|
||||
checksum: 10c0/272242d2eb6238ffcee0cb1f3c66e0eafae804d5d7b449db5ecf904bc37d31ad96cf575a9e650b93c1190f64f49a684b1559d10e05ed3ec210628b19116991a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/dom@npm:1.7.4":
|
||||
version: 1.7.4
|
||||
resolution: "@floating-ui/dom@npm:1.7.4"
|
||||
@@ -2923,6 +2913,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/dom@npm:^1.0.0":
|
||||
version: 1.6.13
|
||||
resolution: "@floating-ui/dom@npm:1.6.13"
|
||||
dependencies:
|
||||
"@floating-ui/core": "npm:^1.6.0"
|
||||
"@floating-ui/utils": "npm:^0.2.9"
|
||||
checksum: 10c0/272242d2eb6238ffcee0cb1f3c66e0eafae804d5d7b449db5ecf904bc37d31ad96cf575a9e650b93c1190f64f49a684b1559d10e05ed3ec210628b19116991a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "@floating-ui/react-dom@npm:2.1.2"
|
||||
@@ -3213,21 +3213,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@livekit/components-core@npm:0.12.11":
|
||||
version: 0.12.11
|
||||
resolution: "@livekit/components-core@npm:0.12.11"
|
||||
dependencies:
|
||||
"@floating-ui/dom": "npm:1.6.13"
|
||||
loglevel: "npm:1.9.1"
|
||||
rxjs: "npm:7.8.2"
|
||||
peerDependencies:
|
||||
livekit-client: ^2.15.14
|
||||
tslib: ^2.6.2
|
||||
checksum: 10c0/9c2ac3d30bb8cc9067ae0b2049784f81e90e57df9eabf7edbaf3c8ceb65a63f644a4e6abeb6cc38d3ebe52663d8dbb88535e01a965011f365d5ae1f3daf86052
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@livekit/components-core@npm:^0.12.0":
|
||||
"@livekit/components-core@npm:0.12.12, @livekit/components-core@npm:^0.12.0":
|
||||
version: 0.12.12
|
||||
resolution: "@livekit/components-core@npm:0.12.12"
|
||||
dependencies:
|
||||
@@ -3242,10 +3228,10 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@livekit/components-react@npm:^2.0.0":
|
||||
version: 2.9.16
|
||||
resolution: "@livekit/components-react@npm:2.9.16"
|
||||
version: 2.9.17
|
||||
resolution: "@livekit/components-react@npm:2.9.17"
|
||||
dependencies:
|
||||
"@livekit/components-core": "npm:0.12.11"
|
||||
"@livekit/components-core": "npm:0.12.12"
|
||||
clsx: "npm:2.1.1"
|
||||
events: "npm:^3.3.0"
|
||||
jose: "npm:^6.0.12"
|
||||
@@ -3259,7 +3245,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
"@livekit/krisp-noise-filter":
|
||||
optional: true
|
||||
checksum: 10c0/4ba4ff473c5a29d3107412733a6676a3b708d70684ed463e9b34cda26abb3d2f317c2828a52e730837b756de9df3fc248260d6f390aedebfb6ec96ef63c7b151
|
||||
checksum: 10c0/ba64ada37d4b3ce4d5ee7c5b2a6bddbffc17c2e641e95881aac9f02b4ff7428105e0a372d364e50ff124e988b7426d322d94caabdb55b634aebf0144d7e37f99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user