tests first batch

This commit is contained in:
Timo K
2025-11-13 11:35:37 +01:00
parent c7f50b53f5
commit 0115242a2b
10 changed files with 98 additions and 198 deletions

View File

@@ -43,9 +43,6 @@ import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
import { MediaDevicesContext } from "../MediaDevicesContext";
import { HeaderStyle } from "../UrlParams";
// vi.hoisted(() => {
// localStorage = {} as unknown as Storage;
// });
vi.hoisted(
() =>
(global.ImageData = class MockImageData {
@@ -109,6 +106,7 @@ function createInCallView(): RenderResult & {
getUserId: () => localRtcMember.userId,
getDeviceId: () => localRtcMember.deviceId,
getRoom: (rId) => (rId === roomId ? room : null),
getDomain: () => "example.com",
} as Partial<MatrixClient> as MatrixClient;
const room = mockMatrixRoom({
relations: {
@@ -119,7 +117,8 @@ function createInCallView(): RenderResult & {
} as unknown as RelationsContainer,
client,
roomId,
getMember: (userId) => roomMembers.get(userId) ?? null,
// getMember: (userId) => roomMembers.get(userId) ?? null,
getMembers: () => Array.from(roomMembers.values()),
getMxcAvatarUrl: () => null,
hasEncryptionStateEvent: vi.fn().mockReturnValue(true),
getCanonicalAlias: () => null,

View File

@@ -118,16 +118,16 @@ export interface ActiveCallProps
}
export const ActiveCall: FC<ActiveCallProps> = (props) => {
const mediaDevices = useMediaDevices();
const [vm, setVm] = useState<CallViewModel | null>(null);
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
useUrlParams();
const urlParams = useUrlParams();
const mediaDevices = useMediaDevices();
const trackProcessorState$ = useTrackProcessorObservable$();
useEffect(() => {
const scope = new ObservableScope();
const reactionsReader = new ReactionsReader(scope, props.rtcSession);
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
urlParams;
const vm = new CallViewModel(
scope,
props.rtcSession,
@@ -152,13 +152,11 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
}, [
props.rtcSession,
props.matrixRoom,
mediaDevices,
props.muteStates,
props.e2eeSystem,
autoLeaveWhenOthersLeft,
sendNotificationType,
waitForCallPickup,
props.onLeft,
urlParams,
mediaDevices,
trackProcessorState$,
]);

View File

@@ -49,66 +49,26 @@ exports[`InCallView > rendering > renders 1`] = `
</svg>
</span>
</div>
<div
class="participantsLine"
>
<svg
aria-label="Participants"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.175 13.825Q10.35 15 12 15t2.825-1.175T16 11t-1.175-2.825T12 7 9.175 8.175 8 11t1.175 2.825m4.237-1.412A1.93 1.93 0 0 1 12 13q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 11q0-.825.588-1.412A1.93 1.93 0 0 1 12 9q.825 0 1.412.588Q14 10.175 14 11t-.588 1.412"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0"
/>
<path
d="M16.23 18.792a13 13 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0q-.73.18-1.455.455a8 8 0 0 1-1.729-1.454q1.336-.618 2.709-.95A13.8 13.8 0 0 1 12 16q1.65 0 3.25.387 1.373.333 2.709.95a8 8 0 0 1-1.73 1.455"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41"
data-testid="roomHeader_participants_count"
>
2
</span>
</div>
</div>
</div>
<div
class="nav rightNav"
/>
</header>
<div>
mocked: MatrixAudioRenderer
</div>
<div
class="scrollingGrid grid"
>
<div
class="layer"
>
<div
class="container slot"
data-id="1"
>
<div
class="slot local slot"
data-block-alignment="start"
data-id="0"
data-inline-alignment="end"
/>
</div>
</div>
class="scrolling"
style="--gap: 20px; --width: NaNpx; --height: NaNpx;"
/>
</div>
<div
class="fixedGrid grid"
>
<div />
<div
class="fixed"
/>
</div>
<div
class="bg animate"

View File

@@ -311,11 +311,14 @@ export function withCallViewModel(
public getDeviceId(): string {
return localRtcMember.deviceId;
}
public getDomain(): string {
return "example.com";
}
public getSyncState(): SyncState {
return syncState;
}
})() as Partial<MatrixClient> as MatrixClient,
getMember: (userId) => roomMembers.get(userId) ?? null,
getMembers: () => Array.from(roomMembers.values()),
});
const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$);
const participantsSpy = vi

View File

@@ -367,21 +367,18 @@ describe("Start connection states", () => {
});
});
function fakeRemoteLivekitParticipant(id: string): RemoteParticipant {
function fakeRemoteLivekitParticipant(
id: string,
publications: number = 1,
): RemoteParticipant {
return {
identity: id,
getTrackPublications: () => Array(publications),
} as unknown as RemoteParticipant;
}
function fakeRtcMemberShip(userId: string, deviceId: string): CallMembership {
return {
userId,
deviceId,
} as unknown as CallMembership;
}
describe("Publishing participants observations", () => {
it("should emit the list of publishing participants", async () => {
it("should emit the list of publishing participants", () => {
setupTest();
const connection = setupRemoteConnection();
@@ -406,107 +403,36 @@ describe("Publishing participants observations", () => {
// on this connection.
let participants: RemoteParticipant[] = [
fakeRemoteLivekitParticipant("@alice:example.org:DEV000"),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222"),
fakeRemoteLivekitParticipant("@dan:example.org:DEV333"),
fakeRemoteLivekitParticipant("@alice:example.org:DEV000", 0),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 0),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222", 0),
fakeRemoteLivekitParticipant("@dan:example.org:DEV333", 0),
];
// Let's simulate 3 members on the livekitRoom
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockImplementation(
() => new Map(participants.map((p) => [p.identity, p])),
);
for (const participant of participants) {
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
}
participants.forEach((p) =>
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, p),
);
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(0);
const otherFocus: LivekitTransport = {
livekit_alias: "!roomID:example.org",
livekit_service_url: "https://other-matrix-rtc.example.org/livekit/jwt",
type: "livekit",
};
const rtcMemberships = [
// Say bob is on the same focus
{
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
transport: livekitFocus,
},
// Alice and carol is on a different focus
{
membership: fakeRtcMemberShip("@alice:example.org", "DEV000"),
transport: otherFocus,
},
{
membership: fakeRtcMemberShip("@carol:example.org", "DEV222"),
transport: otherFocus,
},
// NO DAVE YET
participants = [
fakeRemoteLivekitParticipant("@alice:example.org:DEV000", 1),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 1),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222", 1),
fakeRemoteLivekitParticipant("@dan:example.org:DEV333", 2),
];
// signal this change in rtc memberships
fakeMembershipsFocusMap$.next(rtcMemberships);
// We should have bob has a publisher now
await bobIsAPublisher.promise;
const publishers = observedPublishers.pop();
expect(publishers?.length).toEqual(1);
expect(publishers?.[0].identity).toEqual("@bob:example.org:DEV111");
// Now let's make dan join the rtc memberships
rtcMemberships.push({
membership: fakeRtcMemberShip("@dan:example.org", "DEV333"),
transport: livekitFocus,
});
fakeMembershipsFocusMap$.next(rtcMemberships);
// We should have bob and dan has publishers now
await danIsAPublisher.promise;
const twoPublishers = observedPublishers.pop();
expect(twoPublishers?.length).toEqual(2);
expect(
twoPublishers?.some((p) => p.identity === "@bob:example.org:DEV111"),
).toBeTruthy();
expect(
twoPublishers?.some((p) => p.identity === "@dan:example.org:DEV333"),
).toBeTruthy();
// Now let's make bob leave the livekit room
participants = participants.filter(
(p) => p.identity !== "@bob:example.org:DEV111",
);
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
fakeRoomEventEmiter.emit(
RoomEvent.ParticipantDisconnected,
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
participants.forEach((p) =>
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, p),
);
// TODO: evaluate this test. It looks like this is not the task of the Connection anymore. Valere?
// const updatedPublishers = observedPublishers.pop();
// // Bob is not connected to the room but he is still in the rtc memberships declaring that
// // he is using that focus to publish, so he should still appear as a publisher
// expect(updatedPublishers?.length).toEqual(2);
// const pp = updatedPublishers?.find((p) =>
// p.identity.startsWith("@bob:example.org"),
// );
// expect(pp).toBeDefined();
// expect(pp!).not.toBeDefined();
// expect(
// updatedPublishers?.some(
// (p) => p.participant?.identity === "@dan:example.org:DEV333",
// ),
// ).toBeTruthy();
// // Now if bob is not in the rtc memberships, he should disappear
// const noBob = rtcMemberships.filter(
// ({ membership }) => membership.userId !== "@bob:example.org",
// );
// fakeMembershipsFocusMap$.next(noBob);
// expect(observedPublishers.pop()?.length).toEqual(1);
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(4);
});
it("should be scoped to parent scope", (): void => {
@@ -523,12 +449,12 @@ describe("Publishing participants observations", () => {
onTestFinished(() => s.unsubscribe());
let participants: RemoteParticipant[] = [
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 0),
];
// Let's simulate 3 members on the livekitRoom
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockImplementation(
() => new Map(participants.map((p) => [p.identity, p])),
);
for (const participant of participants) {
@@ -538,15 +464,11 @@ describe("Publishing participants observations", () => {
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(0);
const rtcMemberships = [
// Say bob is on the same focus
{
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
transport: livekitFocus,
},
];
// signal this change in rtc memberships
fakeMembershipsFocusMap$.next(rtcMemberships);
participants = [fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 1)];
for (const participant of participants) {
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
}
// We should have bob has a publisher now
const publishers = observedPublishers.pop();
@@ -561,9 +483,7 @@ describe("Publishing participants observations", () => {
participants = participants.filter(
(p) => p.identity !== "@bob:example.org:DEV111",
);
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
fakeRoomEventEmiter.emit(
RoomEvent.ParticipantDisconnected,
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),

View File

@@ -223,9 +223,10 @@ export class Connection {
],
}).pipe(
map((participants) => {
return participants.filter(
const partsFiltered = participants.filter(
(participant) => participant.getTrackPublications().length > 0,
);
return partsFiltered;
}),
),
[],

View File

@@ -51,7 +51,7 @@ beforeEach(() => {
(transport: LivekitTransport, scope: ObservableScope) => {
const mockConnection = {
transport,
participantsWithTrack$: new BehaviorSubject([]),
remoteParticipantsWithTracks$: new BehaviorSubject([]),
} as unknown as Connection;
vi.mocked(mockConnection).start = vi.fn();
vi.mocked(mockConnection).stop = vi.fn();
@@ -190,7 +190,7 @@ describe("connections$ stream", () => {
});
describe("connectionManagerData$ stream", () => {
// Used in test to control fake connections' participantsWithTrack$ streams
// Used in test to control fake connections' remoteParticipantsWithTracks$ streams
let fakePublishingParticipantsStreams: Map<
string,
Behavior<LivekitParticipant[]>
@@ -222,7 +222,8 @@ describe("connectionManagerData$ stream", () => {
>([]);
const mockConnection = {
transport,
participantsWithTrack$: getPublishingParticipantsFor(transport),
remoteParticipantsWithTracks$:
getPublishingParticipantsFor(transport),
} as unknown as Connection;
vi.mocked(mockConnection).start = vi.fn();
vi.mocked(mockConnection).stop = vi.fn();

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { describe, expect, it } from "vitest";
import { BehaviorSubject, combineLatest, timer } from "rxjs";
import { BehaviorSubject, combineLatest, of, Subject } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger";
import {
@@ -60,19 +60,6 @@ describe("Epoch", () => {
});
});
});
it("obs", () => {
const nothing = Symbol("nothing");
const scope = new ObservableScope();
const sb$ = new BehaviorSubject("initial");
const su$ = new BehaviorSubject(undefined);
expect(sb$.value).toBe("initial");
expect(su$.value).toBe(undefined);
expect(su$.value === nothing).toBe(false);
const a$ = timer(10);
scope.behavior(a$, undefined);
});
it("diamonds emits in a predictable order", () => {
const sb$ = new BehaviorSubject("initial");
@@ -97,4 +84,21 @@ describe("Epoch", () => {
sb$.next("updated");
sb$.next("ANOTERUPDATE");
});
it("behavior test", () => {
const scope = new ObservableScope();
const s$ = new Subject();
const behavior$ = scope.behavior(s$, 0);
behavior$.subscribe((value) => {
logger.log(`Received value: ${value}`);
});
s$.next(1);
s$.next(2);
s$.next(3);
s$.next(3);
s$.next(3);
s$.next(3);
s$.next(3);
s$.complete();
});
});

View File

@@ -21,6 +21,7 @@ import {
} from "rxjs";
import { type Behavior } from "./Behavior";
import { logger } from "matrix-js-sdk/lib/logger";
type MonoTypeOperator = <T>(o: Observable<T>) => Observable<T>;
@@ -73,14 +74,22 @@ export class ObservableScope {
// they will no longer re-emit their current value upon subscription. We want
// to support Observables that complete (for example `of({})`), so we have to
// take care to not propagate the completion event.
setValue$.pipe(this.bind(), distinctUntilChanged()).subscribe({
next(value) {
subject$.next(value);
},
error(err: unknown) {
subject$.error(err);
},
});
setValue$
.pipe(
this.bind(),
distinctUntilChanged((a, b) => {
logger.log("distinctUntilChanged", a, b);
return a === b;
}),
)
.subscribe({
next(value) {
subject$.next(value);
},
error(err: unknown) {
subject$.error(err);
},
});
if (subject$.value === nothing)
throw new Error("Behavior failed to synchronously emit an initial value");
return subject$ as Behavior<T>;

View File

@@ -24,6 +24,7 @@ import {
type CallViewModelOptions,
} from "../state/CallViewModel/CallViewModel";
import {
mockConfig,
mockLivekitRoom,
mockLocalParticipant,
mockMatrixRoom,
@@ -36,6 +37,8 @@ import { aliceRtcMember, localRtcMember } from "./test-fixtures";
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
import { constant } from "../state/Behavior";
mockConfig({ livekit: { livekit_service_url: "https://example.com" } });
export function getBasicRTCSession(
members: RoomMember[],
initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember],
@@ -57,6 +60,7 @@ export function getBasicRTCSession(
getUserId: () => localRtcMember.userId,
getDeviceId: () => localRtcMember.deviceId,
getSyncState: () => SyncState.Syncing,
getDomain: () => null,
sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
decryptEventIfNeeded: vitest.fn().mockResolvedValue(undefined),
@@ -78,6 +82,7 @@ export function getBasicRTCSession(
),
} as Partial<MatrixClient> as MatrixClient,
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
getMembers: () => Array.from(matrixRoomMembers.values()),
roomId: matrixRoomId,
on: vitest
.fn()