mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-17 04:47:02 +00:00
Add tests
This commit is contained in:
@@ -49,7 +49,7 @@ test("Sign up a new account, then login, then logout", async ({ browser }) => {
|
||||
|
||||
// logout
|
||||
await returningUserPage.getByTestId("usermenu_open").click();
|
||||
await returningUserPage.locator('[data-test-id="usermenu_logout"]').click();
|
||||
await returningUserPage.locator('[data-testid="usermenu_logout"]').click();
|
||||
|
||||
await expect(
|
||||
returningUserPage.getByRole("link", { name: "Log In" }),
|
||||
|
||||
@@ -119,7 +119,7 @@ export const UserMenu: FC<Props> = ({
|
||||
key={key}
|
||||
Icon={Icon}
|
||||
label={label}
|
||||
data-test-id={dataTestid}
|
||||
data-testid={dataTestid}
|
||||
onSelect={() => onAction(key)}
|
||||
/>
|
||||
))}
|
||||
|
||||
104
src/livekit/MatrixAudioRenderer.test.tsx
Normal file
104
src/livekit/MatrixAudioRenderer.test.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
getTrackReferenceId,
|
||||
type TrackReference,
|
||||
} from "@livekit/components-core";
|
||||
import { type RemoteAudioTrack } from "livekit-client";
|
||||
import { type ReactNode } from "react";
|
||||
import { useTracks } from "@livekit/components-react";
|
||||
|
||||
import { testAudioContext } from "../useAudioContext.test";
|
||||
import * as MediaDevicesContext from "./MediaDevicesContext";
|
||||
import { MatrixAudioRenderer } from "./MatrixAudioRenderer";
|
||||
import { mockTrack } from "../utils/test";
|
||||
|
||||
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
vi.mock("@livekit/components-react", async (importOriginal) => {
|
||||
return {
|
||||
...(await importOriginal()), // this will only affect "foo" outside of the original module
|
||||
AudioTrack: (props: { trackRef: TrackReference }): ReactNode => {
|
||||
return (
|
||||
<audio data-testid={"audio"}>
|
||||
{getTrackReferenceId(props.trackRef)}
|
||||
</audio>
|
||||
);
|
||||
},
|
||||
useTracks: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const tracks = [mockTrack("test:123")];
|
||||
vi.mocked(useTracks).mockReturnValue(tracks);
|
||||
|
||||
it("should render for member", () => {
|
||||
const { container, queryAllByTestId } = render(
|
||||
<MatrixAudioRenderer
|
||||
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
|
||||
/>,
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
expect(queryAllByTestId("audio")).toHaveLength(1);
|
||||
});
|
||||
it("should not render without member", () => {
|
||||
const { container, queryAllByTestId } = render(
|
||||
<MatrixAudioRenderer
|
||||
members={[{ sender: "othermember", deviceId: "123" }] as CallMembership[]}
|
||||
/>,
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
expect(queryAllByTestId("audio")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not setup audioContext gain and pan if there is no need to.", () => {
|
||||
render(
|
||||
<MatrixAudioRenderer
|
||||
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
|
||||
/>,
|
||||
);
|
||||
const audioTrack = tracks[0].publication.track! as RemoteAudioTrack;
|
||||
|
||||
expect(audioTrack.setAudioContext).toHaveBeenCalledTimes(1);
|
||||
expect(audioTrack.setAudioContext).toHaveBeenCalledWith(undefined);
|
||||
expect(audioTrack.setWebAudioPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(audioTrack.setWebAudioPlugins).toHaveBeenCalledWith([]);
|
||||
|
||||
expect(testAudioContext.gain.gain.value).toEqual(1);
|
||||
expect(testAudioContext.pan.pan.value).toEqual(0);
|
||||
});
|
||||
it("should setup audioContext gain and pan", () => {
|
||||
vi.spyOn(MediaDevicesContext, "useEarpieceAudioConfig").mockReturnValue({
|
||||
pan: 1,
|
||||
volume: 0.1,
|
||||
});
|
||||
render(
|
||||
<MatrixAudioRenderer
|
||||
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const audioTrack = tracks[0].publication.track! as RemoteAudioTrack;
|
||||
expect(audioTrack.setAudioContext).toHaveBeenCalled();
|
||||
expect(audioTrack.setWebAudioPlugins).toHaveBeenCalled();
|
||||
|
||||
expect(testAudioContext.gain.gain.value).toEqual(0.1);
|
||||
expect(testAudioContext.pan.pan.value).toEqual(1);
|
||||
});
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
audioInput as audioInputSetting,
|
||||
audioOutput as audioOutputSetting,
|
||||
videoInput as videoInputSetting,
|
||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||
type Setting,
|
||||
alwaysShowIphoneEarpieceSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
export const EARPIECE_CONFIG_ID = "earpiece-id";
|
||||
|
||||
@@ -21,11 +21,7 @@ import { ConnectionState, type LocalParticipant } from "livekit-client";
|
||||
import { of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import {
|
||||
RoomAudioRenderer,
|
||||
RoomContext,
|
||||
useLocalParticipant,
|
||||
} from "@livekit/components-react";
|
||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
|
||||
import { type MuteStates } from "./MuteStates";
|
||||
@@ -48,6 +44,8 @@ import {
|
||||
} from "../settings/settings";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
// import { testAudioContext } from "../useAudioContext.test";
|
||||
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
||||
|
||||
// vi.hoisted(() => {
|
||||
// localStorage = {} as unknown as Storage;
|
||||
@@ -65,6 +63,7 @@ vi.mock("../tile/GridTile");
|
||||
vi.mock("../tile/SpotlightTile");
|
||||
vi.mock("@livekit/components-react");
|
||||
vi.mock("../e2ee/sharedKeyManagement");
|
||||
vi.mock("../livekit/MatrixAudioRenderer");
|
||||
vi.mock("react-use-measure", () => ({
|
||||
default: (): [() => void, object] => [(): void => {}, {}],
|
||||
}));
|
||||
@@ -81,13 +80,15 @@ const roomMembers = new Map([carol].map((p) => [p.userId, p]));
|
||||
|
||||
const roomId = "!foo:bar";
|
||||
let useRoomEncryptionSystemMock: MockedFunction<typeof useRoomEncryptionSystem>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// RoomAudioRenderer is tested separately.
|
||||
|
||||
// MatrixAudioRenderer is tested separately.
|
||||
(
|
||||
RoomAudioRenderer as MockedFunction<typeof RoomAudioRenderer>
|
||||
MatrixAudioRenderer as MockedFunction<typeof MatrixAudioRenderer>
|
||||
).mockImplementation((_props) => {
|
||||
return <div>mocked: RoomAudioRenderer</div>;
|
||||
return <div>mocked: MatrixAudioRenderer</div>;
|
||||
});
|
||||
(
|
||||
useLocalParticipant as MockedFunction<typeof useLocalParticipant>
|
||||
@@ -98,7 +99,6 @@ beforeEach(() => {
|
||||
localParticipant: localRtcMember as unknown as LocalParticipant,
|
||||
}) as unknown as ReturnType<typeof useLocalParticipant>,
|
||||
);
|
||||
|
||||
useRoomEncryptionSystemMock =
|
||||
useRoomEncryptionSystem as typeof useRoomEncryptionSystemMock;
|
||||
useRoomEncryptionSystemMock.mockReturnValue({ kind: E2eeType.NONE });
|
||||
|
||||
@@ -79,6 +79,7 @@ function mockDevices(available: Map<string, DeviceLabel>): MediaDevice {
|
||||
selectedId: "",
|
||||
selectedGroupId: "",
|
||||
select: (): void => {},
|
||||
useAsEarpiece: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
class="header filler"
|
||||
/>
|
||||
<div>
|
||||
mocked: RoomAudioRenderer
|
||||
mocked: MatrixAudioRenderer
|
||||
</div>
|
||||
<div
|
||||
class="scrollingGrid grid"
|
||||
|
||||
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test, vitest, afterEach } from "vitest";
|
||||
import { expect, vi, afterEach, beforeEach, test } from "vitest";
|
||||
import { type FC } from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import userEvent, { type UserEvent } from "@testing-library/user-event";
|
||||
|
||||
import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext";
|
||||
import { useAudioContext } from "./useAudioContext";
|
||||
@@ -39,61 +39,73 @@ const TestComponent: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
class MockAudioContext {
|
||||
public static testContext: MockAudioContext;
|
||||
|
||||
public constructor() {
|
||||
MockAudioContext.testContext = this;
|
||||
}
|
||||
|
||||
public gain = vitest.mocked(
|
||||
{
|
||||
connect: () => {},
|
||||
gain: {
|
||||
setValueAtTime: vitest.fn(),
|
||||
},
|
||||
const gainNode = vi.mocked(
|
||||
{
|
||||
connect: (node: AudioNode) => node,
|
||||
gain: {
|
||||
setValueAtTime: vi.fn(),
|
||||
value: 1,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
public setSinkId = vitest.fn().mockResolvedValue(undefined);
|
||||
public decodeAudioData = vitest.fn().mockReturnValue(1);
|
||||
public createBufferSource = vitest.fn().mockReturnValue(
|
||||
vitest.mocked({
|
||||
},
|
||||
true,
|
||||
);
|
||||
const panNode = vi.mocked(
|
||||
{
|
||||
connect: (node: AudioNode) => node,
|
||||
pan: {
|
||||
setValueAtTime: vi.fn(),
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
/**
|
||||
* A shared audio context test instance.
|
||||
* It can also be used to mock the `AudioContext` constructor in tests:
|
||||
* `vi.stubGlobal("AudioContext", () => testAudioContext);`
|
||||
*/
|
||||
export const testAudioContext = {
|
||||
gain: gainNode,
|
||||
pan: panNode,
|
||||
setSinkId: vi.fn().mockResolvedValue(undefined),
|
||||
decodeAudioData: vi.fn().mockReturnValue(1),
|
||||
createBufferSource: vi.fn().mockReturnValue(
|
||||
vi.mocked({
|
||||
connect: (v: unknown) => v,
|
||||
start: () => {},
|
||||
addEventListener: (_name: string, cb: () => void) => cb(),
|
||||
}),
|
||||
);
|
||||
public createGain = vitest.fn().mockReturnValue(this.gain);
|
||||
public close = vitest.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
),
|
||||
createGain: vi.fn().mockReturnValue(gainNode),
|
||||
createStereoPanner: vi.fn().mockReturnValue(panNode),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
||||
|
||||
let user: UserEvent;
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.unstubAllGlobals();
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("can play a single sound", async () => {
|
||||
const user = userEvent.setup();
|
||||
vitest.stubGlobal("AudioContext", MockAudioContext);
|
||||
const { findByText } = render(<TestComponent />);
|
||||
await user.click(await findByText("Valid sound"));
|
||||
expect(
|
||||
MockAudioContext.testContext.createBufferSource,
|
||||
).toHaveBeenCalledOnce();
|
||||
expect(testAudioContext.createBufferSource).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("will ignore sounds that are not registered", async () => {
|
||||
const user = userEvent.setup();
|
||||
vitest.stubGlobal("AudioContext", MockAudioContext);
|
||||
const { findByText } = render(<TestComponent />);
|
||||
await user.click(await findByText("Invalid sound"));
|
||||
expect(
|
||||
MockAudioContext.testContext.createBufferSource,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("will use the correct device", () => {
|
||||
vitest.stubGlobal("AudioContext", MockAudioContext);
|
||||
render(
|
||||
<MediaDevicesContext.Provider
|
||||
value={{
|
||||
@@ -103,6 +115,7 @@ test("will use the correct device", () => {
|
||||
selectedGroupId: "",
|
||||
available: new Map(),
|
||||
select: () => {},
|
||||
useAsEarpiece: false,
|
||||
},
|
||||
videoInput: deviceStub,
|
||||
startUsingDeviceNames: () => {},
|
||||
@@ -112,21 +125,46 @@ test("will use the correct device", () => {
|
||||
<TestComponent />
|
||||
</MediaDevicesContext.Provider>,
|
||||
);
|
||||
expect(
|
||||
MockAudioContext.testContext.createBufferSource,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(MockAudioContext.testContext.setSinkId).toHaveBeenCalledWith(
|
||||
"chosen-device",
|
||||
);
|
||||
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
|
||||
expect(testAudioContext.setSinkId).toHaveBeenCalledWith("chosen-device");
|
||||
});
|
||||
|
||||
test("will use the correct volume level", async () => {
|
||||
const user = userEvent.setup();
|
||||
vitest.stubGlobal("AudioContext", MockAudioContext);
|
||||
soundEffectVolumeSetting.setValue(0.33);
|
||||
const { findByText } = render(<TestComponent />);
|
||||
await user.click(await findByText("Valid sound"));
|
||||
expect(
|
||||
MockAudioContext.testContext.gain.gain.setValueAtTime,
|
||||
).toHaveBeenCalledWith(0.33, 0);
|
||||
expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith(
|
||||
0.33,
|
||||
0,
|
||||
);
|
||||
expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(0, 0);
|
||||
});
|
||||
|
||||
test("will use the pan if earpice is selected", async () => {
|
||||
const { findByText } = render(
|
||||
<MediaDevicesContext.Provider
|
||||
value={{
|
||||
audioInput: deviceStub,
|
||||
audioOutput: {
|
||||
selectedId: "chosen-device",
|
||||
selectedGroupId: "",
|
||||
available: new Map(),
|
||||
select: () => {},
|
||||
useAsEarpiece: true,
|
||||
},
|
||||
videoInput: deviceStub,
|
||||
startUsingDeviceNames: () => {},
|
||||
stopUsingDeviceNames: () => {},
|
||||
}}
|
||||
>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext.Provider>,
|
||||
);
|
||||
await user.click(await findByText("Valid sound"));
|
||||
expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(1, 0);
|
||||
|
||||
expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith(
|
||||
soundEffectVolumeSetting.getValue() * 0.1,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,12 +27,14 @@ import {
|
||||
type RemoteParticipant,
|
||||
type RemoteTrackPublication,
|
||||
type Room as LivekitRoom,
|
||||
Track,
|
||||
} from "livekit-client";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
type RoomAndToDeviceEvents,
|
||||
type RoomAndToDeviceEventsHandlerMap,
|
||||
} from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import { type TrackReference } from "@livekit/components-core";
|
||||
|
||||
import {
|
||||
LocalUserMediaViewModel,
|
||||
@@ -309,3 +311,24 @@ export class MockRTCSession extends TypedEventEmitter<
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const mockTrack = (identity: string): TrackReference =>
|
||||
({
|
||||
participant: {
|
||||
identity,
|
||||
},
|
||||
publication: {
|
||||
kind: Track.Kind.Audio,
|
||||
source: "mic",
|
||||
trackSid: "123",
|
||||
track: {
|
||||
attach: vi.fn(),
|
||||
detach: vi.fn(),
|
||||
setAudioContext: vi.fn(),
|
||||
setWebAudioPlugins: vi.fn(),
|
||||
setVolume: vi.fn(),
|
||||
},
|
||||
},
|
||||
track: {},
|
||||
source: {},
|
||||
}) as unknown as TrackReference;
|
||||
|
||||
Reference in New Issue
Block a user