/*
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, Beacon, BeaconEvent, getBeaconInfoIdentifier, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { act, fireEvent, getByTestId, render, screen, waitFor } from "@testing-library/react";
import RoomLiveShareWarning from "../../../../src/components/views/beacon/RoomLiveShareWarning";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../../src/stores/OwnBeaconStore";
import {
advanceDateAndTime,
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter,
makeBeaconInfoEvent,
mockGeolocation,
resetAsyncStoreWithClient,
setupAsyncStoreWithClient,
} from "../../../test-utils";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
jest.useFakeTimers();
describe("", () => {
const aliceId = "@alice:server.org";
const room1Id = "$room1:server.org";
const room2Id = "$room2:server.org";
const room3Id = "$room3:server.org";
const mockClient = getMockClientWithEventEmitter({
getVisibleRooms: jest.fn().mockReturnValue([]),
getUserId: jest.fn().mockReturnValue(aliceId),
getSafeUserId: jest.fn().mockReturnValue(aliceId),
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }),
sendEvent: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
});
// 14.03.2022 16:15
const now = 1647270879403;
const MINUTE_MS = 60000;
const HOUR_MS = 3600000;
// mock the date so events are stable for snapshots etc
jest.spyOn(global.Date, "now").mockReturnValue(now);
const room1Beacon1 = makeBeaconInfoEvent(
aliceId,
room1Id,
{
isLive: true,
timeout: HOUR_MS,
},
"$0",
);
const room2Beacon1 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS }, "$1");
const room2Beacon2 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS * 12 }, "$2");
const room3Beacon1 = makeBeaconInfoEvent(aliceId, room3Id, { isLive: true, timeout: HOUR_MS }, "$3");
// make fresh rooms every time
// as we update room state
const makeRoomsWithStateEvents = (stateEvents: MatrixEvent[] = []): [Room, Room] => {
const room1 = new Room(room1Id, mockClient, aliceId);
const room2 = new Room(room2Id, mockClient, aliceId);
room1.currentState.setStateEvents(stateEvents);
room2.currentState.setStateEvents(stateEvents);
mockClient.getVisibleRooms.mockReturnValue([room1, room2]);
return [room1, room2];
};
const makeOwnBeaconStore = async () => {
const store = OwnBeaconStore.instance;
await setupAsyncStoreWithClient(store, mockClient);
return store;
};
const defaultProps = {
roomId: room1Id,
};
const getComponent = (props = {}) => {
return render();
};
const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined);
beforeEach(() => {
mockGeolocation();
jest.spyOn(global.Date, "now").mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: "1" });
// assume all beacons were created on this device
localStorageSpy.mockReturnValue(
JSON.stringify([room1Beacon1.getId(), room2Beacon1.getId(), room2Beacon2.getId(), room3Beacon1.getId()]),
);
});
afterEach(async () => {
jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockRestore();
await resetAsyncStoreWithClient(OwnBeaconStore.instance);
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
localStorageSpy.mockRestore();
jest.spyOn(defaultDispatcher, "dispatch").mockRestore();
});
it("renders nothing when user has no live beacons at all", async () => {
await makeOwnBeaconStore();
const { asFragment } = getComponent();
expect(asFragment()).toMatchInlineSnapshot(``);
});
it("renders nothing when user has no live beacons in room", async () => {
await act(async () => {
await makeRoomsWithStateEvents([room2Beacon1]);
await makeOwnBeaconStore();
});
const { asFragment } = getComponent({ roomId: room1Id });
expect(asFragment()).toMatchInlineSnapshot(``);
});
it("does not render when geolocation is not working", async () => {
jest.spyOn(logger, "error").mockImplementation(() => {});
// @ts-ignore
navigator.geolocation = undefined;
await act(async () => {
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
await makeOwnBeaconStore();
});
const { asFragment } = getComponent({ roomId: room1Id });
expect(asFragment()).toMatchInlineSnapshot(``);
});
describe("when user has live beacons and geolocation is available", () => {
beforeEach(async () => {
await act(async () => {
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
await makeOwnBeaconStore();
});
});
it("renders correctly with one live beacon in room", () => {
const { asFragment } = getComponent({ roomId: room1Id });
// beacons have generated ids that break snapshots
// assert on html
expect(asFragment()).toMatchSnapshot();
});
it("renders correctly with two live beacons in room", () => {
const { asFragment, container } = getComponent({ roomId: room2Id });
// beacons have generated ids that break snapshots
// assert on html
expect(asFragment()).toMatchSnapshot();
// later expiry displayed
expect(container).toHaveTextContent("12h left");
});
it("removes itself when user stops having live beacons", async () => {
const { container } = getComponent({ roomId: room1Id });
// started out rendered
expect(container.firstChild).toBeTruthy();
// time travel until room1Beacon1 is expired
act(() => {
advanceDateAndTime(HOUR_MS + 1);
});
act(() => {
mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1));
});
await waitFor(() => expect(container.firstChild).toBeFalsy());
});
it("removes itself when user stops monitoring live position", async () => {
const { container } = getComponent({ roomId: room1Id });
// started out rendered
expect(container.firstChild).toBeTruthy();
act(() => {
// cheat to clear this
// @ts-ignore
OwnBeaconStore.instance.clearPositionWatch = undefined;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
});
await waitFor(() => expect(container.firstChild).toBeFalsy());
});
it("renders when user adds a live beacon", async () => {
const { container } = getComponent({ roomId: room3Id });
// started out not rendered
expect(container.firstChild).toBeFalsy();
act(() => {
mockClient.emit(BeaconEvent.New, room3Beacon1, new Beacon(room3Beacon1));
});
await waitFor(() => expect(container.firstChild).toBeTruthy());
});
it("updates beacon time left periodically", () => {
const { container } = getComponent({ roomId: room1Id });
expect(container).toHaveTextContent("1h left");
act(() => {
advanceDateAndTime(MINUTE_MS * 25);
});
expect(container).toHaveTextContent("35m left");
});
it("updates beacon time left when beacon updates", () => {
const { container } = getComponent({ roomId: room1Id });
expect(container).toHaveTextContent("1h left");
act(() => {
const beacon = OwnBeaconStore.instance.getBeaconById(getBeaconInfoIdentifier(room1Beacon1));
const room1Beacon1Update = makeBeaconInfoEvent(
aliceId,
room1Id,
{
isLive: true,
timeout: 3 * HOUR_MS,
},
"$0",
);
beacon?.update(room1Beacon1Update);
});
// update to expiry of new beacon
expect(container).toHaveTextContent("3h left");
});
it("clears expiry time interval on unmount", () => {
const clearIntervalSpy = jest.spyOn(global, "clearInterval");
const { container, unmount } = getComponent({ roomId: room1Id });
expect(container).toHaveTextContent("1h left");
unmount();
expect(clearIntervalSpy).toHaveBeenCalled();
});
it("navigates to beacon tile on click", () => {
const dispatcherSpy = jest.spyOn(defaultDispatcher, "dispatch");
const { container } = getComponent({ roomId: room1Id });
act(() => {
fireEvent.click(container.firstChild! as Node);
});
expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
event_id: room1Beacon1.getId(),
room_id: room1Id,
highlighted: true,
scroll_into_view: true,
metricsTrigger: undefined,
});
});
describe("stopping beacons", () => {
it("stops beacon on stop sharing click", async () => {
const { container } = getComponent({ roomId: room2Id });
const btn = getByTestId(container, "room-live-share-primary-button");
fireEvent.click(btn);
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
await waitFor(() => expect(screen.queryByTestId("spinner")).toBeInTheDocument());
expect(btn.hasAttribute("disabled")).toBe(true);
});
it("displays error when stop sharing fails", async () => {
const { container, asFragment } = getComponent({ roomId: room1Id });
const btn = getByTestId(container, "room-live-share-primary-button");
// fail first time
mockClient.unstable_setLiveBeacon
.mockRejectedValueOnce(new Error("oups"))
.mockResolvedValue({ event_id: "1" });
await act(async () => {
fireEvent.click(btn);
await flushPromisesWithFakeTimers();
});
expect(asFragment()).toMatchSnapshot();
act(() => {
fireEvent.click(btn);
});
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
});
it("displays again with correct state after stopping a beacon", () => {
// make sure the loading state is reset correctly after removing a beacon
const { container } = getComponent({ roomId: room1Id });
const btn = getByTestId(container, "room-live-share-primary-button");
// stop the beacon
act(() => {
fireEvent.click(btn);
});
// time travel until room1Beacon1 is expired
act(() => {
advanceDateAndTime(HOUR_MS + 1);
});
act(() => {
mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1));
});
const newLiveBeacon = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true });
act(() => {
mockClient.emit(BeaconEvent.New, newLiveBeacon, new Beacon(newLiveBeacon));
});
// button not disabled and expiry time shown
expect(btn.hasAttribute("disabled")).toBe(true);
});
});
describe("with location publish errors", () => {
it("displays location publish error when mounted with location publish errors", async () => {
const locationPublishErrorSpy = jest
.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError")
.mockReturnValue(true);
const { asFragment } = getComponent({ roomId: room2Id });
expect(asFragment()).toMatchSnapshot();
expect(locationPublishErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1), 0, [
getBeaconInfoIdentifier(room2Beacon1),
]);
});
it(
"displays location publish error when locationPublishError event is emitted" +
" and beacons have errors",
async () => {
const locationPublishErrorSpy = jest
.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError")
.mockReturnValue(false);
const { container } = getComponent({ roomId: room2Id });
// update mock and emit event
act(() => {
locationPublishErrorSpy.mockReturnValue(true);
OwnBeaconStore.instance.emit(
OwnBeaconStoreEvent.LocationPublishError,
getBeaconInfoIdentifier(room2Beacon1),
);
});
// renders wire error ui
expect(container).toHaveTextContent(
"An error occurred whilst sharing your live location, please try again",
);
expect(screen.queryByTestId("room-live-share-wire-error-close-button")).toBeInTheDocument();
},
);
it("stops displaying wire error when errors are cleared", async () => {
const locationPublishErrorSpy = jest
.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError")
.mockReturnValue(true);
const { container } = getComponent({ roomId: room2Id });
// update mock and emit event
act(() => {
locationPublishErrorSpy.mockReturnValue(false);
OwnBeaconStore.instance.emit(
OwnBeaconStoreEvent.LocationPublishError,
getBeaconInfoIdentifier(room2Beacon1),
);
});
// renders error-free ui
expect(container).toHaveTextContent("You are sharing your live location");
expect(screen.queryByTestId("room-live-share-wire-error-close-button")).not.toBeInTheDocument();
});
it("clicking retry button resets location publish errors", async () => {
jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockReturnValue(true);
const resetErrorSpy = jest.spyOn(OwnBeaconStore.instance, "resetLocationPublishError");
const { container } = getComponent({ roomId: room2Id });
const btn = getByTestId(container, "room-live-share-primary-button");
act(() => {
fireEvent.click(btn);
});
expect(resetErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
});
it("clicking close button stops beacons", async () => {
jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockReturnValue(true);
const stopBeaconSpy = jest.spyOn(OwnBeaconStore.instance, "stopBeacon");
const { container } = getComponent({ roomId: room2Id });
const btn = getByTestId(container, "room-live-share-wire-error-close-button");
act(() => {
fireEvent.click(btn);
});
expect(stopBeaconSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
});
});
});
});