Prepare for Element Call integration (#9224)

* Improve accessibility and testability of Tooltip

Adding a role to Tooltip was motivated by React Testing Library's
reliance on accessibility-related attributes to locate elements.

* Make the ReadyWatchingStore constructor safer

The ReadyWatchingStore constructor previously had a chance to
immediately call onReady, which was dangerous because it was potentially
calling the derived class's onReady at a point when the derived class
hadn't even finished construction yet. In normal usage, I guess this
never was a problem, but it was causing some of the tests I was writing
to crash. This is solved by separating out the onReady call into a start
method.

* Rename 1:1 call components to 'LegacyCall'

to reflect the fact that they're slated for removal, and to not clash
with the new Call code.

* Refactor VideoChannelStore into Call and CallStore

Call is an abstract class that currently only has a Jitsi
implementation, but this will make it easy to later add an Element Call
implementation.

* Remove WidgetReady, ClientReady, and ForceHangupCall hacks

These are no longer used by the new Jitsi call implementation, and can
be removed.

* yarn i18n

* Delete call map entries instead of inserting nulls

* Allow multiple active calls and consolidate call listeners

* Fix a race condition when creating a video room

* Un-hardcode the media device fallback labels

* Apply misc code review fixes

* yarn i18n

* Disconnect from calls more politely on logout

* Fix some strict mode errors

* Fix another updateRoom race condition
This commit is contained in:
Robin 2022-08-30 15:13:39 -04:00 committed by GitHub
parent 50f6986f6c
commit 0d6a550c33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 2573 additions and 2157 deletions

View file

@ -14,51 +14,91 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { stubClient, stubVideoChannelStore, mkRoom } from "../../../test-utils";
import { mocked, MockedObject } from "jest-mock";
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget } from "matrix-widget-api";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { ClientWidgetApi } from "matrix-widget-api";
import { stubClient, setupAsyncStoreWithClient, useMockedCalls, MockedCall } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import { SortAlgorithm, ListAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
import "../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles
import { Algorithm } from "../../../../src/stores/room-list/algorithms/Algorithm";
import { CallStore } from "../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
describe("Algorithm", () => {
let videoChannelStore;
let algorithm;
let textRoom;
let videoRoom;
useMockedCalls();
let client: MockedObject<MatrixClient>;
let algorithm: Algorithm;
beforeEach(() => {
stubClient();
const cli = MatrixClientPeg.get();
client = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
videoChannelStore = stubVideoChannelStore();
algorithm = new Algorithm();
algorithm.start();
textRoom = mkRoom(cli, "!text:example.org");
videoRoom = mkRoom(cli, "!video:example.org");
videoRoom.isElementVideoRoom.mockReturnValue(true);
algorithm.populateTags(
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
);
algorithm.setKnownRooms([textRoom, videoRoom]);
});
afterEach(() => {
algorithm.stop();
});
it("sticks video rooms to the top when they connect", () => {
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
videoChannelStore.connect("!video:example.org");
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
});
it("sticks rooms with calls to the top when they're connected", async () => {
const room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const roomWithCall = new Room("!2:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
it("unsticks video rooms from the top when they disconnect", () => {
videoChannelStore.connect("!video:example.org");
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
videoChannelStore.disconnect();
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
client.getRoom.mockImplementation(roomId => {
switch (roomId) {
case room.roomId: return room;
case roomWithCall.roomId: return roomWithCall;
default: return null;
}
});
client.getRooms.mockReturnValue([room, roomWithCall]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
client.reEmitter.reEmit(roomWithCall, [RoomStateEvent.Events]);
for (const room of client.getRooms()) jest.spyOn(room, "getMyMembership").mockReturnValue("join");
algorithm.setKnownRooms(client.getRooms());
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
MockedCall.create(roomWithCall, "1");
const call = CallStore.instance.get(roomWithCall.roomId);
if (call === null) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, roomWithCall.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
Object.defineProperty(navigator, "mediaDevices", {
value: { enumerateDevices: async () => [] },
});
// End of setup
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
await call.connect();
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
await call.disconnect();
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
});
});

View file

@ -18,7 +18,7 @@ import { mocked } from "jest-mock";
import { Room } from "matrix-js-sdk/src/matrix";
import { VisibilityProvider } from "../../../../src/stores/room-list/filters/VisibilityProvider";
import CallHandler from "../../../../src/CallHandler";
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import VoipUserMapper from "../../../../src/VoipUserMapper";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
import { RoomListCustomisations } from "../../../../src/customisations/RoomList";
@ -28,7 +28,7 @@ jest.mock("../../../../src/VoipUserMapper", () => ({
sharedInstance: jest.fn(),
}));
jest.mock("../../../../src/CallHandler", () => ({
jest.mock("../../../../src/LegacyCallHandler", () => ({
instance: {
getSupportsVirtualRooms: jest.fn(),
},
@ -82,7 +82,7 @@ describe("VisibilityProvider", () => {
describe("isRoomVisible", () => {
describe("for a virtual room", () => {
beforeEach(() => {
mocked(CallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true);
mocked(LegacyCallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true);
mocked(mockVoipUserMapper.isVirtualRoom).mockReturnValue(true);
});