Merge branch 'develop' into weeman1337/mute-broadcast-notifications

This commit is contained in:
Michael Weimann
2023-01-05 09:23:24 +01:00
57 changed files with 984 additions and 694 deletions

View File

@@ -15,11 +15,10 @@ limitations under the License.
*/
import React from "react";
import { render, RenderResult } from "@testing-library/react";
import { screen, render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import PictureInPictureDragger, {
CreatePipChildren,
} from "../../../../src/components/views/voip/PictureInPictureDragger";
import PictureInPictureDragger, { CreatePipChildren } from "../../../src/components/structures/PictureInPictureDragger";
describe("PictureInPictureDragger", () => {
let renderResult: RenderResult;
@@ -82,4 +81,29 @@ describe("PictureInPictureDragger", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
it("doesn't leak drag events to children as clicks", async () => {
const clickSpy = jest.fn();
render(
<PictureInPictureDragger draggable={true}>
{[
({ onStartMoving }) => (
<div onMouseDown={onStartMoving} onClick={clickSpy}>
Hello
</div>
),
]}
</PictureInPictureDragger>,
);
const target = screen.getByText("Hello");
// A click without a drag motion should go through
await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { keys: "[/MouseLeft]" }]);
expect(clickSpy).toHaveBeenCalled();
// A drag motion should not trigger a click
clickSpy.mockClear();
await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { coords: { x: 60, y: 60 } }, "[/MouseLeft]"]);
expect(clickSpy).not.toHaveBeenCalled();
});
});

View File

@@ -16,12 +16,14 @@ limitations under the License.
import React from "react";
import { mocked, Mocked } from "jest-mock";
import { screen, render, act, cleanup, fireEvent, waitFor } from "@testing-library/react";
import { screen, render, act, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, 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, ClientWidgetApi } from "matrix-widget-api";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {
@@ -34,18 +36,19 @@ import {
wrapInMatrixClientContext,
wrapInSdkContext,
mkRoomCreateEvent,
mockPlatformPeg,
flushPromises,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { CallStore } from "../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import UnwrappedPipView from "../../../../src/components/views/voip/PipView";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
import { TestSdkContext } from "../../../TestSdkContext";
} from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { CallStore } from "../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
import { PipContainer as UnwrappedPipContainer } from "../../../src/components/structures/PipContainer";
import ActiveWidgetStore from "../../../src/stores/ActiveWidgetStore";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import { TestSdkContext } from "../../TestSdkContext";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlaybacksStore,
@@ -53,15 +56,21 @@ import {
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators";
} from "../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
import { IRoomStateEventsActionPayload } from "../../../src/actions/MatrixActionCreators";
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import WidgetStore from "../../../src/stores/WidgetStore";
import { WidgetType } from "../../../src/widgets/WidgetType";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
describe("PipView", () => {
describe("PipContainer", () => {
useMockedCalls();
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
let user: UserEvent;
let sdkContext: TestSdkContext;
let client: Mocked<MatrixClient>;
let room: Room;
@@ -78,6 +87,8 @@ describe("PipView", () => {
};
beforeEach(async () => {
user = userEvent.setup();
stubClient();
client = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
@@ -110,6 +121,8 @@ describe("PipView", () => {
);
sdkContext = new TestSdkContext();
// @ts-ignore PipContainer uses SDKContext in the constructor
SdkContextClass.instance = sdkContext;
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore);
@@ -127,11 +140,11 @@ describe("PipView", () => {
});
const renderPip = () => {
const PipView = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipView, sdkContext));
render(<PipView />);
const PipContainer = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipContainer, sdkContext));
render(<PipContainer />);
};
const viewRoom = (roomId: string) =>
const viewRoom = (roomId: string) => {
defaultDispatcher.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
@@ -140,8 +153,9 @@ describe("PipView", () => {
},
true,
);
};
const withCall = async (fn: () => Promise<void>): Promise<void> => {
const withCall = async (fn: (call: MockedCall) => Promise<void>): Promise<void> => {
MockedCall.create(room, "1");
const call = CallStore.instance.getCall(room.roomId);
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
@@ -156,16 +170,16 @@ describe("PipView", () => {
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
});
await fn();
await fn(call);
cleanup();
call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
};
const withWidget = (fn: () => void): void => {
const withWidget = async (fn: () => Promise<void>): Promise<void> => {
act(() => ActiveWidgetStore.instance.setWidgetPersistence("1", room.roomId, true));
fn();
await fn();
cleanup();
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
};
@@ -197,7 +211,7 @@ describe("PipView", () => {
};
const setUpRoomViewStore = () => {
new RoomViewStore(defaultDispatcher, sdkContext);
sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext);
};
const mkVoiceBroadcast = (room: Room): MatrixEvent => {
@@ -220,54 +234,104 @@ describe("PipView", () => {
expect(screen.queryByRole("complementary")).toBeNull();
});
it("shows an active call with a maximise button", async () => {
it("shows an active call with back and leave buttons", async () => {
renderPip();
await withCall(async () => {
await withCall(async (call) => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
// The maximise button should jump to the call
// The return button should jump to the call
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: "Fill screen" }));
await waitFor(() =>
expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
}),
);
await user.click(screen.getByRole("button", { name: "Back" }));
expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
metricsTrigger: expect.any(String),
});
defaultDispatcher.unregister(dispatcherRef);
// The leave button should disconnect from the call
const disconnectSpy = jest.spyOn(call, "disconnect");
await user.click(screen.getByRole("button", { name: "Leave" }));
expect(disconnectSpy).toHaveBeenCalled();
});
});
it("shows a persistent widget with pin and maximise buttons when viewing the room", () => {
it("shows a persistent widget with back button when viewing the room", async () => {
setUpRoomViewStore();
viewRoom(room.roomId);
const widget = WidgetStore.instance.addVirtualWidget(
{
id: "1",
creatorUserId: "@alice:exaxmple.org",
type: WidgetType.CUSTOM.preferred,
url: "https://example.org",
name: "Example widget",
},
room.roomId,
);
renderPip();
withWidget(() => {
await withWidget(async () => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
screen.getByRole("button", { name: "Pin" });
screen.getByRole("button", { name: "Fill screen" });
expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
// The return button should maximize the widget
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
await user.click(screen.getByRole("button", { name: "Back" }));
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
});
WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
it("shows a persistent widget with a return button when not viewing the room", () => {
it("shows a persistent Jitsi widget with back and leave buttons when not viewing the room", async () => {
mockPlatformPeg({ supportsJitsiScreensharing: () => true });
setUpRoomViewStore();
viewRoom(room2.roomId);
const widget = WidgetStore.instance.addVirtualWidget(
{
id: "1",
creatorUserId: "@alice:exaxmple.org",
type: WidgetType.JITSI.preferred,
url: "https://meet.example.org",
name: "Jitsi example",
},
room.roomId,
);
renderPip();
withWidget(() => {
await withWidget(async () => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
expect(screen.queryByRole("button", { name: "Fill screen" })).toBeNull();
screen.getByRole("button", { name: /return/i });
// The return button should view the room
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
await user.click(screen.getByRole("button", { name: "Back" }));
expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: expect.any(String),
});
defaultDispatcher.unregister(dispatcherRef);
// The leave button should hangup the call
const sendSpy = jest
.fn<
ReturnType<ClientWidgetApi["transport"]["send"]>,
Parameters<ClientWidgetApi["transport"]["send"]>
>()
.mockResolvedValue({});
const mockMessaging = { transport: { send: sendSpy }, stop: () => {} } as unknown as ClientWidgetApi;
WidgetMessagingStore.instance.storeMessaging(new Widget(widget), room.roomId, mockMessaging);
await user.click(screen.getByRole("button", { name: "Leave" }));
expect(sendSpy).toHaveBeenCalledWith(ElementWidgetActions.HangupCall, {});
});
WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
describe("when there is a voice broadcast recording and pre-recording", () => {
@@ -287,8 +351,8 @@ describe("PipView", () => {
await withCall(async () => {
// Broadcast: Check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
// Call: Check for the „Fill screen“ button to be present
expect(screen.queryByLabelText("Fill screen")).toBeInTheDocument();
// Call: Check for the „Leave“ button to be present
screen.getByRole("button", { name: "Leave" });
});
});
});

View File

@@ -15,8 +15,7 @@ limitations under the License.
*/
import React from "react";
// eslint-disable-next-line deprecate/import
import { mount } from "enzyme";
import { render, screen } from "@testing-library/react";
import * as maplibregl from "maplibre-gl";
import { act } from "react-dom/test-utils";
import { Beacon, Room, RoomMember, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
@@ -43,6 +42,7 @@ describe("<BeaconMarker />", () => {
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
const mockMarker = new maplibregl.Marker();
const mockClient = getMockClientWithEventEmitter({
getClientWellKnown: jest.fn().mockReturnValue({
@@ -64,14 +64,16 @@ describe("<BeaconMarker />", () => {
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
const notLiveEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-2");
const geoUri1 = "geo:51,41";
const location1 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
geoUri: "geo:51,41",
geoUri: geoUri1,
timestamp: now + 1,
});
const geoUri2 = "geo:52,42";
const location2 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
geoUri: "geo:52,42",
geoUri: geoUri2,
timestamp: now + 10000,
});
@@ -80,11 +82,15 @@ describe("<BeaconMarker />", () => {
beacon: new Beacon(defaultEvent),
};
const getComponent = (props = {}) =>
mount(<BeaconMarker {...defaultProps} {...props} />, {
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient },
const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<BeaconMarker {...defaultProps} {...props} />, {
wrapper: Wrapper,
});
};
beforeEach(() => {
jest.clearAllMocks();
@@ -93,38 +99,45 @@ describe("<BeaconMarker />", () => {
it("renders nothing when beacon is not live", () => {
const room = setupRoom([notLiveEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent));
const component = getComponent({ beacon });
expect(component.html()).toBe(null);
const { asFragment } = renderComponent({ beacon });
expect(asFragment()).toMatchInlineSnapshot(`<DocumentFragment />`);
expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
});
it("renders nothing when beacon has no location", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
const component = getComponent({ beacon });
expect(component.html()).toBe(null);
const { asFragment } = renderComponent({ beacon });
expect(asFragment()).toMatchInlineSnapshot(`<DocumentFragment />`);
expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
});
it("renders marker when beacon has location", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
beacon.addLocations([location1]);
const component = getComponent({ beacon });
expect(component).toMatchSnapshot();
beacon?.addLocations([location1]);
const { asFragment } = renderComponent({ beacon });
expect(asFragment()).toMatchSnapshot();
expect(screen.getByTestId("avatar-img")).toBeInTheDocument();
});
it("updates with new locations", () => {
const lonLat1 = { lon: 41, lat: 51 };
const lonLat2 = { lon: 42, lat: 52 };
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
beacon.addLocations([location1]);
const component = getComponent({ beacon });
expect(component.find("SmartMarker").props()["geoUri"]).toEqual("geo:51,41");
beacon?.addLocations([location1]);
// render the component then add a new location, check mockMarker called as expected
renderComponent({ beacon });
expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat1);
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
// add a location, check mockMarker called with new location details
act(() => {
beacon.addLocations([location2]);
beacon?.addLocations([location2]);
});
component.setProps({});
// updated to latest location
expect(component.find("SmartMarker").props()["geoUri"]).toEqual("geo:52,42");
expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat2);
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
});
});

View File

@@ -1,240 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
<BeaconMarker
beacon={
Beacon {
"_beaconInfo": {
"assetType": "m.self",
"description": undefined,
"live": true,
"timeout": 3600000,
"timestamp": 1647270879403,
},
"_events": {
"Beacon.Destroy": [
[Function],
[Function],
],
"Beacon.LivenessChange": [
[Function],
[Function],
],
"Beacon.LocationUpdate": [Function],
"Beacon.new": [Function],
"Beacon.update": [Function],
},
"_eventsCount": 5,
"_isLive": true,
"_latestLocationEvent": {
"content": {
"m.relates_to": {
"event_id": "$alice-room1-1",
"rel_type": "m.reference",
},
"org.matrix.msc3488.location": {
"description": undefined,
"uri": "geo:51,41",
},
"org.matrix.msc3488.ts": 1647270879404,
},
"room_id": undefined,
"sender": "@alice:server",
"type": "org.matrix.msc3672.beacon",
},
"_maxListeners": undefined,
"clearLatestLocation": [Function],
"livenessWatchTimeout": undefined,
"roomId": "!room:server",
"rootEvent": {
"content": {
"description": undefined,
"live": true,
"org.matrix.msc3488.asset": {
"type": "m.self",
},
"org.matrix.msc3488.ts": 1647270879403,
"timeout": 3600000,
},
"event_id": "$alice-room1-1",
"origin_server_ts": 1647270879403,
"room_id": "!room:server",
"sender": "@alice:server",
"state_key": "@alice:server",
"type": "org.matrix.msc3672.beacon_info",
},
Symbol(kCapture): false,
}
}
map={
MockMap {
"_events": {},
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],
"zoomIn": [MockFunction],
"zoomOut": [MockFunction],
Symbol(kCapture): false,
}
}
>
<SmartMarker
geoUri="geo:51,41"
id="!room:server_@alice:server"
map={
MockMap {
"_events": {},
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],
"zoomIn": [MockFunction],
"zoomOut": [MockFunction],
Symbol(kCapture): false,
}
}
roomMember={
RoomMember {
"_events": {},
"_eventsCount": 0,
"_isOutOfBand": false,
"_maxListeners": undefined,
"disambiguate": false,
"events": {},
"membership": undefined,
"modified": 1647270879403,
"name": "@alice:server",
"powerLevel": 0,
"powerLevelNorm": 0,
"rawDisplayName": "@alice:server",
"requestedProfileInfo": false,
"roomId": "!room:server",
"typing": false,
"user": undefined,
"userId": "@alice:server",
Symbol(kCapture): false,
}
}
useMemberColor={true}
>
<span>
<ForwardRef
id="!room:server_@alice:server"
roomMember={
RoomMember {
"_events": {},
"_eventsCount": 0,
"_isOutOfBand": false,
"_maxListeners": undefined,
"disambiguate": false,
"events": {},
"membership": undefined,
"modified": 1647270879403,
"name": "@alice:server",
"powerLevel": 0,
"powerLevelNorm": 0,
"rawDisplayName": "@alice:server",
"requestedProfileInfo": false,
"roomId": "!room:server",
"typing": false,
"user": undefined,
"userId": "@alice:server",
Symbol(kCapture): false,
}
}
useMemberColor={true}
<DocumentFragment>
<span>
<div
class="mx_Marker mx_Username_color4"
id="!room:server_@alice:server"
>
<div
class="mx_Marker_border"
>
<div
className="mx_Marker mx_Username_color4"
id="!room:server_@alice:server"
<span
class="mx_BaseAvatar"
role="presentation"
>
<OptionalTooltip>
<div
className="mx_Marker_border"
>
<MemberAvatar
height={36}
hideTitle={false}
member={
RoomMember {
"_events": {},
"_eventsCount": 0,
"_isOutOfBand": false,
"_maxListeners": undefined,
"disambiguate": false,
"events": {},
"membership": undefined,
"modified": 1647270879403,
"name": "@alice:server",
"powerLevel": 0,
"powerLevelNorm": 0,
"rawDisplayName": "@alice:server",
"requestedProfileInfo": false,
"roomId": "!room:server",
"typing": false,
"user": undefined,
"userId": "@alice:server",
Symbol(kCapture): false,
}
}
viewUserOnClick={false}
width={36}
>
<BaseAvatar
height={36}
idName="@alice:server"
name="@alice:server"
resizeMethod="crop"
title="@alice:server"
width={36}
>
<span
className="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
className="mx_BaseAvatar_initial"
style={
{
"fontSize": "23.400000000000002px",
"lineHeight": "36px",
"width": "36px",
}
}
>
A
</span>
<img
alt=""
aria-hidden="true"
className="mx_BaseAvatar_image"
data-testid="avatar-img"
onError={[Function]}
src="data:image/png;base64,00"
style={
{
"height": "36px",
"width": "36px",
}
}
title="@alice:server"
/>
</span>
</BaseAvatar>
</MemberAvatar>
</div>
</OptionalTooltip>
</div>
</ForwardRef>
</span>
</SmartMarker>
</BeaconMarker>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
>
A
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
title="@alice:server"
/>
</span>
</div>
</div>
</span>
</DocumentFragment>
`;

View File

@@ -42,14 +42,15 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { ReadPinsEventId } from "../../../../src/components/views/right_panel/types";
import { Action } from "../../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
jest.mock("../../../../src/utils/strings", () => ({
copyPlaintext: jest.fn(),
getSelectedText: jest.fn(),
}));
jest.mock("../../../../src/utils/EventUtils", () => ({
// @ts-ignore don't mock everything
...jest.requireActual("../../../../src/utils/EventUtils"),
...(jest.requireActual("../../../../src/utils/EventUtils") as object),
canEditContent: jest.fn(),
}));
jest.mock("../../../../src/dispatcher/dispatcher");
@@ -241,6 +242,17 @@ describe("MessageContextMenu", () => {
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
});
it("should not allow forwarding a voice broadcast", () => {
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
const menu = createMenu(broadcastStartEvent);
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
});
describe("forwarding beacons", () => {
const aliceId = "@alice:server.org";

View File

@@ -15,8 +15,7 @@ limitations under the License.
*/
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { act } from "react-test-renderer";
import { act, render, fireEvent } from "@testing-library/react";
import { EventType, EventStatus, MatrixEvent, MatrixEventEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
@@ -34,6 +33,8 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { Action } from "../../../../src/dispatcher/actions";
import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
jest.mock("../../../../src/dispatcher/dispatcher");
@@ -405,6 +406,17 @@ describe("<MessageActionBar />", () => {
expect(queryByLabelText("Reply in thread")).toBeTruthy();
});
it("does not render thread button for a voice broadcast", () => {
const broadcastEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
userId,
"ABC123",
);
const { queryByLabelText } = getComponent({ mxEvent: broadcastEvent });
expect(queryByLabelText("Reply in thread")).not.toBeInTheDocument();
});
it("opens user settings on click", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });

View File

@@ -229,7 +229,10 @@ describe("EditWysiwygComposer", () => {
},
"msgtype": "m.text",
};
expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent);
await waitFor(() =>
expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent),
);
expect(spyDispatcher).toBeCalledWith({ action: "message_sent" });
});
});

View File

@@ -19,6 +19,8 @@ import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings";
import * as mockKeyboard from "../../../../../../src/Keyboard";
describe("PlainTextComposer", () => {
const customRender = (
@@ -37,6 +39,17 @@ describe("PlainTextComposer", () => {
);
};
let mockUseSettingValue: jest.SpyInstance;
beforeEach(() => {
// defaults for these tests are:
// ctrlEnterToSend is false
mockUseSettingValue = jest.spyOn(mockUseSettingsHook, "useSettingValue").mockReturnValue(false);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("Should have contentEditable at false when disabled", () => {
// When
customRender(jest.fn(), jest.fn(), true);
@@ -64,7 +77,7 @@ describe("PlainTextComposer", () => {
expect(onChange).toBeCalledWith(content);
});
it("Should call onSend when Enter is pressed", async () => {
it("Should call onSend when Enter is pressed when ctrlEnterToSend is false", async () => {
//When
const onSend = jest.fn();
customRender(jest.fn(), onSend);
@@ -74,9 +87,134 @@ describe("PlainTextComposer", () => {
expect(onSend).toBeCalledTimes(1);
});
it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
await userEvent.type(screen.getByRole("textbox"), "{enter}");
// Then it does not send a message
expect(onSend).toBeCalledTimes(0);
});
it("Should only call onSend when ctrl+enter is pressed when ctrlEnterToSend is true on windows", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
await userEvent.type(textBox, "hello");
// Then it does NOT send a message on enter
await userEvent.type(textBox, "{enter}");
expect(onSend).toBeCalledTimes(0);
// Then it does NOT send a message on windows+enter
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
expect(onSend).toBeCalledTimes(0);
// Then it does send a message on ctrl+enter
await userEvent.type(textBox, "{control>}{enter}{control/}");
expect(onSend).toBeCalledTimes(1);
});
it("Should only call onSend when cmd+enter is pressed when ctrlEnterToSend is true on mac", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
Object.defineProperty(mockKeyboard, "IS_MAC", { value: true });
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
await userEvent.type(textBox, "hello");
// Then it does NOT send a message on enter
await userEvent.type(textBox, "{enter}");
expect(onSend).toBeCalledTimes(0);
// Then it does NOT send a message on ctrl+enter
await userEvent.type(textBox, "{control>}{enter}{control/}");
expect(onSend).toBeCalledTimes(0);
// Then it does send a message on cmd+enter
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
expect(onSend).toBeCalledTimes(1);
});
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is false", async () => {
//When
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const inputWithShiftEnter = "new{Shift>}{enter}{/Shift}line";
const expectedInnerHtml = "new\nline";
await userEvent.click(textBox);
await userEvent.type(textBox, inputWithShiftEnter);
// Then it does not send a message, but inserts a newline character
expect(onSend).toBeCalledTimes(0);
expect(textBox.innerHTML).toBe(expectedInnerHtml);
});
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const keyboardInput = "new{Shift>}{enter}{/Shift}line";
const expectedInnerHtml = "new\nline";
await userEvent.click(textBox);
await userEvent.type(textBox, keyboardInput);
// Then it does not send a message, but inserts a newline character
expect(onSend).toBeCalledTimes(0);
expect(textBox.innerHTML).toBe(expectedInnerHtml);
});
it("Should not insert div and br tags when enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const enterThenTypeHtml = "<div>hello</div";
await userEvent.click(textBox);
await userEvent.type(textBox, "{enter}hello");
// Then it does not send a message, but inserts a newline character
expect(onSend).toBeCalledTimes(0);
expect(textBox).not.toContainHTML(enterThenTypeHtml);
});
it("Should not insert div tags when enter is pressed then user types more when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const defaultEnterHtml = "<div><br></div";
await userEvent.click(textBox);
await userEvent.type(textBox, "{enter}");
// Then it does not send a message, but inserts a newline character
expect(onSend).toBeCalledTimes(0);
expect(textBox).not.toContainHTML(defaultEnterHtml);
});
it("Should clear textbox content when clear is called", async () => {
//When
let composer;
let composer: {
clear: () => void;
insertText: (text: string) => void;
};
render(
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
{(ref, composerFunctions) => {
@@ -85,9 +223,11 @@ describe("PlainTextComposer", () => {
}}
</PlainTextComposer>,
);
await userEvent.type(screen.getByRole("textbox"), "content");
expect(screen.getByRole("textbox").innerHTML).toBe("content");
composer.clear();
composer!.clear();
// Then
expect(screen.getByRole("textbox").innerHTML).toBeFalsy();
@@ -112,7 +252,7 @@ describe("PlainTextComposer", () => {
render(<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />);
// Then
expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("false");
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("false");
expect(editor).toBe(screen.getByRole("textbox"));
// When
@@ -126,7 +266,7 @@ describe("PlainTextComposer", () => {
});
// Then
expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("true");
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("true");
jest.useRealTimers();
(global.ResizeObserver as jest.Mock).mockRestore();

View File

@@ -24,7 +24,7 @@ describe("createMessageContent", () => {
return "$$permalink$$";
},
} as RoomPermalinkCreator;
const message = "<i><b>hello</b> world</i>";
const message = "<em><b>hello</b> world</em>";
const mockEvent = mkEvent({
type: "m.room.message",
room: "myfakeroom",
@@ -37,31 +37,31 @@ describe("createMessageContent", () => {
jest.resetAllMocks();
});
it("Should create html message", () => {
it("Should create html message", async () => {
// When
const content = createMessageContent(message, true, { permalinkCreator });
const content = await createMessageContent(message, true, { permalinkCreator });
// Then
expect(content).toEqual({
body: "hello world",
body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",
});
});
it("Should add reply to message content", () => {
it("Should add reply to message content", async () => {
// When
const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
// Then
expect(content).toEqual({
"body": "> <myfakeuser> Replying to this\n\nhello world",
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
"<br>Replying to this</blockquote></mx-reply><i><b>hello</b> world</i>",
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
@@ -71,17 +71,17 @@ describe("createMessageContent", () => {
});
});
it("Should add relation to message", () => {
it("Should add relation to message", async () => {
// When
const relation = {
rel_type: "m.thread",
event_id: "myFakeThreadId",
};
const content = createMessageContent(message, true, { permalinkCreator, relation });
const content = await createMessageContent(message, true, { permalinkCreator, relation });
// Then
expect(content).toEqual({
"body": "hello world",
"body": "*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": message,
"msgtype": "m.text",
@@ -92,7 +92,7 @@ describe("createMessageContent", () => {
});
});
it("Should add fields related to edition", () => {
it("Should add fields related to edition", async () => {
// When
const editedEvent = mkEvent({
type: "m.room.message",
@@ -110,16 +110,16 @@ describe("createMessageContent", () => {
},
event: true,
});
const content = createMessageContent(message, true, { permalinkCreator, editedEvent });
const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
// Then
expect(content).toEqual({
"body": " * hello world",
"body": " * *__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": ` * ${message}`,
"msgtype": "m.text",
"m.new_content": {
body: "hello world",
body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",

View File

@@ -70,6 +70,79 @@ describe("message", () => {
expect(spyDispatcher).toBeCalledTimes(0);
});
it("Should not send message when there is no roomId", async () => {
// When
const mockRoomWithoutId = mkStubRoom("", "room without id", mockClient) as any;
const mockRoomContextWithoutId: IRoomState = getRoomContext(mockRoomWithoutId, {});
await sendMessage(message, true, {
roomContext: mockRoomContextWithoutId,
mxClient: mockClient,
permalinkCreator,
});
// Then
expect(mockClient.sendMessage).toBeCalledTimes(0);
expect(spyDispatcher).toBeCalledTimes(0);
});
describe("calls client.sendMessage with", () => {
it("a null argument if SendMessageParams is missing relation", async () => {
// When
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
});
// Then
expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.anything(), null, expect.anything());
});
it("a null argument if SendMessageParams has relation but relation is missing event_id", async () => {
// When
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {},
});
// Then
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
});
it("a null argument if SendMessageParams has relation but rel_type does not match THREAD_RELATION_TYPE.name", async () => {
// When
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {
event_id: "valid_id",
rel_type: "m.does_not_match",
},
});
// Then
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
});
it("the event_id if SendMessageParams has relation and rel_type matches THREAD_RELATION_TYPE.name", async () => {
// When
await sendMessage(message, true, {
roomContext: defaultRoomContext,
mxClient: mockClient,
permalinkCreator,
relation: {
event_id: "valid_id",
rel_type: "m.thread",
},
});
// Then
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), "valid_id", expect.anything());
});
});
it("Should send html message", async () => {
// When
await sendMessage(message, true, {
@@ -80,7 +153,7 @@ describe("message", () => {
// Then
const expectedContent = {
body: "hello world",
body: "*__hello__ world*",
format: "org.matrix.custom.html",
formatted_body: "<i><b>hello</b> world</i>",
msgtype: "m.text",
@@ -114,7 +187,7 @@ describe("message", () => {
});
const expectedContent = {
"body": "> <myfakeuser2> My reply\n\nhello world",
"body": "> <myfakeuser2> My reply\n\n*__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body":
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +

View File

@@ -784,6 +784,13 @@ describe("ElementCall", () => {
expect(call.connectionState).toBe(ConnectionState.Connected);
});
it("disconnects if the widget dies", async () => {
await call.connect();
expect(call.connectionState).toBe(ConnectionState.Connected);
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
expect(call.connectionState).toBe(ConnectionState.Disconnected);
});
it("tracks participants in room state", async () => {
expect(call.participants).toEqual(new Map());

View File

@@ -43,6 +43,8 @@ import {
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../test-utils";
import dis from "../../src/dispatcher/dispatcher";
import { Action } from "../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../src/voice-broadcast/types";
jest.mock("../../src/dispatcher/dispatcher");
@@ -151,6 +153,20 @@ describe("EventUtils", () => {
},
});
const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Stopped,
"@user:example.com",
"ABC123",
);
describe("isContentActionable()", () => {
type TestCase = [string, MatrixEvent];
it.each<TestCase>([
@@ -161,6 +177,7 @@ describe("EventUtils", () => {
["room member event", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
["broadcast stop event", voiceBroadcastStop],
])("returns false for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(false);
});
@@ -171,6 +188,7 @@ describe("EventUtils", () => {
["event with empty content body", emptyContentBody],
["event with a content body", niceTextMessage],
["beacon_info event", beaconInfoEvent],
["broadcast start event", voiceBroadcastStart],
])("returns true for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});

View File

@@ -21,7 +21,7 @@ import {
VoiceBroadcastChunkEventType,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
} from "../../../src/voice-broadcast";
} from "../../../src/voice-broadcast/types";
import { mkEvent } from "../../test-utils";
// timestamp incremented on each call to prevent duplicate timestamp