Let a host supply Element Call's client

ClientProvider found its own client: from the widget API, or by restoring
or creating a session. A host that embeds Element Call already has one,
and owns the user's session, so accept it as a prop and skip all of that.

A supplied client seeds the state synchronously, since there is no session
of ours to restore and so nothing to wait for.

Also guard the broadcast that shuts down other instances of the app. It
protects Element Call's own session and crypto stores, which is why it was
already skipped in widget mode — a host's client has the same property, so
without this an embedded Element Call would close down the user's other
tabs on mount.

Adds the first tests for ClientContext, covering both
This commit is contained in:
Valere
2026-09-02 15:14:10 +02:00
parent eb117249d1
commit f6f47ede62
2 changed files with 95 additions and 7 deletions
+69
View File
@@ -0,0 +1,69 @@
/*
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, vi } from "vitest";
import { render } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { type MatrixClient } from "matrix-js-sdk";
import { type FC } from "react";
import { ClientProvider, useClientState } from "./ClientContext";
const mockClient = (): MatrixClient =>
({
on: vi.fn(),
removeListener: vi.fn(),
getUserId: () => "@alice:example.org",
getDeviceId: () => "AAAA",
stopClient: vi.fn(),
}) as Partial<MatrixClient> as MatrixClient;
/** Reports what the context says, so a test can assert on it. */
const ShowClientState: FC = () => {
const state = useClientState();
if (state === undefined) return <span>loading</span>;
if (state.state === "error") return <span>error</span>;
return (
<span>{state.authenticated?.client.getUserId() ?? "unauthenticated"}</span>
);
};
test("uses a client supplied by the host without waiting", () => {
const client = mockClient();
const { container } = render(
<BrowserRouter>
<ClientProvider client={client}>
<ShowClientState />
</ClientProvider>
</BrowserRouter>,
);
// Available on the very first render: a supplied client needs no session
// restoring, so there is no loading state to pass through.
expect(container.textContent).toBe("@alice:example.org");
});
test("does not claim exclusive use of storage when given a client", () => {
// The channel is created when the module loads, so spy on the prototype
// rather than trying to replace the global.
const postMessage = vi.spyOn(BroadcastChannel.prototype, "postMessage");
render(
<BrowserRouter>
<ClientProvider client={mockClient()}>
<ShowClientState />
</ClientProvider>
</BrowserRouter>,
);
// The broadcast shuts down other instances to protect Element Call's own
// stores. A host's client brings its own, so there is nothing to protect.
expect(postMessage).not.toHaveBeenCalled();
postMessage.mockRestore();
});