Test that the local user can see their own screen share

To make this test work I had to extend the mocking of the CallViewModel tests to make a local connection object exist.
This commit is contained in:
Robin
2026-04-15 19:53:11 +02:00
parent b03524e25f
commit e8963effe2
5 changed files with 154 additions and 24 deletions

View File

@@ -18,7 +18,7 @@ import {
import { mockConfig } from "./utils/test";
const sentryInitSpy = vi.fn();
const sentryInitSpy = vi.hoisted(() => vi.fn());
// Place the mock after the spy is defined
vi.mock("@sentry/react", () => ({

View File

@@ -36,7 +36,6 @@ import { deepCompare } from "matrix-js-sdk/lib/utils";
import { type Layout } from "../layout-types.ts";
import {
mockLocalParticipant,
mockMatrixRoomMember,
mockRemoteParticipant,
withTestScheduler,
@@ -61,7 +60,10 @@ import {
import { MediaDevices } from "../MediaDevices.ts";
import { getValue } from "../../utils/observable.ts";
import { type Behavior, constant } from "../Behavior.ts";
import { withCallViewModel as withCallViewModelInMode } from "./CallViewModelTestUtils.ts";
import {
localParticipant,
withCallViewModel as withCallViewModelInMode,
} from "./CallViewModelTestUtils.ts";
import { MatrixRTCMode } from "../../settings/settings.ts";
import { initializeWidget } from "../../widget.ts";
@@ -104,7 +106,6 @@ const dave = mockMatrixRoomMember(daveRtcMember, { rawDisplayName: "Dave" });
const daveId = `${dave.userId}:${daveRtcMember.deviceId}`;
const localParticipant = mockLocalParticipant({ identity: "" });
const bobParticipant = mockRemoteParticipant({ identity: bobId });
const daveParticipant = mockRemoteParticipant({ identity: daveId });
@@ -269,7 +270,7 @@ describe.each([
});
});
test("screen sharing activates spotlight layout", () => {
test("remote screen sharing activates spotlight layout", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => {
// Start with no screen shares, then have Alice and Bob share their screens,
// then return to no screen shares, then have just Alice share for a bit
@@ -350,6 +351,76 @@ describe.each([
});
});
test("local screen sharing stays in grid layout", () => {
withTestScheduler(({ behavior, expectObservable }) => {
// Local participant shares their screen, then stops sharing
const sharingInputMarbles = " nyn";
// Layout should show the screen share but stay in type: "grid"
const expectedLayoutMarbles = "aba";
withCallViewModel(
{
remoteParticipants$: constant([aliceParticipant, bobParticipant]),
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
sharingScreen: new Map([
[localParticipant, behavior(sharingInputMarbles, yesNo)],
]),
},
(vm) => {
expectObservable(summarizeLayout$(vm.layout$)).toBe(
expectedLayoutMarbles,
{
a: {
type: "grid",
spotlight: undefined,
grid: [`${localId}:0`, `${aliceId}:0`, `${bobId}:0`],
},
b: {
type: "grid",
spotlight: [`${localId}:0:screen-share`],
grid: [`${localId}:0`, `${aliceId}:0`, `${bobId}:0`],
},
},
);
},
);
});
});
test("local screen sharing in one-on-one call activates grid layout", () => {
withTestScheduler(({ behavior, expectObservable }) => {
// Local participant shares their screen, then stops sharing
const sharingInputMarbles = " nyn";
// Layout should switch to grid layout then back to one-on-one layout
const expectedLayoutMarbles = "aba";
withCallViewModel(
{
remoteParticipants$: constant([aliceParticipant]),
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
sharingScreen: new Map([
[localParticipant, behavior(sharingInputMarbles, yesNo)],
]),
},
(vm) => {
expectObservable(summarizeLayout$(vm.layout$)).toBe(
expectedLayoutMarbles,
{
a: {
type: "one-on-one",
pip: `${localId}:0`,
spotlight: `${aliceId}:0`,
},
b: {
type: "grid",
spotlight: [`${localId}:0:screen-share`],
grid: [`${localId}:0`, `${aliceId}:0`],
},
},
);
},
);
});
});
test("participants stay in the same order unless to appear/disappear", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => {
const visibilityInputMarbles = "a";

View File

@@ -105,12 +105,16 @@ import {
import {
createLocalTransport$,
JwtEndpointVersion,
type LocalTransport,
} from "./localMember/LocalTransport.ts";
import {
createMemberships$,
membershipsAndTransports$,
} from "../SessionBehaviors.ts";
import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts";
import {
type ConnectionFactory,
ECConnectionFactory,
} from "./remoteMembers/ConnectionFactory.ts";
import {
type ConnectionManagerData,
createConnectionManager$,
@@ -170,6 +174,10 @@ export interface CallViewModelOptions {
connectionState$?: Behavior<ConnectionState>;
/** Optional behavior overriding the computed window size, mainly for testing purposes. */
windowSize$?: Behavior<{ width: number; height: number }>;
/** Optional value overriding the local transport, for testing purposes. */
localTransport?: LocalTransport;
/** Optional value overriding the connection factory, for testing purposes. */
connectionFactory?: ConnectionFactory;
/** The version & compatibility mode of MatrixRTC that we should use. */
matrixRTCMode$?: Behavior<MatrixRTCMode>;
}
@@ -441,6 +449,7 @@ export function createCallViewModel$(
// Re-create LocalTransport whenever the mode changes
(mode) => ({ keys: [mode], data: undefined }),
(scope, _data$, mode) =>
options.localTransport ??
createLocalTransport$({
scope: scope,
memberships$: memberships$,
@@ -467,17 +476,19 @@ export function createCallViewModel$(
),
);
const connectionFactory = new ECConnectionFactory(
client,
matrixRoom.roomId,
mediaDevices,
trackProcessorState$,
livekitKeyProvider,
getUrlParams().controlledAudioDevices,
options.livekitRoomFactory,
getUrlParams().echoCancellation,
getUrlParams().noiseSuppression,
);
const connectionFactory =
options.connectionFactory ??
new ECConnectionFactory(
client,
matrixRoom.roomId,
mediaDevices,
trackProcessorState$,
livekitKeyProvider,
getUrlParams().controlledAudioDevices,
options.livekitRoomFactory,
getUrlParams().echoCancellation,
getUrlParams().noiseSuppression,
);
const connectionManager = createConnectionManager$({
scope: scope,

View File

@@ -30,7 +30,10 @@ import {
type CallViewModelOptions,
} from "./CallViewModel";
import {
exampleSfuConfig,
exampleTransport,
mockConfig,
MockConnection,
mockLivekitRoom,
mockLocalParticipant,
mockMatrixRoom,
@@ -75,7 +78,7 @@ export interface CallViewModelInputs {
windowSize$: Behavior<{ width: number; height: number }>;
}
const localParticipant = mockLocalParticipant({ identity: "" });
export const localParticipant = mockLocalParticipant({ identity: "" });
export function withCallViewModel(mode: MatrixRTCMode) {
return (
@@ -180,6 +183,13 @@ export function withCallViewModel(mode: MatrixRTCMode) {
);
const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({});
const livekitRoomFactory = (): LivekitRoom =>
mockLivekitRoom({
localParticipant,
disconnect: async () => Promise.resolve(),
setE2EEEnabled: async () => Promise.resolve(),
});
const vm = createCallViewModel$(
testScope(),
rtcSession.asMockedSession(),
@@ -189,14 +199,38 @@ export function withCallViewModel(mode: MatrixRTCMode) {
{
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
autoLeaveWhenOthersLeft: false,
livekitRoomFactory: (): LivekitRoom =>
mockLivekitRoom({
localParticipant,
disconnect: async () => Promise.resolve(),
setE2EEEnabled: async () => Promise.resolve(),
}),
livekitRoomFactory,
connectionState$,
windowSize$,
localTransport: {
active$: constant({
transport: exampleTransport,
sfuConfig: exampleSfuConfig,
}),
advertised$: constant(exampleTransport),
},
connectionFactory: {
createConnection(
scope,
transport,
ownMembershipIdentity,
logger,
sfuConfig,
) {
return new MockConnection(
{
scope,
transport,
ownMembershipIdentity,
existingSFUConfig: sfuConfig,
client: room.client,
roomId: room.roomId,
livekitRoomFactory,
},
logger,
);
},
},
matrixRTCMode$: constant(mode),
...options,
},

View File

@@ -74,6 +74,8 @@ import {
createRemoteScreenShare,
type RemoteScreenShareViewModel,
} from "../state/media/RemoteScreenShareViewModel";
import { Connection } from "../state/CallViewModel/remoteMembers/Connection";
import { type SFUConfig } from "../livekit/openIDSFU";
export function withFakeTimers(continuation: () => void): void {
vi.useFakeTimers();
@@ -210,6 +212,13 @@ export const exampleTransport: LivekitTransport = {
livekit_alias: "!alias:example.org",
};
export const exampleSfuConfig: SFUConfig = {
jwt: "foo",
livekitAlias: "bar",
livekitIdentity: "baz",
url: "bro",
};
export function mockRtcMembership(
user: string | RoomMember,
deviceId: string,
@@ -564,3 +573,8 @@ export function mockMuteStates(
videoEnabled: false,
});
}
export class MockConnection extends Connection {
public async start(): Promise<void> {}
public async stop(): Promise<void> {}
}