Simplify some test helpers that no longer need continuations

This commit is contained in:
Robin
2025-10-17 12:34:06 -04:00
parent d5efba285b
commit 13894aaf3a
4 changed files with 175 additions and 195 deletions

View File

@@ -17,8 +17,8 @@ import {
mockLocalParticipant, mockLocalParticipant,
mockMediaDevices, mockMediaDevices,
mockRtcMembership, mockRtcMembership,
withLocalMedia, createLocalMedia,
withRemoteMedia, createRemoteMedia,
withTestScheduler, withTestScheduler,
} from "../utils/test"; } from "../utils/test";
import { getValue } from "../utils/observable"; import { getValue } from "../utils/observable";
@@ -42,92 +42,89 @@ vi.mock("../Platform", () => ({
const rtcMembership = mockRtcMembership("@alice:example.org", "AAAA"); const rtcMembership = mockRtcMembership("@alice:example.org", "AAAA");
test("control a participant's volume", async () => { test("control a participant's volume", () => {
const setVolumeSpy = vi.fn(); const setVolumeSpy = vi.fn();
await withRemoteMedia(rtcMembership, {}, { setVolume: setVolumeSpy }, (vm) => const vm = createRemoteMedia(rtcMembership, {}, { setVolume: setVolumeSpy });
withTestScheduler(({ expectObservable, schedule }) => { withTestScheduler(({ expectObservable, schedule }) => {
schedule("-ab---c---d|", { schedule("-ab---c---d|", {
a() { a() {
// Try muting by toggling // Try muting by toggling
vm.toggleLocallyMuted(); vm.toggleLocallyMuted();
expect(setVolumeSpy).toHaveBeenLastCalledWith(0); expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
}, },
b() { b() {
// Try unmuting by dragging the slider back up // Try unmuting by dragging the slider back up
vm.setLocalVolume(0.6); vm.setLocalVolume(0.6);
vm.setLocalVolume(0.8); vm.setLocalVolume(0.8);
vm.commitLocalVolume(); vm.commitLocalVolume();
expect(setVolumeSpy).toHaveBeenCalledWith(0.6); expect(setVolumeSpy).toHaveBeenCalledWith(0.6);
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
}, },
c() { c() {
// Try muting by dragging the slider back down // Try muting by dragging the slider back down
vm.setLocalVolume(0.2); vm.setLocalVolume(0.2);
vm.setLocalVolume(0); vm.setLocalVolume(0);
vm.commitLocalVolume(); vm.commitLocalVolume();
expect(setVolumeSpy).toHaveBeenCalledWith(0.2); expect(setVolumeSpy).toHaveBeenCalledWith(0.2);
expect(setVolumeSpy).toHaveBeenLastCalledWith(0); expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
}, },
d() { d() {
// Try unmuting by toggling // Try unmuting by toggling
vm.toggleLocallyMuted(); vm.toggleLocallyMuted();
// The volume should return to the last non-zero committed volume // The volume should return to the last non-zero committed volume
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
}, },
}); });
expectObservable(vm.localVolume$).toBe("ab(cd)(ef)g", { expectObservable(vm.localVolume$).toBe("ab(cd)(ef)g", {
a: 1, a: 1,
b: 0, b: 0,
c: 0.6, c: 0.6,
d: 0.8, d: 0.8,
e: 0.2, e: 0.2,
f: 0, f: 0,
g: 0.8, g: 0.8,
}); });
}), });
);
}); });
test("toggle fit/contain for a participant's video", async () => { test("toggle fit/contain for a participant's video", () => {
await withRemoteMedia(rtcMembership, {}, {}, (vm) => const vm = createRemoteMedia(rtcMembership, {}, {});
withTestScheduler(({ expectObservable, schedule }) => { withTestScheduler(({ expectObservable, schedule }) => {
schedule("-ab|", { schedule("-ab|", {
a: () => vm.toggleFitContain(), a: () => vm.toggleFitContain(),
b: () => vm.toggleFitContain(), b: () => vm.toggleFitContain(),
}); });
expectObservable(vm.cropVideo$).toBe("abc", { expectObservable(vm.cropVideo$).toBe("abc", {
a: true, a: true,
b: false, b: false,
c: true, c: true,
}); });
}), });
);
}); });
test("local media remembers whether it should always be shown", async () => { test("local media remembers whether it should always be shown", () => {
await withLocalMedia( const vm1 = createLocalMedia(
rtcMembership, rtcMembership,
{}, {},
mockLocalParticipant({}), mockLocalParticipant({}),
mockMediaDevices({}), mockMediaDevices({}),
(vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
expectObservable(vm.alwaysShow$).toBe("ab", { a: true, b: false });
}),
); );
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm1.setAlwaysShow(false) });
expectObservable(vm1.alwaysShow$).toBe("ab", { a: true, b: false });
});
// Next local media should start out *not* always shown // Next local media should start out *not* always shown
await withLocalMedia( const vm2 = createLocalMedia(
rtcMembership, rtcMembership,
{}, {},
mockLocalParticipant({}), mockLocalParticipant({}),
mockMediaDevices({}), mockMediaDevices({}),
(vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
expectObservable(vm.alwaysShow$).toBe("ab", { a: false, b: true });
}),
); );
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm2.setAlwaysShow(true) });
expectObservable(vm2.alwaysShow$).toBe("ab", { a: false, b: true });
});
}); });
test("switch cameras", async () => { test("switch cameras", async () => {
@@ -164,7 +161,7 @@ test("switch cameras", async () => {
const selectVideoInput = vi.fn(); const selectVideoInput = vi.fn();
await withLocalMedia( const vm = createLocalMedia(
rtcMembership, rtcMembership,
{}, {},
mockLocalParticipant({ mockLocalParticipant({
@@ -179,27 +176,26 @@ test("switch cameras", async () => {
select: selectVideoInput, select: selectVideoInput,
}, },
}), }),
async (vm) => {
// Switch to back camera
getValue(vm.switchCamera$)!();
expect(restartTrack).toHaveBeenCalledExactlyOnceWith({
facingMode: "environment",
});
await waitFor(() => {
expect(selectVideoInput).toHaveBeenCalledTimes(1);
expect(selectVideoInput).toHaveBeenCalledWith("back camera");
});
expect(deviceId).toBe("back camera");
// Switch to front camera
getValue(vm.switchCamera$)!();
expect(restartTrack).toHaveBeenCalledTimes(2);
expect(restartTrack).toHaveBeenLastCalledWith({ facingMode: "user" });
await waitFor(() => {
expect(selectVideoInput).toHaveBeenCalledTimes(2);
expect(selectVideoInput).toHaveBeenLastCalledWith("front camera");
});
expect(deviceId).toBe("front camera");
},
); );
// Switch to back camera
getValue(vm.switchCamera$)!();
expect(restartTrack).toHaveBeenCalledExactlyOnceWith({
facingMode: "environment",
});
await waitFor(() => {
expect(selectVideoInput).toHaveBeenCalledTimes(1);
expect(selectVideoInput).toHaveBeenCalledWith("back camera");
});
expect(deviceId).toBe("back camera");
// Switch to front camera
getValue(vm.switchCamera$)!();
expect(restartTrack).toHaveBeenCalledTimes(2);
expect(restartTrack).toHaveBeenLastCalledWith({ facingMode: "user" });
await waitFor(() => {
expect(selectVideoInput).toHaveBeenCalledTimes(2);
expect(selectVideoInput).toHaveBeenLastCalledWith("front camera");
});
expect(deviceId).toBe("front camera");
}); });

View File

@@ -12,7 +12,7 @@ import { axe } from "vitest-axe";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { GridTile } from "./GridTile"; import { GridTile } from "./GridTile";
import { mockRtcMembership, withRemoteMedia } from "../utils/test"; import { mockRtcMembership, createRemoteMedia } from "../utils/test";
import { GridTileViewModel } from "../state/TileViewModel"; import { GridTileViewModel } from "../state/TileViewModel";
import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
import type { CallViewModel } from "../state/CallViewModel"; import type { CallViewModel } from "../state/CallViewModel";
@@ -25,7 +25,7 @@ global.IntersectionObserver = class MockIntersectionObserver {
} as unknown as typeof IntersectionObserver; } as unknown as typeof IntersectionObserver;
test("GridTile is accessible", async () => { test("GridTile is accessible", async () => {
await withRemoteMedia( const vm = createRemoteMedia(
mockRtcMembership("@alice:example.org", "AAAA"), mockRtcMembership("@alice:example.org", "AAAA"),
{ {
rawDisplayName: "Alice", rawDisplayName: "Alice",
@@ -36,41 +36,40 @@ test("GridTile is accessible", async () => {
getTrackPublication: () => getTrackPublication: () =>
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication, ({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
}, },
async (vm) => { );
const fakeRtcSession = {
const fakeRtcSession = {
on: () => {},
off: () => {},
room: {
on: () => {},
off: () => {},
client: {
getUserId: () => null,
getDeviceId: () => null,
on: () => {}, on: () => {},
off: () => {}, off: () => {},
room: { },
on: () => {},
off: () => {},
client: {
getUserId: () => null,
getDeviceId: () => null,
on: () => {},
off: () => {},
},
},
memberships: [],
} as unknown as MatrixRTCSession;
const cVm = {
reactions$: constant({}),
handsRaised$: constant({}),
} as Partial<CallViewModel> as CallViewModel;
const { container } = render(
<ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}>
<GridTile
vm={new GridTileViewModel(constant(vm))}
onOpenProfile={() => {}}
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
focusable={true}
/>
</ReactionsSenderProvider>,
);
expect(await axe(container)).toHaveNoViolations();
// Name should be visible
screen.getByText("Alice");
}, },
memberships: [],
} as unknown as MatrixRTCSession;
const cVm = {
reactions$: constant({}),
handsRaised$: constant({}),
} as Partial<CallViewModel> as CallViewModel;
const { container } = render(
<ReactionsSenderProvider vm={cVm} rtcSession={fakeRtcSession}>
<GridTile
vm={new GridTileViewModel(constant(vm))}
onOpenProfile={() => {}}
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
focusable={true}
/>
</ReactionsSenderProvider>,
); );
expect(await axe(container)).toHaveNoViolations();
// Name should be visible
screen.getByText("Alice");
}); });

View File

@@ -15,8 +15,8 @@ import {
mockLocalParticipant, mockLocalParticipant,
mockMediaDevices, mockMediaDevices,
mockRtcMembership, mockRtcMembership,
withLocalMedia, createLocalMedia,
withRemoteMedia, createRemoteMedia,
} from "../utils/test"; } from "../utils/test";
import { SpotlightTileViewModel } from "../state/TileViewModel"; import { SpotlightTileViewModel } from "../state/TileViewModel";
import { constant } from "../state/Behavior"; import { constant } from "../state/Behavior";
@@ -27,62 +27,53 @@ global.IntersectionObserver = class MockIntersectionObserver {
} as unknown as typeof IntersectionObserver; } as unknown as typeof IntersectionObserver;
test("SpotlightTile is accessible", async () => { test("SpotlightTile is accessible", async () => {
await withRemoteMedia( const vm1 = createRemoteMedia(
mockRtcMembership("@alice:example.org", "AAAA"), mockRtcMembership("@alice:example.org", "AAAA"),
{ {
rawDisplayName: "Alice", rawDisplayName: "Alice",
getMxcAvatarUrl: () => "mxc://adfsg", getMxcAvatarUrl: () => "mxc://adfsg",
}, },
{}, {},
async (vm1) => {
await withLocalMedia(
mockRtcMembership("@bob:example.org", "BBBB"),
{
rawDisplayName: "Bob",
getMxcAvatarUrl: () => "mxc://dlskf",
},
mockLocalParticipant({}),
mockMediaDevices({}),
async (vm2) => {
const user = userEvent.setup();
const toggleExpanded = vi.fn();
const { container } = render(
<SpotlightTile
vm={
new SpotlightTileViewModel(
constant([vm1, vm2]),
constant(false),
)
}
targetWidth={300}
targetHeight={200}
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
focusable={true}
/>,
);
expect(await axe(container)).toHaveNoViolations();
// Alice should be in the spotlight, with her name and avatar on the
// first page
screen.getByText("Alice");
const aliceAvatar = screen.getByRole("img");
expect(screen.queryByRole("button", { name: "common.back" })).toBe(
null,
);
// Bob should be out of the spotlight, and therefore invisible
expect(isInaccessible(screen.getByText("Bob"))).toBe(true);
// Now navigate to Bob
await user.click(screen.getByRole("button", { name: "Next" }));
screen.getByText("Bob");
expect(screen.getByRole("img")).not.toBe(aliceAvatar);
expect(isInaccessible(screen.getByText("Alice"))).toBe(true);
// Can toggle whether the tile is expanded
await user.click(screen.getByRole("button", { name: "Expand" }));
expect(toggleExpanded).toHaveBeenCalled();
},
);
},
); );
const vm2 = createLocalMedia(
mockRtcMembership("@bob:example.org", "BBBB"),
{
rawDisplayName: "Bob",
getMxcAvatarUrl: () => "mxc://dlskf",
},
mockLocalParticipant({}),
mockMediaDevices({}),
);
const user = userEvent.setup();
const toggleExpanded = vi.fn();
const { container } = render(
<SpotlightTile
vm={new SpotlightTileViewModel(constant([vm1, vm2]), constant(false))}
targetWidth={300}
targetHeight={200}
expanded={false}
onToggleExpanded={toggleExpanded}
showIndicators
focusable={true}
/>,
);
expect(await axe(container)).toHaveNoViolations();
// Alice should be in the spotlight, with her name and avatar on the
// first page
screen.getByText("Alice");
const aliceAvatar = screen.getByRole("img");
expect(screen.queryByRole("button", { name: "common.back" })).toBe(null);
// Bob should be out of the spotlight, and therefore invisible
expect(isInaccessible(screen.getByText("Bob"))).toBe(true);
// Now navigate to Bob
await user.click(screen.getByRole("button", { name: "Next" }));
screen.getByText("Bob");
expect(screen.getByRole("img")).not.toBe(aliceAvatar);
expect(isInaccessible(screen.getByText("Alice"))).toBe(true);
// Can toggle whether the tile is expanded
await user.click(screen.getByRole("button", { name: "Expand" }));
expect(toggleExpanded).toHaveBeenCalled();
}); });

View File

@@ -268,14 +268,13 @@ export function mockLocalParticipant(
} as Partial<LocalParticipant> as LocalParticipant; } as Partial<LocalParticipant> as LocalParticipant;
} }
export async function withLocalMedia( export function createLocalMedia(
localRtcMember: CallMembership, localRtcMember: CallMembership,
roomMember: Partial<RoomMember>, roomMember: Partial<RoomMember>,
localParticipant: LocalParticipant, localParticipant: LocalParticipant,
mediaDevices: MediaDevices, mediaDevices: MediaDevices,
continuation: (vm: LocalUserMediaViewModel) => void | Promise<void>, ): LocalUserMediaViewModel {
): Promise<void> { return new LocalUserMediaViewModel(
const vm = new LocalUserMediaViewModel(
testScope(), testScope(),
"local", "local",
mockMatrixRoomMember(localRtcMember, roomMember), mockMatrixRoomMember(localRtcMember, roomMember),
@@ -290,8 +289,6 @@ export async function withLocalMedia(
constant(null), constant(null),
constant(null), constant(null),
); );
// TODO: Simplify to just return the view model
await continuation(vm);
} }
export function mockRemoteParticipant( export function mockRemoteParticipant(
@@ -307,14 +304,13 @@ export function mockRemoteParticipant(
} as RemoteParticipant; } as RemoteParticipant;
} }
export async function withRemoteMedia( export function createRemoteMedia(
localRtcMember: CallMembership, localRtcMember: CallMembership,
roomMember: Partial<RoomMember>, roomMember: Partial<RoomMember>,
participant: Partial<RemoteParticipant>, participant: Partial<RemoteParticipant>,
continuation: (vm: RemoteUserMediaViewModel) => void | Promise<void>, ): RemoteUserMediaViewModel {
): Promise<void> {
const remoteParticipant = mockRemoteParticipant(participant); const remoteParticipant = mockRemoteParticipant(participant);
const vm = new RemoteUserMediaViewModel( return new RemoteUserMediaViewModel(
testScope(), testScope(),
"remote", "remote",
mockMatrixRoomMember(localRtcMember, roomMember), mockMatrixRoomMember(localRtcMember, roomMember),
@@ -329,8 +325,6 @@ export async function withRemoteMedia(
constant(null), constant(null),
constant(null), constant(null),
); );
// TODO: Simplify to just return the view model
await continuation(vm);
} }
export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void { export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void {