Prepare for repo merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 11:35:21 +01:00
parent 0f670b8dc0
commit b084ff2313
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
807 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,183 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import { Beacon, RoomMember, MatrixEvent, LocationAssetType } from "matrix-js-sdk/src/matrix";
import BeaconListItem from "../../../../src/components/views/beacon/BeaconListItem";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
makeRoomWithBeacons,
} from "../../../test-utils";
describe("<BeaconListItem />", () => {
// 14.03.2022 16:15
const now = 1647270879403;
// go back in time to create beacons and locations in the past
jest.spyOn(global.Date, "now").mockReturnValue(now - 600000);
const roomId = "!room:server";
const aliceId = "@alice:server";
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
getRoom: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
});
const aliceBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
const alicePinBeaconEvent = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true, assetType: LocationAssetType.Pin, description: "Alice's car" },
"$alice-room1-1",
);
const pinBeaconWithoutDescription = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true, assetType: LocationAssetType.Pin },
"$alice-room1-1",
);
const aliceLocation1 = makeBeaconEvent(aliceId, {
beaconInfoId: aliceBeaconEvent.getId(),
geoUri: "geo:51,41",
timestamp: now - 1,
});
const aliceLocation2 = makeBeaconEvent(aliceId, {
beaconInfoId: aliceBeaconEvent.getId(),
geoUri: "geo:52,42",
timestamp: now - 500000,
});
const defaultProps = {
beacon: new Beacon(aliceBeaconEvent),
};
const getComponent = (props = {}) =>
render(
<MatrixClientContext.Provider value={mockClient}>
<BeaconListItem {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
);
const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => {
const beacons = makeRoomWithBeacons(roomId, mockClient, beaconInfoEvents, locationEvents);
const member = new RoomMember(roomId, aliceId);
member.name = `Alice`;
const room = mockClient.getRoom(roomId)!;
jest.spyOn(room, "getMember").mockReturnValue(member);
return beacons;
};
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, "now").mockReturnValue(now);
});
it("renders null when beacon is not live", () => {
const notLiveBeacon = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
const [beacon] = setupRoomWithBeacons([notLiveBeacon]);
const { container } = getComponent({ beacon });
expect(container.innerHTML).toBeFalsy();
});
it("renders null when beacon has no location", () => {
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent]);
const { container } = getComponent({ beacon });
expect(container.innerHTML).toBeFalsy();
});
describe("when a beacon is live and has locations", () => {
it("renders beacon info", () => {
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
const { asFragment } = getComponent({ beacon });
expect(asFragment()).toMatchSnapshot();
});
describe("non-self beacons", () => {
it("uses beacon description as beacon name", () => {
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent("Alice's car");
});
it("uses beacon owner mxid as beacon name for a beacon without description", () => {
const [beacon] = setupRoomWithBeacons([pinBeaconWithoutDescription], [aliceLocation1]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent(aliceId);
});
it("renders location icon", () => {
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_StyledLiveBeaconIcon")).toBeTruthy();
});
});
describe("self locations", () => {
it("renders beacon owner avatar", () => {
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_BaseAvatar")).toBeTruthy();
});
it("uses beacon owner name as beacon name", () => {
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent("Alice");
});
});
describe("on location updates", () => {
it("updates last updated time on location updated", () => {
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation2]);
const { container } = getComponent({ beacon });
expect(container.querySelector(".mx_BeaconListItem_lastUpdated")).toHaveTextContent(
"Updated 9 minutes ago",
);
// update to a newer location
act(() => {
beacon.addLocations([aliceLocation1]);
});
expect(container.querySelector(".mx_BeaconListItem_lastUpdated")).toHaveTextContent(
"Updated a few seconds ago",
);
});
});
describe("interactions", () => {
it("does not call onClick handler when clicking share button", () => {
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
const onClick = jest.fn();
const { getByTestId } = getComponent({ beacon, onClick });
fireEvent.click(getByTestId("open-location-in-osm"));
expect(onClick).not.toHaveBeenCalled();
});
it("calls onClick handler when clicking outside of share buttons", () => {
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
const onClick = jest.fn();
const { container } = getComponent({ beacon, onClick });
// click the beacon name
fireEvent.click(container.querySelector(".mx_BeaconStatus_description")!);
expect(onClick).toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,136 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, screen, waitFor } from "jest-matrix-react";
import * as maplibregl from "maplibre-gl";
import { Beacon, Room, RoomMember, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
import BeaconMarker from "../../../../src/components/views/beacon/BeaconMarker";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
makeRoomWithStateEvents,
} from "../../../test-utils";
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
describe("<BeaconMarker />", () => {
// 14.03.2022 16:15
const now = 1647270879403;
// stable date for snapshots
jest.spyOn(global.Date, "now").mockReturnValue(now);
const roomId = "!room:server";
const aliceId = "@alice:server";
const aliceMember = new RoomMember(roomId, aliceId);
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({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
}),
getUserId: jest.fn().mockReturnValue(aliceId),
getRoom: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
});
// make fresh rooms every time
// as we update room state
const setupRoom = (stateEvents: MatrixEvent[] = []): Room => {
const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient });
jest.spyOn(room1, "getMember").mockReturnValue(aliceMember);
return room1;
};
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: geoUri1,
timestamp: now + 1,
});
const geoUri2 = "geo:52,42";
const location2 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
geoUri: geoUri2,
timestamp: now + 10000,
});
const defaultProps = {
map: mockMap,
beacon: new Beacon(defaultEvent),
};
const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<BeaconMarker {...defaultProps} {...props} />, {
wrapper: Wrapper,
});
};
beforeEach(() => {
jest.clearAllMocks();
});
it("renders nothing when beacon is not live", () => {
const room = setupRoom([notLiveEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent));
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 { asFragment } = renderComponent({ beacon });
expect(asFragment()).toMatchInlineSnapshot(`<DocumentFragment />`);
expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
});
it("renders marker when beacon has location", async () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
beacon?.addLocations([location1]);
const { asFragment } = renderComponent({ beacon });
await waitFor(() => {
expect(screen.getByTestId("avatar-img")).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
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]);
// 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]);
});
expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat2);
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
});
});

View file

@ -0,0 +1,80 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { Beacon } from "matrix-js-sdk/src/matrix";
import BeaconStatus from "../../../../src/components/views/beacon/BeaconStatus";
import { BeaconDisplayStatus } from "../../../../src/components/views/beacon/displayStatus";
import { makeBeaconInfoEvent } from "../../../test-utils";
describe("<BeaconStatus />", () => {
const defaultProps = {
displayStatus: BeaconDisplayStatus.Loading,
label: "test label",
withIcon: true,
};
const renderComponent = (props = {}) => render(<BeaconStatus {...defaultProps} {...props} />);
it("renders loading state", () => {
const { asFragment } = renderComponent({ displayStatus: BeaconDisplayStatus.Loading });
expect(asFragment()).toMatchSnapshot();
});
it("renders stopped state", () => {
const { asFragment } = renderComponent({ displayStatus: BeaconDisplayStatus.Stopped });
expect(asFragment()).toMatchSnapshot();
});
it("renders without icon", () => {
const iconClassName = "mx_StyledLiveBeaconIcon";
const { container } = renderComponent({ withIcon: false, displayStatus: BeaconDisplayStatus.Stopped });
expect(container.getElementsByClassName(iconClassName)).toHaveLength(0);
});
describe("active state", () => {
it("renders without children", () => {
// mock for stable snapshot
jest.spyOn(Date, "now").mockReturnValue(123456789);
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
const { asFragment } = renderComponent({ beacon, displayStatus: BeaconDisplayStatus.Active });
expect(asFragment()).toMatchSnapshot();
});
it("renders with children", () => {
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:sever", { isLive: false }));
renderComponent({
beacon,
children: <span data-testid="test-child">test</span>,
displayStatus: BeaconDisplayStatus.Active,
});
expect(screen.getByTestId("test-child")).toMatchSnapshot();
});
it("renders static remaining time when displayLiveTimeRemaining is falsy", () => {
// mock for stable snapshot
jest.spyOn(Date, "now").mockReturnValue(123456789);
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
renderComponent({ beacon, displayStatus: BeaconDisplayStatus.Active });
expect(screen.getByText("Live until 11:17")).toBeInTheDocument();
});
it("renders live time remaining when displayLiveTimeRemaining is truthy", () => {
// mock for stable snapshot
jest.spyOn(Date, "now").mockReturnValue(123456789);
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
renderComponent({
beacon,
displayStatus: BeaconDisplayStatus.Active,
displayLiveTimeRemaining: true,
});
expect(screen.getByText("1h left")).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,371 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import { MatrixClient, MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
import * as maplibregl from "maplibre-gl";
import { mocked } from "jest-mock";
import BeaconViewDialog from "../../../../src/components/views/beacon/BeaconViewDialog";
import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
makeRoomWithBeacons,
makeRoomWithStateEvents,
} from "../../../test-utils";
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
import { OwnBeaconStore } from "../../../../src/stores/OwnBeaconStore";
describe("<BeaconViewDialog />", () => {
// 14.03.2022 16:15
const now = 1647270879403;
// stable date for snapshots
jest.spyOn(global.Date, "now").mockReturnValue(now);
const roomId = "!room:server";
const aliceId = "@alice:server";
const bobId = "@bob:server";
const aliceMember = new RoomMember(roomId, aliceId);
const mockClient = getMockClientWithEventEmitter({
getClientWellKnown: jest.fn().mockReturnValue({
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
}),
getUserId: jest.fn().mockReturnValue(bobId),
getRoom: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
getVisibleRooms: jest.fn().mockReturnValue([]),
});
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
const mockMap = new maplibregl.Map(mapOptions);
const mockMarker = new maplibregl.Marker();
// make fresh rooms every time
// as we update room state
const setupRoom = (stateEvents: MatrixEvent[] = []): Room => {
const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient });
jest.spyOn(room1, "getMember").mockReturnValue(aliceMember);
return room1;
};
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
const location1 = makeBeaconEvent(aliceId, {
beaconInfoId: defaultEvent.getId(),
geoUri: "geo:51,41",
timestamp: now + 1,
});
const defaultProps = {
onFinished: jest.fn(),
roomId,
matrixClient: mockClient as MatrixClient,
};
const getComponent = (props = {}): RenderResult => render(<BeaconViewDialog {...defaultProps} {...props} />);
const openSidebar = (getByTestId: RenderResult["getByTestId"]) => {
fireEvent.click(getByTestId("beacon-view-dialog-open-sidebar"));
};
beforeEach(() => {
jest.spyOn(OwnBeaconStore.instance, "getLiveBeaconIds").mockRestore();
jest.spyOn(OwnBeaconStore.instance, "getBeaconById").mockRestore();
jest.spyOn(global.Date, "now").mockReturnValue(now);
jest.clearAllMocks();
});
it("renders a map with markers", async () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
getComponent();
// centered on default event
expect(mockMap.setCenter).toHaveBeenCalledWith({
lon: 41,
lat: 51,
});
// marker added
await waitFor(() => {
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
});
});
it("does not render any own beacon status when user is not live sharing", () => {
// default event belongs to alice, we are bob
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { queryByText } = getComponent();
expect(queryByText("Live location enabled")).not.toBeInTheDocument();
});
it("renders own beacon status when user is live sharing", () => {
// default event belongs to alice
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
// mock own beacon store to show default event as alice's live beacon
jest.spyOn(OwnBeaconStore.instance, "getLiveBeaconIds").mockReturnValue([beacon.identifier]);
jest.spyOn(OwnBeaconStore.instance, "getBeaconById").mockReturnValue(beacon);
const { container } = getComponent();
expect(container.querySelector(".mx_DialogOwnBeaconStatus")).toMatchSnapshot();
});
it("updates markers on changes to beacons", async () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { container } = getComponent();
// one marker
expect(mockMarker.addTo).toHaveBeenCalledTimes(1);
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
const anotherBeaconEvent = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-1");
act(() => {
// emits RoomStateEvent.BeaconLiveness
room.currentState.setStateEvents([anotherBeaconEvent]);
const beacon2 = room.currentState.beacons.get(getBeaconInfoIdentifier(anotherBeaconEvent))!;
beacon2.addLocations([location1]);
});
// two markers now!
expect(container.getElementsByClassName("mx_Marker").length).toEqual(2);
});
it("does not update bounds or center on changing beacons", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { container } = getComponent();
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
const anotherBeaconEvent = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-1");
act(() => {
// emits RoomStateEvent.BeaconLiveness
room.currentState.setStateEvents([anotherBeaconEvent]);
const beacon2 = room.currentState.beacons.get(getBeaconInfoIdentifier(anotherBeaconEvent))!;
beacon2.addLocations([location1]);
});
// called once on init
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
});
it("renders a fallback when there are no locations", () => {
// this is a cornercase, should not be a reachable state in UI anymore
const onFinished = jest.fn();
const room = setupRoom([defaultEvent]);
room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
const { getByTestId } = getComponent({ onFinished });
// map placeholder
expect(getByTestId("beacon-view-dialog-map-fallback")).toMatchSnapshot();
fireEvent.click(getByTestId("beacon-view-dialog-fallback-close"));
expect(onFinished).toHaveBeenCalled();
});
it("renders map without markers when no live beacons remain", () => {
const onFinished = jest.fn();
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { container } = getComponent({ onFinished });
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
// this will replace the defaultEvent
// leading to no more live beacons
const anotherBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-2");
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
// reset call counts
mocked(mockMap.setCenter).mockClear();
mocked(mockMap.fitBounds).mockClear();
act(() => {
// emits RoomStateEvent.BeaconLiveness
room.currentState.setStateEvents([anotherBeaconEvent]);
});
// no more avatars
expect(container.getElementsByClassName("mx_Marker").length).toEqual(0);
// map still rendered
expect(container.querySelector("#mx_Map_mx_BeaconViewDialog")).toBeInTheDocument();
// map location unchanged
expect(mockMap.setCenter).not.toHaveBeenCalled();
expect(mockMap.fitBounds).not.toHaveBeenCalled();
});
describe("sidebar", () => {
it("opens sidebar on view list button click", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { container, getByTestId } = getComponent();
openSidebar(getByTestId);
expect(container.querySelector(".mx_DialogSidebar")).toBeInTheDocument();
});
it("closes sidebar on close button click", () => {
const room = setupRoom([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
beacon.addLocations([location1]);
const { container, getByTestId } = getComponent();
// open the sidebar
openSidebar(getByTestId);
expect(container.querySelector(".mx_DialogSidebar")).toBeInTheDocument();
// now close it
fireEvent.click(getByTestId("dialog-sidebar-close"));
expect(container.querySelector(".mx_DialogSidebar")).not.toBeInTheDocument();
});
});
describe("focused beacons", () => {
const beacon2Event = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-2");
const location2 = makeBeaconEvent(bobId, {
beaconInfoId: beacon2Event.getId(),
geoUri: "geo:33,22",
timestamp: now + 1,
});
const fitBoundsOptions = { maxZoom: 15, padding: 100 };
it("opens map with both beacons in view on first load without initialFocusedBeacon", () => {
const [beacon1, beacon2] = makeRoomWithBeacons(
roomId,
mockClient,
[defaultEvent, beacon2Event],
[location1, location2],
);
getComponent({ beacons: [beacon1, beacon2] });
// start centered on mid point between both beacons
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 42, lon: 31.5 });
// only called once
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
// bounds fit both beacons, only called once
expect(mockMap.fitBounds).toHaveBeenCalledWith(
new maplibregl.LngLatBounds([22, 33], [41, 51]),
fitBoundsOptions,
);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
});
it("opens map with both beacons in view on first load with an initially focused beacon", () => {
const [beacon1, beacon2] = makeRoomWithBeacons(
roomId,
mockClient,
[defaultEvent, beacon2Event],
[location1, location2],
);
getComponent({ beacons: [beacon1, beacon2], initialFocusedBeacon: beacon1 });
// start centered on initialFocusedBeacon
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
// only called once
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
// bounds fit both beacons, only called once
expect(mockMap.fitBounds).toHaveBeenCalledWith(
new maplibregl.LngLatBounds([22, 33], [41, 51]),
fitBoundsOptions,
);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
});
it("focuses on beacon location on sidebar list item click", () => {
const [beacon1, beacon2] = makeRoomWithBeacons(
roomId,
mockClient,
[defaultEvent, beacon2Event],
[location1, location2],
);
const { container, getByTestId } = getComponent({ beacons: [beacon1, beacon2] });
// reset call counts on map mocks after initial render
jest.clearAllMocks();
openSidebar(getByTestId);
act(() => {
const listItems = container.querySelectorAll(".mx_BeaconListItem");
// click on the first beacon in the list
fireEvent.click(listItems[0]!);
});
// centered on clicked beacon
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
// only called once
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
// bounds fitted just to clicked beacon
expect(mockMap.fitBounds).toHaveBeenCalledWith(
new maplibregl.LngLatBounds([41, 51], [41, 51]),
fitBoundsOptions,
);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
});
it("refocuses on same beacon when clicking list item again", () => {
// test the map responds to refocusing the same beacon
const [beacon1, beacon2] = makeRoomWithBeacons(
roomId,
mockClient,
[defaultEvent, beacon2Event],
[location1, location2],
);
const { container, getByTestId } = getComponent({ beacons: [beacon1, beacon2] });
// reset call counts on map mocks after initial render
jest.clearAllMocks();
openSidebar(getByTestId);
act(() => {
// click on the second beacon in the list
const listItems = container.querySelectorAll(".mx_BeaconListItem");
fireEvent.click(listItems[1]!);
});
const expectedBounds = new maplibregl.LngLatBounds([22, 33], [22, 33]);
// date is mocked but this relies on timestamp, manually mock a tick
jest.spyOn(global.Date, "now").mockReturnValue(now + 1);
act(() => {
// click on the second beacon in the list
const listItems = container.querySelectorAll(".mx_BeaconListItem");
fireEvent.click(listItems[1]!);
});
// centered on clicked beacon
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 33, lon: 22 });
// bounds fitted just to clicked beacon
expect(mockMap.fitBounds).toHaveBeenCalledWith(expectedBounds, fitBoundsOptions);
// each called once per click
expect(mockMap.setCenter).toHaveBeenCalledTimes(2);
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -0,0 +1,93 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentProps } from "react";
import { act, fireEvent, render } from "jest-matrix-react";
import DialogSidebar from "../../../../src/components/views/beacon/DialogSidebar";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
makeRoomWithBeacons,
mockClientMethodsUser,
} from "../../../test-utils";
describe("<DialogSidebar />", () => {
const defaultProps: ComponentProps<typeof DialogSidebar> = {
beacons: [],
requestClose: jest.fn(),
onBeaconClick: jest.fn(),
};
const now = 1647270879403;
const roomId = "!room:server.org";
const aliceId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(aliceId),
getRoom: jest.fn(),
});
const beaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true, timestamp: now }, "$alice-room1-1");
const location1 = makeBeaconEvent(aliceId, {
beaconInfoId: beaconEvent.getId(),
geoUri: "geo:51,41",
timestamp: now,
});
const getComponent = (props = {}) => (
<MatrixClientContext.Provider value={client}>
<DialogSidebar {...defaultProps} {...props} />
</MatrixClientContext.Provider>
);
beforeEach(() => {
// mock now so time based text in snapshots is stable
jest.spyOn(Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(Date, "now").mockRestore();
});
it("renders sidebar correctly without beacons", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders sidebar correctly with beacons", () => {
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
const { container } = render(getComponent({ beacons: [beacon] }));
expect(container).toMatchSnapshot();
});
it("calls on beacon click", () => {
const onBeaconClick = jest.fn();
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
act(() => {
const [listItem] = container.getElementsByClassName("mx_BeaconListItem");
fireEvent.click(listItem);
});
expect(onBeaconClick).toHaveBeenCalled();
});
it("closes on close button click", () => {
const requestClose = jest.fn();
const { getByTestId } = render(getComponent({ requestClose }));
act(() => {
fireEvent.click(getByTestId("dialog-sidebar-close"));
});
expect(requestClose).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,260 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import { act, fireEvent, render } from "jest-matrix-react";
import { Beacon, BeaconIdentifier } from "matrix-js-sdk/src/matrix";
import LeftPanelLiveShareWarning from "../../../../src/components/views/beacon/LeftPanelLiveShareWarning";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../../src/stores/OwnBeaconStore";
import { flushPromises, makeBeaconInfoEvent } from "../../../test-utils";
import dispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
jest.mock("../../../../src/stores/OwnBeaconStore", () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const EventEmitter = require("events");
class MockOwnBeaconStore extends EventEmitter {
public getLiveBeaconIdsWithLocationPublishError = jest.fn().mockReturnValue([]);
public getBeaconById = jest.fn();
public getLiveBeaconIds = jest.fn().mockReturnValue([]);
public readonly beaconUpdateErrors = new Map<BeaconIdentifier, Error>();
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
}
return {
// @ts-ignore
...jest.requireActual("../../../../src/stores/OwnBeaconStore"),
OwnBeaconStore: {
instance: new MockOwnBeaconStore() as unknown as OwnBeaconStore,
},
};
});
describe("<LeftPanelLiveShareWarning />", () => {
const getComponent = (props = {}) => render(<LeftPanelLiveShareWarning {...props} />);
const roomId1 = "!room1:server";
const roomId2 = "!room2:server";
const aliceId = "@alive:server";
const now = 1647270879403;
const HOUR_MS = 3600000;
beforeEach(() => {
jest.spyOn(global.Date, "now").mockReturnValue(now);
jest.spyOn(dispatcher, "dispatch")
.mockClear()
.mockImplementation(() => {});
OwnBeaconStore.instance.beaconUpdateErrors.clear();
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.restoreAllMocks();
});
// 12h old, 12h left
const beacon1 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId1, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
);
// 10h left
const beacon2 = new Beacon(makeBeaconInfoEvent(aliceId, roomId2, { timeout: HOUR_MS * 10, timestamp: now }, "$2"));
it("renders nothing when user has no live beacons", () => {
const { container } = getComponent();
expect(container.innerHTML).toBeFalsy();
});
describe("when user has live location monitor", () => {
beforeAll(() => {
mocked(OwnBeaconStore.instance).getBeaconById.mockImplementation((beaconId) => {
if (beaconId === beacon1.identifier) {
return beacon1;
}
return beacon2;
});
});
beforeEach(() => {
// @ts-ignore writing to readonly variable
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([]);
mocked(OwnBeaconStore.instance).getLiveBeaconIds.mockReturnValue([beacon2.identifier, beacon1.identifier]);
});
afterAll(() => {
jest.spyOn(document, "addEventListener").mockRestore();
});
it("renders correctly when not minimized", () => {
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
it("goes to room of latest beacon when clicked", () => {
const { container } = getComponent();
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
fireEvent.click(container.querySelector("[role=button]")!);
expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
metricsTrigger: undefined,
// latest beacon's room
room_id: roomId2,
event_id: beacon2.beaconInfoId,
highlighted: true,
scroll_into_view: true,
});
});
it("renders correctly when minimized", () => {
const { asFragment } = getComponent({ isMinimized: true });
expect(asFragment()).toMatchSnapshot();
});
it("renders location publish error", () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
beacon1.identifier,
]);
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
it("goes to room of latest beacon with location publish error when clicked", () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
beacon1.identifier,
]);
const { container } = getComponent();
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
fireEvent.click(container.querySelector("[role=button]")!);
expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
metricsTrigger: undefined,
// error beacon's room
room_id: roomId1,
event_id: beacon1.beaconInfoId,
highlighted: true,
scroll_into_view: true,
});
});
it("goes back to default style when wire errors are cleared", () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
beacon1.identifier,
]);
const { container, rerender } = getComponent();
// error mode
expect(container.querySelector(".mx_LeftPanelLiveShareWarning")?.textContent).toEqual(
"An error occurred whilst sharing your live location",
);
act(() => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([]);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LocationPublishError, "abc");
});
rerender(<LeftPanelLiveShareWarning />);
// default mode
expect(container.querySelector(".mx_LeftPanelLiveShareWarning")?.textContent).toEqual(
"You are sharing your live location",
);
});
it("removes itself when user stops having live beacons", async () => {
const { container, rerender } = getComponent({ isMinimized: true });
// started out rendered
expect(container.innerHTML).toBeTruthy();
act(() => {
// @ts-ignore writing to readonly variable
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
});
await flushPromises();
rerender(<LeftPanelLiveShareWarning />);
expect(container.innerHTML).toBeFalsy();
});
it("refreshes beacon liveness monitors when pagevisibilty changes to visible", () => {
OwnBeaconStore.instance.beacons.set(beacon1.identifier, beacon1);
OwnBeaconStore.instance.beacons.set(beacon2.identifier, beacon2);
const beacon1MonitorSpy = jest.spyOn(beacon1, "monitorLiveness");
const beacon2MonitorSpy = jest.spyOn(beacon1, "monitorLiveness");
jest.spyOn(document, "addEventListener").mockImplementation((_e, listener) =>
(listener as EventListener)(new Event("")),
);
expect(beacon1MonitorSpy).not.toHaveBeenCalled();
getComponent();
expect(beacon1MonitorSpy).toHaveBeenCalled();
expect(beacon2MonitorSpy).toHaveBeenCalled();
});
describe("stopping errors", () => {
it("renders stopping error", () => {
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
const { container } = getComponent();
expect(container.textContent).toEqual("An error occurred while stopping your live location");
});
it("starts rendering stopping error on beaconUpdateError emit", () => {
const { container } = getComponent();
// no error
expect(container.textContent).toEqual("You are sharing your live location");
act(() => {
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.BeaconUpdateError, beacon2.identifier, true);
});
expect(container.textContent).toEqual("An error occurred while stopping your live location");
});
it("renders stopping error when beacons have stopping and location errors", () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
beacon1.identifier,
]);
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
const { container } = getComponent();
expect(container.textContent).toEqual("An error occurred while stopping your live location");
});
it("goes to room of latest beacon with stopping error when clicked", () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
beacon1.identifier,
]);
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
const { container } = getComponent();
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
fireEvent.click(container.querySelector("[role=button]")!);
expect(dispatchSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
metricsTrigger: undefined,
// stopping error beacon's room
room_id: beacon2.roomId,
event_id: beacon2.beaconInfoId,
highlighted: true,
scroll_into_view: true,
});
});
});
});
});

View file

@ -0,0 +1,155 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import { Beacon } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import OwnBeaconStatus from "../../../../src/components/views/beacon/OwnBeaconStatus";
import { BeaconDisplayStatus } from "../../../../src/components/views/beacon/displayStatus";
import { useOwnLiveBeacons } from "../../../../src/utils/beacon";
import { makeBeaconInfoEvent } from "../../../test-utils";
jest.mock("../../../../src/utils/beacon/useOwnLiveBeacons", () => ({
useOwnLiveBeacons: jest.fn(),
}));
const defaultLiveBeaconsState = {
onStopSharing: jest.fn(),
onResetLocationPublishError: jest.fn(),
stoppingInProgress: false,
hasStopSharingError: false,
hasLocationPublishError: false,
};
describe("<OwnBeaconStatus />", () => {
const defaultProps = {
displayStatus: BeaconDisplayStatus.Loading,
};
const userId = "@user:server";
const roomId = "!room:server";
let defaultBeacon: Beacon;
const renderComponent = (props: Partial<React.ComponentProps<typeof OwnBeaconStatus>> = {}) =>
render(<OwnBeaconStatus {...defaultProps} {...props} />);
const getRetryButton = () => screen.getByRole("button", { name: "Retry" });
const getStopButton = () => screen.getByRole("button", { name: "Stop" });
beforeEach(() => {
jest.spyOn(global.Date, "now").mockReturnValue(123456789);
mocked(useOwnLiveBeacons).mockClear().mockReturnValue(defaultLiveBeaconsState);
defaultBeacon = new Beacon(makeBeaconInfoEvent(userId, roomId));
});
it("renders without a beacon instance", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
describe("Active state", () => {
it("renders stop button", () => {
const displayStatus = BeaconDisplayStatus.Active;
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
onStopSharing: jest.fn(),
});
renderComponent({ displayStatus, beacon: defaultBeacon });
expect(screen.getByText("Live location enabled")).toBeInTheDocument();
expect(getStopButton()).toBeInTheDocument();
});
it("stops sharing on stop button click", async () => {
const displayStatus = BeaconDisplayStatus.Active;
const onStopSharing = jest.fn();
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
onStopSharing,
});
renderComponent({ displayStatus, beacon: defaultBeacon });
await userEvent.click(getStopButton());
expect(onStopSharing).toHaveBeenCalled();
});
});
describe("errors", () => {
it("renders in error mode when displayStatus is error", () => {
const displayStatus = BeaconDisplayStatus.Error;
renderComponent({ displayStatus });
expect(screen.getByText("Live location error")).toBeInTheDocument();
// no actions for plain error
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
describe("with location publish error", () => {
it("renders in error mode", () => {
const displayStatus = BeaconDisplayStatus.Active;
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
hasLocationPublishError: true,
onResetLocationPublishError: jest.fn(),
});
renderComponent({ displayStatus, beacon: defaultBeacon });
expect(screen.getByText("Live location error")).toBeInTheDocument();
// retry button
expect(getRetryButton()).toBeInTheDocument();
});
it("retry button resets location publish error", async () => {
const displayStatus = BeaconDisplayStatus.Active;
const onResetLocationPublishError = jest.fn();
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
hasLocationPublishError: true,
onResetLocationPublishError,
});
renderComponent({ displayStatus, beacon: defaultBeacon });
await userEvent.click(getRetryButton());
expect(onResetLocationPublishError).toHaveBeenCalled();
});
});
describe("with stopping error", () => {
it("renders in error mode", () => {
const displayStatus = BeaconDisplayStatus.Active;
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
hasLocationPublishError: false,
hasStopSharingError: true,
onStopSharing: jest.fn(),
});
renderComponent({ displayStatus, beacon: defaultBeacon });
expect(screen.getByText("Live location error")).toBeInTheDocument();
// retry button
expect(getRetryButton()).toBeInTheDocument();
});
it("retry button retries stop sharing", async () => {
const displayStatus = BeaconDisplayStatus.Active;
const onStopSharing = jest.fn();
mocked(useOwnLiveBeacons).mockReturnValue({
...defaultLiveBeaconsState,
hasStopSharingError: true,
onStopSharing,
});
renderComponent({ displayStatus, beacon: defaultBeacon });
await userEvent.click(getRetryButton());
expect(onStopSharing).toHaveBeenCalled();
});
});
});
it("renders loading state correctly", () => {
const component = renderComponent();
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,118 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { Room, PendingEventOrdering, MatrixClient, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, Widget } from "matrix-widget-api";
import { act, cleanup, render, screen } from "jest-matrix-react";
import { mocked, Mocked } from "jest-mock";
import { mkRoomMember, MockedCall, setupAsyncStoreWithClient, stubClient, useMockedCalls } from "../../../test-utils";
import RoomCallBanner from "../../../../src/components/views/beacon/RoomCallBanner";
import { CallStore } from "../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { ConnectionState } from "../../../../src/models/Call";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
describe("<RoomCallBanner />", () => {
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
useMockedCalls();
const defaultProps = {
roomId: "!1:example.org",
};
beforeEach(() => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
alice = mkRoomMember(room.roomId, "@alice:example.org");
jest.spyOn(room, "getMember").mockImplementation((userId) => (userId === alice.userId ? alice : null));
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
client.getRooms.mockReturnValue([room]);
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
setupAsyncStoreWithClient(CallStore.instance, client);
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
});
afterEach(async () => {
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
});
const renderBanner = async (props = {}): Promise<void> => {
render(<RoomCallBanner {...defaultProps} {...props} />);
await act(() => Promise.resolve()); // Let effects settle
};
it("renders nothing when there is no call", async () => {
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
describe("call started", () => {
let call: MockedCall;
let widget: Widget;
beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) {
throw new Error("Failed to create call");
}
call = maybeCall;
widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
});
afterEach(() => {
cleanup(); // Unmount before we do any cleanup that might update the component
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
});
it("renders if there is a call", async () => {
await renderBanner();
await screen.findByText("Video call");
});
it("shows Join button if the user has not joined", async () => {
await renderBanner();
await screen.findByText("Join");
});
it("doesn't show banner if the call is connected", async () => {
call.setConnectionState(ConnectionState.Connected);
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
it("doesn't show banner if the call is shown", async () => {
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall");
mocked(SdkContextClass.instance.roomViewStore.isViewingCall).mockReturnValue(true);
await renderBanner();
const banner = await screen.queryByText("Video call");
expect(banner).toBeFalsy();
});
});
// TODO: test clicking buttons
// TODO: add live location share warning test (should not render if there is an active live location share)
});

View file

@ -0,0 +1,47 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render } from "jest-matrix-react";
import ShareLatestLocation from "../../../../src/components/views/beacon/ShareLatestLocation";
import { copyPlaintext } from "../../../../src/utils/strings";
import { flushPromises } from "../../../test-utils";
jest.mock("../../../../src/utils/strings", () => ({
copyPlaintext: jest.fn().mockResolvedValue(undefined),
}));
describe("<ShareLatestLocation />", () => {
const defaultProps = {
latestLocationState: {
uri: "geo:51,42;u=35",
timestamp: 123,
},
};
const getComponent = (props = {}) => render(<ShareLatestLocation {...defaultProps} {...props} />);
beforeEach(() => {
jest.clearAllMocks();
});
it("renders null when no location", () => {
const { container } = getComponent({ latestLocationState: undefined });
expect(container.innerHTML).toBeFalsy();
});
it("renders share buttons when there is a location", async () => {
const { container, asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
fireEvent.click(container.querySelector(".mx_CopyableText_copyButton")!);
await flushPromises();
expect(copyPlaintext).toHaveBeenCalledWith("51,42");
});
});

View file

@ -0,0 +1,22 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import StyledLiveBeaconIcon from "../../../../src/components/views/beacon/StyledLiveBeaconIcon";
describe("<StyledLiveBeaconIcon />", () => {
const defaultProps = {};
const getComponent = (props = {}) => render(<StyledLiveBeaconIcon {...defaultProps} {...props} />);
it("renders", () => {
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `
<DocumentFragment>
<li
class="mx_BeaconListItem"
>
<div
class="mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon"
/>
<div
class="mx_BeaconListItem_info"
>
<div
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_label"
>
Alice's car
</span>
<span
class="mx_BeaconStatus_expiryTime"
>
Live until 16:04
</span>
</div>
<div
class="mx_BeaconListItem_interactions"
>
<a
aria-labelledby="floating-ui-1"
data-testid="open-location-in-osm"
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
rel="noreferrer noopener"
target="_blank"
>
<div
class="mx_ShareLatestLocation_icon"
/>
</a>
<div
class="mx_CopyableText mx_ShareLatestLocation_copy"
>
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
</div>
</div>
<span
class="mx_BeaconListItem_lastUpdated"
>
Updated a few seconds ago
</span>
</div>
</li>
</DocumentFragment>
`;

View file

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
<DocumentFragment>
<span>
<div
class="mx_Marker mx_Username_color6"
id="!room:server_@alice:server"
>
<div
class="mx_Marker_border"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="6"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
title="@alice:server"
>
a
</span>
</div>
</div>
</span>
</DocumentFragment>
`;

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconStatus /> active state renders with children 1`] = `
<span
data-testid="test-child"
>
test
</span>
`;
exports[`<BeaconStatus /> active state renders without children 1`] = `
<DocumentFragment>
<div
class="mx_BeaconStatus mx_BeaconStatus_Active"
>
<div
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon"
/>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_label"
>
test label
</span>
<span
class="mx_BeaconStatus_expiryTime"
>
Live until 11:17
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`<BeaconStatus /> renders loading state 1`] = `
<DocumentFragment>
<div
class="mx_BeaconStatus mx_BeaconStatus_Loading"
>
<div
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle"
/>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_description_status"
>
Loading live location…
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`<BeaconStatus /> renders stopped state 1`] = `
<DocumentFragment>
<div
class="mx_BeaconStatus mx_BeaconStatus_Stopped"
>
<div
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle"
/>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_description_status"
>
Live location ended
</span>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconViewDialog /> renders a fallback when there are no locations 1`] = `
<div
class="mx_MapFallback mx_BeaconViewDialog_map"
data-testid="beacon-view-dialog-map-fallback"
>
<div
class="mx_MapFallback_bg"
/>
<div
class="mx_MapFallback_icon"
/>
<span
class="mx_BeaconViewDialog_mapFallbackMessage"
>
No live locations
</span>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-testid="beacon-view-dialog-fallback-close"
role="button"
tabindex="0"
>
Close
</div>
</div>
`;
exports[`<BeaconViewDialog /> renders own beacon status when user is live sharing 1`] = `
<div
class="mx_DialogOwnBeaconStatus"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_DialogOwnBeaconStatus_avatar _avatar-imageless_mcap2_61"
data-color="6"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@alice:server"
>
a
</span>
<div
class="mx_BeaconStatus mx_BeaconStatus_Active mx_DialogOwnBeaconStatus_status"
>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_label"
>
Live location enabled
</span>
<span
class="mx_LiveTimeRemaining"
data-testid="room-live-share-expiry"
>
1h left
</span>
</div>
<div
class="mx_AccessibleButton mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
data-testid="beacon-status-stop-beacon"
role="button"
tabindex="0"
>
Stop
</div>
</div>
</div>
`;

View file

@ -0,0 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
<div>
<div
class="mx_DialogSidebar"
>
<div
class="mx_DialogSidebar_header"
>
<h4
class="mx_Heading_h4"
>
View list
</h4>
<div
aria-label="Close sidebar"
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
data-testid="dialog-sidebar-close"
role="button"
tabindex="0"
>
<svg
class="mx_DialogSidebar_closeButtonIcon"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
/>
</svg>
</div>
</div>
<ol
class="mx_DialogSidebar_list"
>
<li
class="mx_BeaconListItem"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_BeaconListItem_avatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
</span>
<div
class="mx_BeaconListItem_info"
>
<div
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_label"
>
@alice:server.org
</span>
<span
class="mx_BeaconStatus_expiryTime"
>
Live until 16:14
</span>
</div>
<div
class="mx_BeaconListItem_interactions"
>
<a
aria-labelledby="floating-ui-8"
data-testid="open-location-in-osm"
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
rel="noreferrer noopener"
target="_blank"
>
<div
class="mx_ShareLatestLocation_icon"
/>
</a>
<div
class="mx_CopyableText mx_ShareLatestLocation_copy"
>
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
</div>
</div>
<span
class="mx_BeaconListItem_lastUpdated"
>
Updated a few seconds ago
</span>
</div>
</li>
</ol>
</div>
</div>
`;
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
<div>
<div
class="mx_DialogSidebar"
>
<div
class="mx_DialogSidebar_header"
>
<h4
class="mx_Heading_h4"
>
View list
</h4>
<div
aria-label="Close sidebar"
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
data-testid="dialog-sidebar-close"
role="button"
tabindex="0"
>
<svg
class="mx_DialogSidebar_closeButtonIcon"
fill="currentColor"
height="24px"
viewBox="0 0 24 24"
width="24px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
/>
</svg>
</div>
</div>
<div
class="mx_DialogSidebar_noResults"
>
No live locations
</div>
</div>
</div>
`;

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
<DocumentFragment>
<div
aria-label="You are sharing your live location"
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
role="button"
tabindex="0"
>
<div
height="10"
/>
</div>
</DocumentFragment>
`;
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning"
role="button"
tabindex="0"
>
You are sharing your live location
</div>
</DocumentFragment>
`;
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders location publish error 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
role="button"
tabindex="0"
>
An error occurred whilst sharing your live location
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<OwnBeaconStatus /> renders without a beacon instance 1`] = `
<DocumentFragment>
<div
class="mx_BeaconStatus mx_BeaconStatus_Loading"
>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_description_status"
>
Loading live location…
</span>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ShareLatestLocation /> renders share buttons when there is a location 1`] = `
<DocumentFragment>
<a
aria-labelledby="floating-ui-1"
data-testid="open-location-in-osm"
href="https://www.openstreetmap.org/?mlat=51&mlon=42#map=16/51/42"
rel="noreferrer noopener"
target="_blank"
>
<div
class="mx_ShareLatestLocation_icon"
/>
</a>
<div
class="mx_CopyableText mx_ShareLatestLocation_copy"
>
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<StyledLiveBeaconIcon /> renders 1`] = `
<DocumentFragment>
<div
class="mx_StyledLiveBeaconIcon"
/>
</DocumentFragment>
`;