Add the MatrixRTC SDK package, its harness and a failing smoke test

Set up `sdk/` as a package shaped exactly like `component/`: its own
package.json with peer dependencies, the pnpm workspace marker so a host can
install it as a git dependency, a build tsconfig for type emission, and a vite
lib config with an externals list that `pnpm lint:externals` now checks for
both library builds.

The package holds only the interface for now. `createRtcSession` returns an
empty object; `RtcSession`, `RtcMember`, `MemberMedia` and `TransportMetadata`
are the contract Element Call's `CallViewModel` will be rebuilt on (see
sdk-plan.md). The shared primitives are re-exported from `src` until the first
implementation slice moves them.

`sdk/dev` is the smallest consumer of the SDK: a plain page that logs in, joins
a room and renders one tile per member, served by `pnpm dev:sdk` on port 3002.
Playwright starts it as a third web server, and `playwright/sdk/smoke.spec.ts`
has two browsers join one session. It fails on the status line with the SDK's
own error until there is an implementation behind the interface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Timo K.
2026-10-08 13:53:56 +02:00
co-authored by Claude Fable 5.1
parent 2b9fdfe4d5
commit 9910496b90
21 changed files with 1070 additions and 79 deletions
+88
View File
@@ -0,0 +1,88 @@
/*
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, type Page } from "@playwright/test";
import { SynapseAdmin } from "../utils/synapse-admin.ts";
/**
* Where the SDK harness is served — `sdk/dev`, a page that uses the SDK the way
* a host would, with none of Element Call in it.
*/
export const SDK_HARNESS_URL = "https://localhost:3002";
const HOMESERVER_URL = "https://synapse.m.localhost";
const PASSWORD = "foobarbaz1!";
/**
* Registers two users through the Synapse admin API and has the first create a
* public room, so that the second can join it by id without an invite.
*/
export async function createUsersAndRoom(
name: string,
): Promise<{ usernames: [string, string]; roomId: string }> {
const admin = SynapseAdmin.forHomeserver(HOMESERVER_URL);
const usernames: [string, string] = [
`${name}_a_${Date.now()}`,
`${name}_b_${Date.now()}`,
];
const [{ access_token: accessToken }] = await Promise.all(
usernames.map(async (username, index) =>
admin.registerUser(username, PASSWORD, `${name} ${"AB"[index]}`),
),
);
const response = await fetch(
`${HOMESERVER_URL}/_matrix/client/v3/createRoom`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: `${name}'s session`,
preset: "public_chat",
}),
},
);
if (!response.ok)
throw new Error(
`Could not create a room: ${response.status} ${await response.text()}`,
);
const { room_id: roomId } = (await response.json()) as { room_id: string };
return { usernames, roomId };
}
/**
* Opens the harness signed in as the given user and waits until the SDK
* reports the session as joined. The status line carries the SDK's error if
* it does not get there, so the failure says why.
*/
export async function startHarness(
page: Page,
username: string,
roomId: string,
): Promise<void> {
const query = new URLSearchParams({
homeserver: HOMESERVER_URL,
username,
password: PASSWORD,
room: roomId,
});
await page.goto(`${SDK_HARNESS_URL}/?${query.toString()}`);
await page.getByRole("button", { name: "Start" }).click();
// A login, a crypto setup and an initial sync happen first. An error is
// final, so it is not worth waiting out the timeout for "Joined" after one.
const status = page.getByTestId("status");
await expect(status).toHaveText(/^(Joined|Error)/, { timeout: 120_000 });
const text = await status.textContent();
if (text !== "Joined")
throw new Error(`The harness did not join the session: ${text}`);
}
+56
View File
@@ -0,0 +1,56 @@
/*
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 Page, test } from "@playwright/test";
import { createUsersAndRoom, startHarness } from "./harness.ts";
/**
* The MatrixRTC SDK driven through its harness in `sdk/dev`: no Element Call
* on the page, only the SDK's public API.
*
* This is the smoke test the implementation is built against. It fails until
* `createRtcSession` does something, and it fails on the status line first, so
* the SDK's own error is what the report shows.
*/
// Two browsers each log in, set up crypto and sync before anything is on
// screen, then wait for media to connect
test.describe.configure({ timeout: 300_000 });
test("two browsers see each other in one session", async ({ browser }) => {
const { usernames, roomId } = await createUsersAndRoom("sdksmoke");
const [pageA, pageB] = await Promise.all(
usernames.map(async () => newPage(browser)),
);
await Promise.all([
startHarness(pageA, usernames[0], roomId),
startHarness(pageB, usernames[1], roomId),
]);
// Each page shows itself and the other, each tile named by its user
for (const page of [pageA, pageB]) {
await expect(page.getByTestId("member")).toHaveCount(2, {
timeout: 60_000,
});
for (const username of usernames)
await expect(
page.getByTestId("member").filter({ hasText: username }),
).toHaveCount(1);
}
});
/**
* A context of its own per user, since a browser profile holds one login. No
* permissions to grant: each browser is launched with fake media that is
* handed out without asking (see playwright.config.ts).
*/
async function newPage(browser: Browser): Promise<Page> {
const context = await browser.newContext({ ignoreHTTPSErrors: true });
return context.newPage();
}