Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0f670b8dc0
commit
b084ff2313
807 changed files with 0 additions and 0 deletions
275
test/unit-tests/components/structures/AutocompleteInput-test.tsx
Normal file
275
test/unit-tests/components/structures/AutocompleteInput-test.tsx
Normal file
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
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 { screen, render, fireEvent, waitFor, within, act } from "jest-matrix-react";
|
||||
|
||||
import * as TestUtils from "../../test-utils";
|
||||
import AutocompleteProvider from "../../../src/autocomplete/AutocompleteProvider";
|
||||
import { ICompletion } from "../../../src/autocomplete/Autocompleter";
|
||||
import { AutocompleteInput } from "../../../src/components/structures/AutocompleteInput";
|
||||
|
||||
describe("AutocompleteInput", () => {
|
||||
const mockCompletion: ICompletion[] = [
|
||||
{
|
||||
type: "user",
|
||||
completion: "user_1",
|
||||
completionId: "@user_1:host.local",
|
||||
range: { start: 1, end: 1 },
|
||||
component: <div />,
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
completion: "user_2",
|
||||
completionId: "@user_2:host.local",
|
||||
range: { start: 1, end: 1 },
|
||||
component: <div />,
|
||||
},
|
||||
];
|
||||
|
||||
const constructMockProvider = (data: ICompletion[]) =>
|
||||
({
|
||||
getCompletions: jest.fn().mockImplementation(async () => data),
|
||||
}) as unknown as AutocompleteProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
TestUtils.stubClient();
|
||||
});
|
||||
|
||||
const getEditorInput = () => {
|
||||
const input = screen.getByTestId("autocomplete-input");
|
||||
expect(input).toBeDefined();
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
it("should render suggestions when a query is set", async () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={[]}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(input);
|
||||
fireEvent.change(input, { target: { value: "user" } });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByTestId("autocomplete-matches").childNodes).toHaveLength(mockCompletion.length);
|
||||
});
|
||||
|
||||
it("should render selected items passed in via props", () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editor = screen.getByTestId("autocomplete-editor");
|
||||
const selection = within(editor).getAllByTestId("autocomplete-selection-item", { exact: false });
|
||||
expect(selection).toHaveLength(mockCompletion.length);
|
||||
});
|
||||
|
||||
it("should call onSelectionChange() when an item is removed from selection", () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editor = screen.getByTestId("autocomplete-editor");
|
||||
const removeButtons = within(editor).getAllByTestId("autocomplete-selection-remove-button", { exact: false });
|
||||
expect(removeButtons).toHaveLength(mockCompletion.length);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(removeButtons[0]);
|
||||
});
|
||||
|
||||
expect(onSelectionChangeMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSelectionChangeMock).toHaveBeenCalledWith([mockCompletion[1]]);
|
||||
});
|
||||
|
||||
it("should render custom selection element when renderSelection() is defined", () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
const renderSelection = () => <span data-testid="custom-selection-element">custom selection element</span>;
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
renderSelection={renderSelection}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByTestId("custom-selection-element")).toHaveLength(mockCompletion.length);
|
||||
});
|
||||
|
||||
it("should render custom suggestion element when renderSuggestion() is defined", async () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
const renderSuggestion = () => <span data-testid="custom-suggestion-element">custom suggestion element</span>;
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
renderSuggestion={renderSuggestion}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(input);
|
||||
fireEvent.change(input, { target: { value: "user" } });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getAllByTestId("custom-suggestion-element")).toHaveLength(mockCompletion.length);
|
||||
});
|
||||
|
||||
it("should mark selected suggestions as selected", async () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
const { container } = render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(input);
|
||||
fireEvent.change(input, { target: { value: "user" } });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockProvider.getCompletions).toHaveBeenCalledTimes(1));
|
||||
const suggestions = await within(container).findAllByTestId("autocomplete-suggestion-item", { exact: false });
|
||||
expect(suggestions).toHaveLength(mockCompletion.length);
|
||||
suggestions.map((suggestion) => expect(suggestion).toHaveClass("mx_AutocompleteInput_suggestion--selected"));
|
||||
});
|
||||
|
||||
it("should remove the last added selection when backspace is pressed in empty input", () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={mockCompletion}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(input, { key: "Backspace" });
|
||||
});
|
||||
|
||||
expect(onSelectionChangeMock).toHaveBeenCalledWith([mockCompletion[0]]);
|
||||
});
|
||||
|
||||
it("should toggle a selected item when a suggestion is clicked", async () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
const { container } = render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={[]}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(input);
|
||||
fireEvent.change(input, { target: { value: "user" } });
|
||||
});
|
||||
|
||||
const suggestions = await within(container).findAllByTestId("autocomplete-suggestion-item", { exact: false });
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseDown(suggestions[0]);
|
||||
});
|
||||
|
||||
expect(onSelectionChangeMock).toHaveBeenCalledWith([mockCompletion[0]]);
|
||||
});
|
||||
|
||||
it("should clear text field and suggestions when a suggestion is accepted", async () => {
|
||||
const mockProvider = constructMockProvider(mockCompletion);
|
||||
const onSelectionChangeMock = jest.fn();
|
||||
|
||||
const { container } = render(
|
||||
<AutocompleteInput
|
||||
provider={mockProvider}
|
||||
placeholder="Search ..."
|
||||
selection={[]}
|
||||
onSelectionChange={onSelectionChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = getEditorInput();
|
||||
|
||||
act(() => {
|
||||
fireEvent.focus(input);
|
||||
fireEvent.change(input, { target: { value: "user" } });
|
||||
});
|
||||
|
||||
const suggestions = await within(container).findAllByTestId("autocomplete-suggestion-item", { exact: false });
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseDown(suggestions[0]);
|
||||
});
|
||||
|
||||
expect(input).toHaveValue("");
|
||||
expect(within(container).queryAllByTestId("autocomplete-suggestion-item", { exact: false })).toHaveLength(0);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
});
|
79
test/unit-tests/components/structures/ContextMenu-test.ts
Normal file
79
test/unit-tests/components/structures/ContextMenu-test.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 { toLeftOf, toLeftOrRightOf, toRightOf } from "../../../src/components/structures/ContextMenu";
|
||||
import UIStore from "../../../src/stores/UIStore";
|
||||
|
||||
describe("ContextMenu", () => {
|
||||
const rect = new DOMRect();
|
||||
// @ts-ignore
|
||||
rect.left = 23;
|
||||
// @ts-ignore
|
||||
rect.right = 46;
|
||||
// @ts-ignore
|
||||
rect.top = 42;
|
||||
rect.width = 640;
|
||||
rect.height = 480;
|
||||
|
||||
beforeEach(() => {
|
||||
window.scrollX = 31;
|
||||
window.scrollY = 41;
|
||||
UIStore.instance.windowWidth = 1280;
|
||||
});
|
||||
|
||||
describe("toLeftOf", () => {
|
||||
it("should return the correct positioning", () => {
|
||||
expect(toLeftOf(rect)).toEqual({
|
||||
chevronOffset: 12,
|
||||
right: 1285, // 1280 - 23 + 31 - 3
|
||||
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toRightOf", () => {
|
||||
it("should return the correct positioning", () => {
|
||||
expect(toRightOf(rect)).toEqual({
|
||||
chevronOffset: 12,
|
||||
left: 80, // 46 + 31 + 3
|
||||
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toLeftOrRightOf", () => {
|
||||
describe("when there is more space to the right", () => {
|
||||
// default case from test setup
|
||||
|
||||
it("should return a position to the right", () => {
|
||||
expect(toLeftOrRightOf(rect)).toEqual({
|
||||
chevronOffset: 12,
|
||||
left: 80, // 46 + 31 + 3
|
||||
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is more space to the left", () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
rect.left = 500;
|
||||
// @ts-ignore
|
||||
rect.right = 1000;
|
||||
});
|
||||
|
||||
it("should return a position to the left", () => {
|
||||
expect(toLeftOrRightOf(rect)).toEqual({
|
||||
chevronOffset: 12,
|
||||
right: 808, // 1280 - 500 + 31 - 3
|
||||
top: 303, // 42 + (480 / 2) + 41 - (12 + 8)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
50
test/unit-tests/components/structures/FilePanel-test.tsx
Normal file
50
test/unit-tests/components/structures/FilePanel-test.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { screen, render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import FilePanel from "../../../src/components/structures/FilePanel";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||
TimelineWindow: jest.fn().mockReturnValue({
|
||||
load: jest.fn().mockResolvedValue(null),
|
||||
getEvents: jest.fn().mockReturnValue([]),
|
||||
canPaginate: jest.fn().mockReturnValue(false),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("FilePanel", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("renders empty state", async () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const room = new Room("!room:server", cli, cli.getSafeUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
|
||||
mocked(cli.getRoom).mockReturnValue(room);
|
||||
|
||||
const { asFragment } = render(
|
||||
<FilePanel roomId={room.roomId} onClose={jest.fn()} resizeNotifier={new ResizeNotifier()} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No files visible in this room")).toBeInTheDocument();
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
24
test/unit-tests/components/structures/LargeLoader-test.tsx
Normal file
24
test/unit-tests/components/structures/LargeLoader-test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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 { LargeLoader } from "../../../src/components/structures/LargeLoader";
|
||||
|
||||
describe("LargeLoader", () => {
|
||||
const text = "test loading text";
|
||||
|
||||
beforeEach(() => {
|
||||
render(<LargeLoader text={text} />);
|
||||
});
|
||||
|
||||
it("should render the text", () => {
|
||||
screen.getByText(text);
|
||||
});
|
||||
});
|
46
test/unit-tests/components/structures/LeftPanel-test.tsx
Normal file
46
test/unit-tests/components/structures/LeftPanel-test.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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, RenderResult, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import LeftPanel from "../../../src/components/structures/LeftPanel";
|
||||
import PageType from "../../../src/PageTypes";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("LeftPanel", () => {
|
||||
function renderComponent(): RenderResult {
|
||||
return render(
|
||||
<LeftPanel isMinimized={false} pageType={PageType.RoomView} resizeNotifier={new ResizeNotifier()} />,
|
||||
);
|
||||
}
|
||||
|
||||
it("does not show filter container when disabled by UIComponent customisations", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
renderComponent();
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.FilterContainer);
|
||||
expect(screen.queryByRole("button", { name: /search/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Explore rooms" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders filter container when enabled by UIComponent customisations", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
renderComponent();
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.FilterContainer);
|
||||
expect(screen.getByRole("button", { name: /search/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Explore rooms" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { CallState } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import LegacyCallEventGrouper from "../../../src/components/structures/LegacyCallEventGrouper";
|
||||
|
||||
const MY_USER_ID = "@me:here";
|
||||
const THEIR_USER_ID = "@they:here";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe("LegacyCallEventGrouper", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
client.getUserId = () => {
|
||||
return MY_USER_ID;
|
||||
};
|
||||
});
|
||||
|
||||
it("detects a missed call", () => {
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
// This assumes that the other party aborted the call by sending a hangup,
|
||||
// which is the usual case. Another possible test would be for the edge
|
||||
// case where there is only an expired invite event.
|
||||
grouper.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallInvite;
|
||||
},
|
||||
sender: {
|
||||
userId: THEIR_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
grouper.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallHangup;
|
||||
},
|
||||
sender: {
|
||||
userId: THEIR_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
expect(grouper.state).toBe(CallState.Ended);
|
||||
expect(grouper.callWasMissed).toBe(true);
|
||||
});
|
||||
|
||||
it("detects an ended call", () => {
|
||||
const grouperHangup = new LegacyCallEventGrouper();
|
||||
const grouperReject = new LegacyCallEventGrouper();
|
||||
|
||||
grouperHangup.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallInvite;
|
||||
},
|
||||
sender: {
|
||||
userId: MY_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
grouperHangup.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallHangup;
|
||||
},
|
||||
sender: {
|
||||
userId: THEIR_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
grouperReject.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallInvite;
|
||||
},
|
||||
sender: {
|
||||
userId: MY_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
grouperReject.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallReject;
|
||||
},
|
||||
sender: {
|
||||
userId: THEIR_USER_ID,
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
expect(grouperHangup.state).toBe(CallState.Ended);
|
||||
expect(grouperReject.state).toBe(CallState.Ended);
|
||||
});
|
||||
|
||||
it("detects call type", () => {
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
grouper.add({
|
||||
getContent: () => {
|
||||
return {
|
||||
call_id: "callId",
|
||||
offer: {
|
||||
sdp: "this is definitely an SDP m=video",
|
||||
},
|
||||
};
|
||||
},
|
||||
getType: () => {
|
||||
return EventType.CallInvite;
|
||||
},
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
expect(grouper.isVoice).toBe(false);
|
||||
});
|
||||
});
|
460
test/unit-tests/components/structures/LoggedInView-test.tsx
Normal file
460
test/unit-tests/components/structures/LoggedInView-test.tsx
Normal file
|
@ -0,0 +1,460 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2015-2023 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, RenderResult } from "jest-matrix-react";
|
||||
import { ConditionKind, EventType, IPushRule, MatrixEvent, ClientEvent, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import LoggedInView from "../../../src/components/structures/LoggedInView";
|
||||
import { SDKContext } from "../../../src/contexts/SDKContext";
|
||||
import { StandardActions } from "../../../src/notifications/StandardActions";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils";
|
||||
import { TestSdkContext } from "../../TestSdkContext";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import Modal from "../../../src/Modal";
|
||||
import { SETTINGS } from "../../../src/settings/Settings";
|
||||
|
||||
describe("<LoggedInView />", () => {
|
||||
const userId = "@alice:domain.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getAccountData: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getSyncState: jest.fn().mockReturnValue(null),
|
||||
getSyncStateData: jest.fn().mockReturnValue(null),
|
||||
getMediaHandler: jest.fn(),
|
||||
setPushRuleEnabled: jest.fn(),
|
||||
setPushRuleActions: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(undefined),
|
||||
setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
|
||||
deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
|
||||
});
|
||||
const mediaHandler = new MediaHandler(mockClient);
|
||||
const mockSdkContext = new TestSdkContext();
|
||||
|
||||
const defaultProps = {
|
||||
matrixClient: mockClient,
|
||||
onRegistered: jest.fn(),
|
||||
resizeNotifier: new ResizeNotifier(),
|
||||
collapseLhs: false,
|
||||
hideToSRUsers: false,
|
||||
config: {
|
||||
brand: "Test",
|
||||
element_call: {},
|
||||
},
|
||||
currentRoomId: "",
|
||||
currentUserId: "@bob:server",
|
||||
};
|
||||
|
||||
const getComponent = (props = {}): RenderResult =>
|
||||
render(<LoggedInView {...defaultProps} {...props} />, {
|
||||
wrapper: ({ children }) => <SDKContext.Provider value={mockSdkContext}>{children}</SDKContext.Provider>,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.getMediaHandler.mockReturnValue(mediaHandler);
|
||||
mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe("synced push rules", () => {
|
||||
const pushRulesEvent = new MatrixEvent({ type: EventType.PushRules });
|
||||
|
||||
const oneToOneRule = {
|
||||
conditions: [
|
||||
{ kind: ConditionKind.RoomMemberCount, is: "2" },
|
||||
{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" },
|
||||
],
|
||||
actions: StandardActions.ACTION_NOTIFY,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const oneToOneRuleDisabled = {
|
||||
...oneToOneRule,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
const groupRule = {
|
||||
conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" }],
|
||||
actions: StandardActions.ACTION_NOTIFY,
|
||||
rule_id: ".m.rule.message",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const pollStartOneToOne = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.RoomMemberCount,
|
||||
is: "2",
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.start",
|
||||
},
|
||||
],
|
||||
actions: StandardActions.ACTION_NOTIFY,
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const pollEndOneToOne = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.RoomMemberCount,
|
||||
is: "2",
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.end",
|
||||
},
|
||||
],
|
||||
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
const pollStartGroup = {
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3381.poll.start",
|
||||
},
|
||||
],
|
||||
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
rule_id: ".org.matrix.msc3930.rule.poll_start",
|
||||
default: true,
|
||||
enabled: true,
|
||||
} as IPushRule;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getAccountData.mockImplementation((eventType: string) =>
|
||||
eventType === EventType.PushRules ? pushRulesEvent : undefined,
|
||||
);
|
||||
setPushRules([]);
|
||||
// stub out error logger to avoid littering console
|
||||
jest.spyOn(logger, "error")
|
||||
.mockClear()
|
||||
.mockImplementation(() => {});
|
||||
|
||||
mockClient.setPushRuleActions.mockClear();
|
||||
mockClient.setPushRuleEnabled.mockClear();
|
||||
});
|
||||
|
||||
const setPushRules = (rules: IPushRule[] = []): void => {
|
||||
const pushRules = {
|
||||
global: {
|
||||
underride: [...rules],
|
||||
},
|
||||
};
|
||||
|
||||
mockClient.pushRules = pushRules;
|
||||
};
|
||||
|
||||
describe("on mount", () => {
|
||||
it("handles when user has no push rules event in account data", () => {
|
||||
mockClient.getAccountData.mockReturnValue(undefined);
|
||||
getComponent();
|
||||
|
||||
expect(mockClient.getAccountData).toHaveBeenCalledWith(EventType.PushRules);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles when user doesnt have a push rule defined in vector definitions", () => {
|
||||
// synced push rules uses VectorPushRulesDefinitions
|
||||
// rules defined there may not exist in m.push_rules
|
||||
// mock push rules with group rule, but missing oneToOne rule
|
||||
setPushRules([pollStartOneToOne, groupRule, pollStartGroup]);
|
||||
|
||||
getComponent();
|
||||
|
||||
// just called once for one-to-one
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);
|
||||
// set to match primary rule (groupRule)
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollStartGroup.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates all mismatched rules from synced rules", () => {
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRule, // on
|
||||
pollStartOneToOne, // on
|
||||
pollEndOneToOne, // loud
|
||||
// poll group rules are synced with groupRule
|
||||
groupRule, // on
|
||||
pollStartGroup, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
|
||||
// only called for rules not in sync with their primary rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||||
// set to match primary rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollStartGroup.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollEndOneToOne.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates all mismatched rules from synced rules when primary rule is disabled", async () => {
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRuleDisabled, // off
|
||||
pollStartOneToOne, // on
|
||||
pollEndOneToOne, // loud
|
||||
// poll group rules are synced with groupRule
|
||||
groupRule, // on
|
||||
pollStartGroup, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// set to match primary rule
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
pollStartOneToOne.rule_id,
|
||||
false,
|
||||
);
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
pollEndOneToOne.rule_id,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("catches and logs errors while updating a rule", async () => {
|
||||
mockClient.setPushRuleActions.mockRejectedValueOnce("oups").mockResolvedValueOnce({});
|
||||
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRule, // on
|
||||
pollStartOneToOne, // on
|
||||
pollEndOneToOne, // loud
|
||||
// poll group rules are synced with groupRule
|
||||
groupRule, // on
|
||||
pollStartGroup, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);
|
||||
// both calls made
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollStartGroup.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
// second primary rule still updated after first rule failed
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollEndOneToOne.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Failed to fully synchronise push rules for .m.rule.room_one_to_one",
|
||||
"oups",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on changes to account_data", () => {
|
||||
it("ignores other account data events", () => {
|
||||
// setup a push rule state with mismatched rules
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRule, // on
|
||||
pollEndOneToOne, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
|
||||
mockClient.setPushRuleActions.mockClear();
|
||||
|
||||
const someOtherAccountData = new MatrixEvent({ type: "my-test-account-data " });
|
||||
mockClient.emit(ClientEvent.AccountData, someOtherAccountData);
|
||||
|
||||
// didnt check rule sync
|
||||
expect(mockClient.setPushRuleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates all mismatched rules from synced rules on a change to push rules account data", () => {
|
||||
// setup a push rule state with mismatched rules
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRule, // on
|
||||
pollEndOneToOne, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
|
||||
mockClient.setPushRuleActions.mockClear();
|
||||
|
||||
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||||
|
||||
// set to match primary rule
|
||||
expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollEndOneToOne.rule_id,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
|
||||
it("updates all mismatched rules from synced rules on a change to push rules account data when primary rule is disabled", async () => {
|
||||
// setup a push rule state with mismatched rules
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRuleDisabled, // off
|
||||
pollEndOneToOne, // loud
|
||||
]);
|
||||
|
||||
getComponent();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
mockClient.setPushRuleEnabled.mockClear();
|
||||
|
||||
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||||
|
||||
// set to match primary rule
|
||||
expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
"underride",
|
||||
pollEndOneToOne.rule_id,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("stops listening to account data events on unmount", () => {
|
||||
// setup a push rule state with mismatched rules
|
||||
setPushRules([
|
||||
// poll 1-1 rules are synced with oneToOneRule
|
||||
oneToOneRule, // on
|
||||
pollEndOneToOne, // loud
|
||||
]);
|
||||
|
||||
const { unmount } = getComponent();
|
||||
|
||||
mockClient.setPushRuleActions.mockClear();
|
||||
|
||||
unmount();
|
||||
|
||||
mockClient.emit(ClientEvent.AccountData, pushRulesEvent);
|
||||
|
||||
// not called
|
||||
expect(mockClient.setPushRuleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should fire FocusMessageSearch on Ctrl+F when enabled", async () => {
|
||||
jest.spyOn(defaultDispatcher, "fire");
|
||||
await SettingsStore.setValue("ctrlFForSearch", null, SettingLevel.DEVICE, true);
|
||||
|
||||
getComponent();
|
||||
await userEvent.keyboard("{Control>}f{/Control}");
|
||||
expect(defaultDispatcher.fire).toHaveBeenCalledWith(Action.FocusMessageSearch);
|
||||
});
|
||||
|
||||
it("should go home on home shortcut", async () => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch");
|
||||
|
||||
getComponent();
|
||||
await userEvent.keyboard("{Control>}{Alt>}h</Alt>{/Control}");
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
it("should ignore home shortcut if dialogs are open", async () => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch");
|
||||
jest.spyOn(Modal, "hasDialogs").mockReturnValue(true);
|
||||
|
||||
getComponent();
|
||||
|
||||
await userEvent.keyboard("{Control>}{Alt>}h</Alt>{/Control}");
|
||||
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
describe("timezone updates", () => {
|
||||
const userTimezone = "Europe/London";
|
||||
const originalController = SETTINGS["userTimezonePublish"].controller;
|
||||
|
||||
beforeEach(async () => {
|
||||
SETTINGS["userTimezonePublish"].controller = undefined;
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||||
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, userTimezone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SETTINGS["userTimezonePublish"].controller = originalController;
|
||||
});
|
||||
|
||||
it("does not update the timezone when userTimezonePublish is off", async () => {
|
||||
getComponent();
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||||
expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz");
|
||||
expect(mockClient.setExtendedProfileProperty).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should set the user timezone when userTimezonePublish is enabled", async () => {
|
||||
getComponent();
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||||
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||||
});
|
||||
|
||||
it("should set the user timezone when the timezone is changed", async () => {
|
||||
const newTimezone = "Europe/Paris";
|
||||
getComponent();
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||||
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||||
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, newTimezone);
|
||||
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", newTimezone);
|
||||
});
|
||||
|
||||
it("should clear the timezone when the publish feature is turned off", async () => {
|
||||
getComponent();
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true);
|
||||
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone);
|
||||
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
|
||||
expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz");
|
||||
});
|
||||
});
|
||||
});
|
99
test/unit-tests/components/structures/MainSplit-test.tsx
Normal file
99
test/unit-tests/components/structures/MainSplit-test.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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, fireEvent } from "jest-matrix-react";
|
||||
|
||||
import MainSplit from "../../../src/components/structures/MainSplit";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { PosthogAnalytics } from "../../../src/PosthogAnalytics.ts";
|
||||
|
||||
describe("<MainSplit/>", () => {
|
||||
const resizeNotifier = new ResizeNotifier();
|
||||
const children = (
|
||||
<div>
|
||||
Child<span>Foo</span>Bar
|
||||
</div>
|
||||
);
|
||||
const panel = <div>Right panel</div>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const { asFragment, container } = render(
|
||||
<MainSplit
|
||||
resizeNotifier={resizeNotifier}
|
||||
children={children}
|
||||
panel={panel}
|
||||
analyticsRoomType="other_room"
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// Assert it matches the default width of 320
|
||||
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("320px");
|
||||
});
|
||||
|
||||
it("respects defaultSize prop", () => {
|
||||
const { asFragment, container } = render(
|
||||
<MainSplit
|
||||
resizeNotifier={resizeNotifier}
|
||||
children={children}
|
||||
panel={panel}
|
||||
defaultSize={500}
|
||||
analyticsRoomType="other_room"
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// Assert it matches the default width of 350
|
||||
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("500px");
|
||||
});
|
||||
|
||||
it("prefers size stashed in LocalStorage to the defaultSize prop", () => {
|
||||
localStorage.setItem("mx_rhs_size_thread", "333");
|
||||
const { container } = render(
|
||||
<MainSplit
|
||||
resizeNotifier={resizeNotifier}
|
||||
children={children}
|
||||
panel={panel}
|
||||
sizeKey="thread"
|
||||
defaultSize={400}
|
||||
analyticsRoomType="other_room"
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("333px");
|
||||
});
|
||||
|
||||
it("should report to analytics on resize stop", () => {
|
||||
const { container } = render(
|
||||
<MainSplit
|
||||
resizeNotifier={resizeNotifier}
|
||||
children={children}
|
||||
panel={panel}
|
||||
sizeKey="thread"
|
||||
defaultSize={400}
|
||||
analyticsRoomType="other_room"
|
||||
/>,
|
||||
);
|
||||
|
||||
const spy = jest.spyOn(PosthogAnalytics.instance, "trackEvent");
|
||||
|
||||
const handle = container.querySelector(".mx_ResizeHandle--horizontal")!;
|
||||
fireEvent.mouseDown(handle);
|
||||
fireEvent.mouseMove(handle, { clientX: 0 });
|
||||
fireEvent.mouseUp(handle);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
eventName: "WebPanelResize",
|
||||
panel: "right",
|
||||
roomType: "other_room",
|
||||
size: 400,
|
||||
});
|
||||
});
|
||||
});
|
1520
test/unit-tests/components/structures/MatrixChat-test.tsx
Normal file
1520
test/unit-tests/components/structures/MatrixChat-test.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { act, render } from "jest-matrix-react";
|
||||
import React, { useContext } from "react";
|
||||
import { CryptoEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
|
||||
import { LocalDeviceVerificationStateContext } from "../../../src/contexts/LocalDeviceVerificationStateContext";
|
||||
import {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsUser,
|
||||
} from "../../test-utils";
|
||||
|
||||
describe("MatrixClientContextProvider", () => {
|
||||
it("Should expose a matrix client context", () => {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
getCrypto: () => null,
|
||||
});
|
||||
|
||||
let receivedClient: MatrixClient | undefined;
|
||||
function ContextReceiver() {
|
||||
receivedClient = useContext(MatrixClientContext);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
render(
|
||||
<MatrixClientContextProvider client={mockClient}>
|
||||
<ContextReceiver />
|
||||
</MatrixClientContextProvider>,
|
||||
);
|
||||
|
||||
expect(receivedClient).toBe(mockClient);
|
||||
});
|
||||
|
||||
describe("Should expose a verification status context", () => {
|
||||
/** The most recent verification status received by our `ContextReceiver` */
|
||||
let receivedState: boolean | undefined;
|
||||
|
||||
/** The mock client for use in the tests */
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
function ContextReceiver() {
|
||||
receivedState = useContext(LocalDeviceVerificationStateContext);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function getComponent(mockClient: MatrixClient) {
|
||||
return render(
|
||||
<MatrixClientContextProvider client={mockClient}>
|
||||
<ContextReceiver />
|
||||
</MatrixClientContextProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
receivedState = undefined;
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
...mockClientMethodsCrypto(),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false if device is unverified", async () => {
|
||||
mockClient.getCrypto()!.getUserVerificationStatus = jest
|
||||
.fn()
|
||||
.mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
getComponent(mockClient);
|
||||
expect(receivedState).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true if device is verified", async () => {
|
||||
mockClient.getCrypto()!.getUserVerificationStatus = jest
|
||||
.fn()
|
||||
.mockResolvedValue(new UserVerificationStatus(true, false, false));
|
||||
getComponent(mockClient);
|
||||
await act(() => flushPromises());
|
||||
expect(receivedState).toBe(true);
|
||||
});
|
||||
|
||||
it("updates when the trust status updates", async () => {
|
||||
const getVerificationStatus = jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
mockClient.getCrypto()!.getUserVerificationStatus = getVerificationStatus;
|
||||
getComponent(mockClient);
|
||||
|
||||
// starts out false
|
||||
await act(() => flushPromises());
|
||||
expect(receivedState).toBe(false);
|
||||
|
||||
// Now the state is updated
|
||||
const verifiedStatus = new UserVerificationStatus(true, false, false);
|
||||
getVerificationStatus.mockResolvedValue(verifiedStatus);
|
||||
act(() => {
|
||||
mockClient.emit(CryptoEvent.UserTrustStatusChanged, mockClient.getSafeUserId(), verifiedStatus);
|
||||
});
|
||||
expect(receivedState).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
835
test/unit-tests/components/structures/MessagePanel-test.tsx
Normal file
835
test/unit-tests/components/structures/MessagePanel-test.tsx
Normal file
|
@ -0,0 +1,835 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019-2021 , 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 { EventEmitter } from "events";
|
||||
import { MatrixEvent, Room, RoomMember, Thread, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import MessagePanel, { shouldFormContinuation } from "../../../src/components/structures/MessagePanel";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import * as TestUtilsMatrix from "../../test-utils";
|
||||
import {
|
||||
createTestClient,
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconInfoEvent,
|
||||
mockClientMethodsEvents,
|
||||
mockClientMethodsUser,
|
||||
} from "../../test-utils";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { IRoomState } from "../../../src/components/structures/RoomView";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("../../../src/utils/beacon", () => ({
|
||||
useBeacon: jest.fn(),
|
||||
}));
|
||||
|
||||
const roomId = "!roomId:server_name";
|
||||
|
||||
describe("MessagePanel", function () {
|
||||
const events = mkEvents();
|
||||
const userId = "@me:here";
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsEvents(),
|
||||
getAccountData: jest.fn(),
|
||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn(),
|
||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
const bobMember = new RoomMember(roomId, "@bob:id");
|
||||
bobMember.name = "Bob";
|
||||
jest.spyOn(bobMember, "getAvatarUrl").mockReturnValue("avatar.jpeg");
|
||||
jest.spyOn(bobMember, "getMxcAvatarUrl").mockReturnValue("mxc://avatar.url/image.png");
|
||||
|
||||
const alice = "@alice:example.org";
|
||||
const aliceMember = new RoomMember(roomId, alice);
|
||||
aliceMember.name = "Alice";
|
||||
jest.spyOn(aliceMember, "getAvatarUrl").mockReturnValue("avatar.jpeg");
|
||||
jest.spyOn(aliceMember, "getMxcAvatarUrl").mockReturnValue("mxc://avatar.url/image.png");
|
||||
|
||||
const defaultProps = {
|
||||
resizeNotifier: new EventEmitter() as unknown as ResizeNotifier,
|
||||
callEventGroupers: new Map(),
|
||||
room,
|
||||
className: "cls",
|
||||
events: [] as MatrixEvent[],
|
||||
};
|
||||
|
||||
const defaultRoomContext = {
|
||||
...RoomContext,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
room,
|
||||
roomId: room.roomId,
|
||||
canReact: true,
|
||||
canSendMessages: true,
|
||||
showReadReceipts: true,
|
||||
showRedactions: false,
|
||||
showJoinLeaves: false,
|
||||
showAvatarChanges: false,
|
||||
showDisplaynameChanges: true,
|
||||
showHiddenEvents: false,
|
||||
} as unknown as IRoomState;
|
||||
|
||||
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}>
|
||||
<MessagePanel {...defaultProps} {...props} />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
// HACK: We assume all settings want to be disabled
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((arg) => {
|
||||
return arg === "showDisplaynameChanges";
|
||||
});
|
||||
|
||||
DMRoomMap.makeShared(client);
|
||||
});
|
||||
|
||||
function mkEvents() {
|
||||
const events: MatrixEvent[] = [];
|
||||
const ts0 = Date.now();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
// Just to avoid breaking Dateseparator tests that might run at 00hrs
|
||||
function mkOneDayEvents() {
|
||||
const events: MatrixEvent[] = [];
|
||||
const ts0 = Date.parse("09 May 2004 00:12:00 GMT");
|
||||
for (let i = 0; i < 10; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
// make a collection of events with some member events that should be collapsed with an EventListSummary
|
||||
function mkMelsEvents() {
|
||||
const events: MatrixEvent[] = [];
|
||||
const ts0 = Date.now();
|
||||
|
||||
let i = 0;
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: ts0 + ++i * 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMembership({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
target: bobMember,
|
||||
ts: ts0 + i * 1000,
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
name: "A user",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: ts0 + ++i * 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
// A list of membership events only with nothing else
|
||||
function mkMelsEventsOnly() {
|
||||
const events: MatrixEvent[] = [];
|
||||
const ts0 = Date.now();
|
||||
|
||||
let i = 0;
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMembership({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
target: bobMember,
|
||||
ts: ts0 + i * 1000,
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
name: "A user",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
// A list of room creation, encryption, and invite events.
|
||||
function mkCreationEvents() {
|
||||
const mkEvent = TestUtilsMatrix.mkEvent;
|
||||
const mkMembership = TestUtilsMatrix.mkMembership;
|
||||
const roomId = "!someroom";
|
||||
|
||||
const ts0 = Date.now();
|
||||
|
||||
return [
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: alice,
|
||||
content: {
|
||||
creator: alice,
|
||||
room_version: "5",
|
||||
predecessor: {
|
||||
room_id: "!prevroom",
|
||||
event_id: "$someevent",
|
||||
},
|
||||
},
|
||||
ts: ts0,
|
||||
}),
|
||||
mkMembership({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: alice,
|
||||
target: aliceMember,
|
||||
ts: ts0 + 1,
|
||||
mship: KnownMembership.Join,
|
||||
name: "Alice",
|
||||
}),
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: "m.room.join_rules",
|
||||
room: roomId,
|
||||
user: alice,
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
},
|
||||
ts: ts0 + 2,
|
||||
}),
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: "m.room.history_visibility",
|
||||
room: roomId,
|
||||
user: alice,
|
||||
content: {
|
||||
history_visibility: "invited",
|
||||
},
|
||||
ts: ts0 + 3,
|
||||
}),
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: "m.room.encryption",
|
||||
room: roomId,
|
||||
user: alice,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
ts: ts0 + 4,
|
||||
}),
|
||||
mkMembership({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: alice,
|
||||
skey: "@bob:example.org",
|
||||
target: bobMember,
|
||||
ts: ts0 + 5,
|
||||
mship: KnownMembership.Invite,
|
||||
name: "Bob",
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function mkMixedHiddenAndShownEvents() {
|
||||
const roomId = "!room:id";
|
||||
const userId = "@alice:example.org";
|
||||
const ts0 = Date.now();
|
||||
|
||||
return [
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: userId,
|
||||
ts: ts0,
|
||||
}),
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
type: "org.example.a_hidden_event",
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: {},
|
||||
ts: ts0 + 1,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function isReadMarkerVisible(rmContainer?: Element) {
|
||||
return !!rmContainer?.children.length;
|
||||
}
|
||||
|
||||
it("should show the events", function () {
|
||||
const { container } = render(getComponent({ events }));
|
||||
|
||||
// just check we have the right number of tiles for now
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(tiles.length).toEqual(10);
|
||||
});
|
||||
|
||||
it("should collapse adjacent member events", function () {
|
||||
const { container } = render(getComponent({ events: mkMelsEvents() }));
|
||||
|
||||
// just check we have the right number of tiles for now
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(tiles.length).toEqual(2);
|
||||
|
||||
const summaryTiles = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(summaryTiles.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should insert the read-marker in the right place", function () {
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
events,
|
||||
readMarkerEventId: events[4].getId(),
|
||||
readMarkerVisible: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
|
||||
// find the <li> which wraps the read marker
|
||||
const [rm] = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
|
||||
// it should follow the <li> which wraps the event tile for event 4
|
||||
const eventContainer = tiles[4];
|
||||
expect(rm.previousSibling).toEqual(eventContainer);
|
||||
});
|
||||
|
||||
it("should show the read-marker that fall in summarised events after the summary", function () {
|
||||
const melsEvents = mkMelsEvents();
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
events: melsEvents,
|
||||
readMarkerEventId: melsEvents[4].getId(),
|
||||
readMarkerVisible: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const [summary] = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
|
||||
// find the <li> which wraps the read marker
|
||||
const [rm] = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
|
||||
expect(rm.previousSibling).toEqual(summary);
|
||||
|
||||
// read marker should be visible given props and not at the last event
|
||||
expect(isReadMarkerVisible(rm)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide the read-marker at the end of summarised events", function () {
|
||||
const melsEvents = mkMelsEventsOnly();
|
||||
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
events: melsEvents,
|
||||
readMarkerEventId: melsEvents[9].getId(),
|
||||
readMarkerVisible: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const [summary] = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
|
||||
// find the <li> which wraps the read marker
|
||||
const [rm] = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
|
||||
expect(rm.previousSibling).toEqual(summary);
|
||||
|
||||
// read marker should be hidden given props and at the last event
|
||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows a ghost read-marker when the read-marker moves", function () {
|
||||
// fake the clock so that we can test the velocity animation.
|
||||
jest.useFakeTimers();
|
||||
|
||||
const { container, rerender } = render(
|
||||
<div>
|
||||
{getComponent({
|
||||
events,
|
||||
readMarkerEventId: events[4].getId(),
|
||||
readMarkerVisible: true,
|
||||
})}
|
||||
</div>,
|
||||
);
|
||||
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
|
||||
// find the <li> which wraps the read marker
|
||||
const [rm] = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
expect(rm.previousSibling).toEqual(tiles[4]);
|
||||
|
||||
rerender(
|
||||
<div>
|
||||
{getComponent({
|
||||
events,
|
||||
readMarkerEventId: events[6].getId(),
|
||||
readMarkerVisible: true,
|
||||
})}
|
||||
</div>,
|
||||
);
|
||||
|
||||
// now there should be two RM containers
|
||||
const readMarkers = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
|
||||
expect(readMarkers.length).toEqual(2);
|
||||
|
||||
// the first should be the ghost
|
||||
expect(readMarkers[0].previousSibling).toEqual(tiles[4]);
|
||||
const hr: HTMLElement = readMarkers[0].children[0] as HTMLElement;
|
||||
|
||||
// the second should be the real thing
|
||||
expect(readMarkers[1].previousSibling).toEqual(tiles[6]);
|
||||
|
||||
// advance the clock, and then let the browser run an animation frame to let the animation start
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(hr.style.opacity).toEqual("0");
|
||||
});
|
||||
|
||||
it("should collapse creation events", function () {
|
||||
const events = mkCreationEvents();
|
||||
const createEvent = events.find((event) => event.getType() === "m.room.create")!;
|
||||
const encryptionEvent = events.find((event) => event.getType() === "m.room.encryption")!;
|
||||
client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null));
|
||||
TestUtilsMatrix.upsertRoomStateEvents(room, events);
|
||||
|
||||
const { container } = render(getComponent({ events }));
|
||||
|
||||
// we expect that
|
||||
// - the room creation event, the room encryption event, and Alice inviting Bob,
|
||||
// should be outside of the room creation summary
|
||||
// - all other events should be inside the room creation summary
|
||||
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
|
||||
expect(tiles[0].getAttribute("data-event-id")).toEqual(createEvent.getId());
|
||||
expect(tiles[1].getAttribute("data-event-id")).toEqual(encryptionEvent.getId());
|
||||
|
||||
const [summaryTile] = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
|
||||
const summaryEventTiles = summaryTile.getElementsByClassName("mx_EventTile");
|
||||
// every event except for the room creation, room encryption, and Bob's
|
||||
// invite event should be in the event summary
|
||||
expect(summaryEventTiles.length).toEqual(tiles.length - 3);
|
||||
});
|
||||
|
||||
it("should not collapse beacons as part of creation events", function () {
|
||||
const events = mkCreationEvents();
|
||||
const creationEvent = events.find((event) => event.getType() === "m.room.create")!;
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(creationEvent.getSender()!, creationEvent.getRoomId()!, {
|
||||
isLive: true,
|
||||
});
|
||||
const combinedEvents = [...events, beaconInfoEvent];
|
||||
TestUtilsMatrix.upsertRoomStateEvents(room, combinedEvents);
|
||||
const { container } = render(getComponent({ events: combinedEvents }));
|
||||
|
||||
const [summaryTile] = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
|
||||
// beacon body is not in the summary
|
||||
expect(summaryTile.getElementsByClassName("mx_MBeaconBody").length).toBe(0);
|
||||
// beacon tile is rendered
|
||||
expect(container.getElementsByClassName("mx_MBeaconBody").length).toBe(1);
|
||||
});
|
||||
|
||||
it("should hide read-marker at the end of creation event summary", function () {
|
||||
const events = mkCreationEvents();
|
||||
const createEvent = events.find((event) => event.getType() === "m.room.create");
|
||||
client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null));
|
||||
TestUtilsMatrix.upsertRoomStateEvents(room, events);
|
||||
|
||||
const { container } = render(
|
||||
getComponent({
|
||||
events,
|
||||
readMarkerEventId: events[5].getId(),
|
||||
readMarkerVisible: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// find the <li> which wraps the read marker
|
||||
const [rm] = container.getElementsByClassName("mx_MessagePanel_myReadMarker");
|
||||
|
||||
const [messageList] = container.getElementsByClassName("mx_RoomView_MessageList");
|
||||
const rows = messageList.children;
|
||||
expect(rows.length).toEqual(7); // 6 events + the NewRoomIntro
|
||||
expect(rm.previousSibling).toEqual(rows[5]);
|
||||
|
||||
// read marker should be hidden given props and at the last event
|
||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should render Date separators for the events", function () {
|
||||
const events = mkOneDayEvents();
|
||||
const { queryAllByRole } = render(getComponent({ events }));
|
||||
const dates = queryAllByRole("separator");
|
||||
|
||||
expect(dates.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("appends events into summaries during forward pagination without changing key", () => {
|
||||
const events = mkMelsEvents().slice(1, 11);
|
||||
|
||||
const { container, rerender } = render(getComponent({ events }));
|
||||
let els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId());
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(10);
|
||||
|
||||
const updatedEvents = [
|
||||
...events,
|
||||
TestUtilsMatrix.mkMembership({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
target: bobMember,
|
||||
ts: Date.now(),
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
name: "A user",
|
||||
}),
|
||||
];
|
||||
rerender(getComponent({ events: updatedEvents }));
|
||||
|
||||
els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId());
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("prepends events into summaries during backward pagination without changing key", () => {
|
||||
const events = mkMelsEvents().slice(1, 11);
|
||||
|
||||
const { container, rerender } = render(getComponent({ events }));
|
||||
let els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId());
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(10);
|
||||
|
||||
const updatedEvents = [
|
||||
TestUtilsMatrix.mkMembership({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
target: bobMember,
|
||||
ts: Date.now(),
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
name: "A user",
|
||||
}),
|
||||
...events,
|
||||
];
|
||||
rerender(getComponent({ events: updatedEvents }));
|
||||
|
||||
els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId());
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("assigns different keys to summaries that get split up", () => {
|
||||
const events = mkMelsEvents().slice(1, 11);
|
||||
|
||||
const { container, rerender } = render(getComponent({ events }));
|
||||
let els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual(`eventlistsummary-${events[0].getId()}`);
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(10);
|
||||
|
||||
const updatedEvents = [
|
||||
...events.slice(0, 5),
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
msg: "Hello!",
|
||||
}),
|
||||
...events.slice(5, 10),
|
||||
];
|
||||
rerender(getComponent({ events: updatedEvents }));
|
||||
|
||||
// summaries split becuase room messages are not summarised
|
||||
els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(2);
|
||||
expect(els[0].getAttribute("data-testid")).toEqual(`eventlistsummary-${events[0].getId()}`);
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(5);
|
||||
|
||||
expect(els[1].getAttribute("data-testid")).toEqual(`eventlistsummary-${events[5].getId()}`);
|
||||
expect(els[1].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(5);
|
||||
});
|
||||
|
||||
// We test this because setting lookups can be *slow*, and we don't want
|
||||
// them to happen in this code path
|
||||
it("doesn't lookup showHiddenEventsInTimeline while rendering", () => {
|
||||
// We're only interested in the setting lookups that happen on every render,
|
||||
// rather than those happening on first mount, so let's get those out of the way
|
||||
const { rerender } = render(getComponent({ events: [] }));
|
||||
|
||||
// Set up our spy and re-render with new events
|
||||
const settingsSpy = jest.spyOn(SettingsStore, "getValue").mockClear();
|
||||
|
||||
rerender(getComponent({ events: mkMixedHiddenAndShownEvents() }));
|
||||
|
||||
expect(settingsSpy).not.toHaveBeenCalledWith("showHiddenEventsInTimeline");
|
||||
settingsSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should group hidden event reactions into an event list summary", () => {
|
||||
const events = [
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
type: "m.reaction",
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
content: {},
|
||||
ts: 1,
|
||||
}),
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
type: "m.reaction",
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
content: {},
|
||||
ts: 2,
|
||||
}),
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
type: "m.reaction",
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
content: {},
|
||||
ts: 3,
|
||||
}),
|
||||
];
|
||||
const { container } = render(getComponent({ events }, { showHiddenEvents: true }));
|
||||
|
||||
const els = container.getElementsByClassName("mx_GenericEventListSummary");
|
||||
expect(els.length).toEqual(1);
|
||||
expect(els[0].getAttribute("data-scroll-tokens")?.split(",")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should handle large numbers of hidden events quickly", () => {
|
||||
// Increase the length of the loop here to test performance issues with
|
||||
// rendering
|
||||
|
||||
const events: MatrixEvent[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
type: "unknown.event.type",
|
||||
content: { key: "value" },
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: 1000000 + i,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { asFragment } = render(getComponent({ events }, { showHiddenEvents: false }));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle lots of room creation events quickly", () => {
|
||||
// Increase the length of the loop here to test performance issues with
|
||||
// rendering
|
||||
|
||||
const events = [TestUtilsMatrix.mkRoomCreateEvent("@user:id", "!room:id")];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMembership({
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
event: true,
|
||||
skey: "123",
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { asFragment } = render(getComponent({ events }, { showHiddenEvents: false }));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle lots of membership events quickly", () => {
|
||||
// Increase the length of the loop here to test performance issues with
|
||||
// rendering
|
||||
|
||||
const events: MatrixEvent[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
events.push(
|
||||
TestUtilsMatrix.mkMembership({
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
event: true,
|
||||
skey: "123",
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { asFragment } = render(getComponent({ events }, { showHiddenEvents: true }));
|
||||
const cpt = asFragment();
|
||||
|
||||
// Ignore properties that change every time
|
||||
cpt.querySelectorAll("li").forEach((li) => {
|
||||
li.setAttribute("data-scroll-tokens", "__scroll_tokens__");
|
||||
li.setAttribute("data-testid", "__testid__");
|
||||
});
|
||||
|
||||
expect(cpt).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should set lastSuccessful=true on non-last event if last event is not eligible for special receipt", () => {
|
||||
client.getRoom.mockImplementation((id) => (id === room.roomId ? room : null));
|
||||
const events = [
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user: client.getSafeUserId(),
|
||||
ts: 1000,
|
||||
}),
|
||||
TestUtilsMatrix.mkEvent({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user: client.getSafeUserId(),
|
||||
ts: 1000,
|
||||
type: "m.room.topic",
|
||||
skey: "",
|
||||
content: { topic: "TOPIC" },
|
||||
}),
|
||||
];
|
||||
const { container } = render(getComponent({ events, showReadReceipts: true }));
|
||||
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(tiles.length).toEqual(2);
|
||||
expect(tiles[0].querySelector(".mx_EventTile_receiptSent")).toBeTruthy();
|
||||
expect(tiles[1].querySelector(".mx_EventTile_receiptSent")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should set lastSuccessful=false on non-last event if last event has a receipt from someone else", () => {
|
||||
client.getRoom.mockImplementation((id) => (id === room.roomId ? room : null));
|
||||
const events = [
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user: client.getSafeUserId(),
|
||||
ts: 1000,
|
||||
}),
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user: "@other:user",
|
||||
ts: 1001,
|
||||
}),
|
||||
];
|
||||
room.addReceiptToStructure(
|
||||
events[1].getId()!,
|
||||
ReceiptType.Read,
|
||||
"@other:user",
|
||||
{
|
||||
ts: 1001,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { container } = render(getComponent({ events, showReadReceipts: true }));
|
||||
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(tiles.length).toEqual(2);
|
||||
expect(tiles[0].querySelector(".mx_EventTile_receiptSent")).toBeFalsy();
|
||||
expect(tiles[1].querySelector(".mx_EventTile_receiptSent")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldFormContinuation", () => {
|
||||
it("does not form continuations from thread roots which have summaries", () => {
|
||||
const message1 = TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
msg: "Here is a message in the main timeline",
|
||||
});
|
||||
|
||||
const message2 = TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
msg: "And here's another message in the main timeline",
|
||||
});
|
||||
|
||||
const threadRoot = TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
msg: "Here is a thread",
|
||||
});
|
||||
jest.spyOn(threadRoot, "isThreadRoot", "get").mockReturnValue(true);
|
||||
|
||||
const message3 = TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
msg: "And here's another message in the main timeline after the thread root",
|
||||
});
|
||||
|
||||
const client = createTestClient();
|
||||
expect(shouldFormContinuation(message1, message2, client, false)).toEqual(true);
|
||||
expect(shouldFormContinuation(message2, threadRoot, client, false)).toEqual(true);
|
||||
expect(shouldFormContinuation(threadRoot, message3, client, false)).toEqual(true);
|
||||
|
||||
const thread = {
|
||||
length: 1,
|
||||
replyToEvent: {},
|
||||
} as unknown as Thread;
|
||||
jest.spyOn(threadRoot, "getThread").mockReturnValue(thread);
|
||||
expect(shouldFormContinuation(message2, threadRoot, client, false)).toEqual(false);
|
||||
expect(shouldFormContinuation(threadRoot, message3, client, false)).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
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, { MouseEventHandler } from "react";
|
||||
import { screen, render, RenderResult } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import PictureInPictureDragger, { CreatePipChildren } from "../../../src/components/structures/PictureInPictureDragger";
|
||||
|
||||
describe("PictureInPictureDragger", () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const mkContent1: Array<CreatePipChildren> = [
|
||||
() => {
|
||||
return <div>content 1</div>;
|
||||
},
|
||||
];
|
||||
|
||||
const mkContent2: Array<CreatePipChildren> = [
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
content 2<br />
|
||||
content 2.2
|
||||
</div>
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
describe("when rendering the dragger with PiP content 1", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(<PictureInPictureDragger draggable={true}>{mkContent1}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should render the PiP content", () => {
|
||||
expect(renderResult.container).toMatchSnapshot("pip-content-1");
|
||||
});
|
||||
|
||||
describe("and rerendering PiP content 1", () => {
|
||||
beforeEach(() => {
|
||||
renderResult.rerender(<PictureInPictureDragger draggable={true}>{mkContent1}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should not change the PiP content", () => {
|
||||
expect(renderResult.container).toMatchSnapshot("pip-content-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("and rendering PiP content 2", () => {
|
||||
beforeEach(() => {
|
||||
renderResult.rerender(<PictureInPictureDragger draggable={true}>{mkContent2}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should update the PiP content", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering the dragger with PiP content 1 and 2", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<PictureInPictureDragger draggable={true}>{[...mkContent1, ...mkContent2]}</PictureInPictureDragger>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render both contents", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering the dragger", () => {
|
||||
let clickSpy: jest.Mocked<MouseEventHandler>;
|
||||
let target: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
clickSpy = jest.fn();
|
||||
render(
|
||||
<PictureInPictureDragger draggable={true}>
|
||||
{[
|
||||
({ onStartMoving }) => (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div onMouseDown={onStartMoving} onClick={clickSpy}>
|
||||
Hello
|
||||
</div>
|
||||
),
|
||||
]}
|
||||
</PictureInPictureDragger>,
|
||||
);
|
||||
target = screen.getByText("Hello");
|
||||
});
|
||||
|
||||
it("and clicking without a drag motion, it should pass the click to children", async () => {
|
||||
await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { keys: "[/MouseLeft]" }]);
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and clicking with a drag motion above the threshold of 5px, it should not pass the click to children", async () => {
|
||||
await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { coords: { x: 60, y: 2 } }, "[/MouseLeft]"]);
|
||||
expect(clickSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and clickign with a drag motion below the threshold of 5px, it should pass the click to the children", async () => {
|
||||
await userEvent.pointer([{ keys: "[MouseLeft>]", target }, { coords: { x: 4, y: 4 } }, "[/MouseLeft]"]);
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
476
test/unit-tests/components/structures/PipContainer-test.tsx
Normal file
476
test/unit-tests/components/structures/PipContainer-test.tsx
Normal file
|
@ -0,0 +1,476 @@
|
|||
/*
|
||||
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, Mocked } from "jest-mock";
|
||||
import { screen, render, act, cleanup } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { Widget, ClientWidgetApi } from "matrix-widget-api";
|
||||
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
useMockedCalls,
|
||||
MockedCall,
|
||||
mkRoomMember,
|
||||
stubClient,
|
||||
setupAsyncStoreWithClient,
|
||||
resetAsyncStoreWithClient,
|
||||
wrapInMatrixClientContext,
|
||||
wrapInSdkContext,
|
||||
mkRoomCreateEvent,
|
||||
mockPlatformPeg,
|
||||
flushPromises,
|
||||
useMockMediaDevices,
|
||||
} 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,
|
||||
VoiceBroadcastPreRecording,
|
||||
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";
|
||||
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";
|
||||
|
||||
jest.mock("../../../src/stores/OwnProfileStore", () => ({
|
||||
OwnProfileStore: {
|
||||
instance: {
|
||||
isProfileInfoFetched: true,
|
||||
removeListener: jest.fn(),
|
||||
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("PipContainer", () => {
|
||||
useMockedCalls();
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
let user: UserEvent;
|
||||
let sdkContext: TestSdkContext;
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let room2: Room;
|
||||
let alice: RoomMember;
|
||||
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
|
||||
let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore;
|
||||
let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore;
|
||||
|
||||
const actFlushPromises = async () => {
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
useMockMediaDevices();
|
||||
|
||||
user = userEvent.setup();
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
client.getUserId.mockReturnValue("@alice:example.org");
|
||||
client.getSafeUserId.mockReturnValue("@alice:example.org");
|
||||
DMRoomMap.makeShared(client);
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
|
||||
room2 = new Room("!2:example.com", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
client.getRoom.mockImplementation((roomId: string) => {
|
||||
if (roomId === room.roomId) return room;
|
||||
if (roomId === room2.roomId) return room2;
|
||||
return null;
|
||||
});
|
||||
client.getRooms.mockReturnValue([room, room2]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
room.currentState.setStateEvents([mkRoomCreateEvent(alice.userId, room.roomId)]);
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => (userId === alice.userId ? alice : null));
|
||||
|
||||
room2.currentState.setStateEvents([mkRoomCreateEvent(alice.userId, room2.roomId)]);
|
||||
|
||||
await Promise.all(
|
||||
[CallStore.instance, WidgetMessagingStore.instance].map((store) =>
|
||||
setupAsyncStoreWithClient(store, client),
|
||||
),
|
||||
);
|
||||
|
||||
sdkContext = new TestSdkContext();
|
||||
// @ts-ignore PipContainer uses SDKContext in the constructor
|
||||
SdkContextClass.instance = sdkContext;
|
||||
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
|
||||
voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore);
|
||||
sdkContext.client = client;
|
||||
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
|
||||
sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore;
|
||||
sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(resetAsyncStoreWithClient));
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderPip = () => {
|
||||
const PipContainer = wrapInMatrixClientContext(wrapInSdkContext(UnwrappedPipContainer, sdkContext));
|
||||
render(<PipContainer />);
|
||||
};
|
||||
|
||||
const viewRoom = (roomId: string) => {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
const widget = new Widget(call.widget);
|
||||
WidgetStore.instance.addVirtualWidget(call.widget, room.roomId);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
await act(async () => {
|
||||
await call.start();
|
||||
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
|
||||
});
|
||||
|
||||
await fn(call);
|
||||
|
||||
cleanup();
|
||||
call.destroy();
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
|
||||
WidgetStore.instance.removeVirtualWidget(widget.id, room.roomId);
|
||||
};
|
||||
|
||||
const withWidget = async (fn: () => Promise<void>): Promise<void> => {
|
||||
act(() => ActiveWidgetStore.instance.setWidgetPersistence("1", room.roomId, true));
|
||||
await fn();
|
||||
cleanup();
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
|
||||
};
|
||||
|
||||
const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => {
|
||||
return mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
alice.userId,
|
||||
client.getDeviceId() || "",
|
||||
);
|
||||
};
|
||||
|
||||
const setUpVoiceBroadcastRecording = () => {
|
||||
const infoEvent = makeVoiceBroadcastInfoStateEvent();
|
||||
const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
|
||||
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
|
||||
};
|
||||
|
||||
const setUpVoiceBroadcastPreRecording = () => {
|
||||
const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording(
|
||||
room,
|
||||
alice,
|
||||
client,
|
||||
voiceBroadcastPlaybacksStore,
|
||||
voiceBroadcastRecordingsStore,
|
||||
);
|
||||
voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording);
|
||||
};
|
||||
|
||||
const setUpRoomViewStore = () => {
|
||||
sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext);
|
||||
};
|
||||
|
||||
const mkVoiceBroadcast = (room: Room): MatrixEvent => {
|
||||
const infoEvent = makeVoiceBroadcastInfoStateEvent();
|
||||
room.currentState.setStateEvents([infoEvent]);
|
||||
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
|
||||
{
|
||||
action: "MatrixActions.RoomState.events",
|
||||
event: infoEvent,
|
||||
state: room.currentState,
|
||||
lastStateEvent: null,
|
||||
},
|
||||
true,
|
||||
);
|
||||
return infoEvent;
|
||||
};
|
||||
|
||||
it("hides if there's no content", () => {
|
||||
renderPip();
|
||||
expect(screen.queryByRole("complementary")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows an active call with back and leave buttons", async () => {
|
||||
renderPip();
|
||||
|
||||
await withCall(async (call) => {
|
||||
screen.getByRole("complementary");
|
||||
|
||||
// The return button should jump to the call
|
||||
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,
|
||||
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 back button when viewing the room", async () => {
|
||||
setUpRoomViewStore();
|
||||
viewRoom(room.roomId);
|
||||
const widget = WidgetStore.instance.addVirtualWidget(
|
||||
{
|
||||
id: "1",
|
||||
creatorUserId: "@alice:example.org",
|
||||
type: WidgetType.CUSTOM.preferred,
|
||||
url: "https://example.org",
|
||||
name: "Example widget",
|
||||
},
|
||||
room.roomId,
|
||||
);
|
||||
renderPip();
|
||||
|
||||
await withWidget(async () => {
|
||||
screen.getByRole("complementary");
|
||||
|
||||
// The return button should maximize the widget
|
||||
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
await user.click(await screen.findByRole("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 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:example.org",
|
||||
type: WidgetType.JITSI.preferred,
|
||||
url: "https://meet.example.org",
|
||||
name: "Jitsi example",
|
||||
},
|
||||
room.roomId,
|
||||
);
|
||||
renderPip();
|
||||
|
||||
await withWidget(async () => {
|
||||
screen.getByRole("complementary");
|
||||
|
||||
// The return button should view the room
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
await user.click(await screen.findByRole("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", () => {
|
||||
beforeEach(async () => {
|
||||
setUpVoiceBroadcastPreRecording();
|
||||
setUpVoiceBroadcastRecording();
|
||||
renderPip();
|
||||
await actFlushPromises();
|
||||
});
|
||||
|
||||
it("should render the voice broadcast recording PiP", () => {
|
||||
// check for the „Live“ badge to be present
|
||||
expect(screen.queryByText("Live")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("and a call it should show both, the call and the recording", async () => {
|
||||
await withCall(async () => {
|
||||
// Broadcast: Check for the „Live“ badge to be present
|
||||
expect(screen.queryByText("Live")).toBeInTheDocument();
|
||||
// Call: Check for the „Leave“ button to be present
|
||||
screen.getByRole("button", { name: "Leave" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a voice broadcast playback and pre-recording", () => {
|
||||
beforeEach(async () => {
|
||||
mkVoiceBroadcast(room);
|
||||
setUpVoiceBroadcastPreRecording();
|
||||
renderPip();
|
||||
await actFlushPromises();
|
||||
});
|
||||
|
||||
it("should render the voice broadcast pre-recording PiP", () => {
|
||||
// check for the „Go live“ button
|
||||
expect(screen.queryByText("Go live")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a voice broadcast pre-recording", () => {
|
||||
beforeEach(async () => {
|
||||
setUpVoiceBroadcastPreRecording();
|
||||
renderPip();
|
||||
await actFlushPromises();
|
||||
});
|
||||
|
||||
it("should render the voice broadcast pre-recording PiP", () => {
|
||||
// check for the „Go live“ button
|
||||
expect(screen.queryByText("Go live")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when listening to a voice broadcast in a room and then switching to another room", () => {
|
||||
beforeEach(async () => {
|
||||
setUpRoomViewStore();
|
||||
viewRoom(room.roomId);
|
||||
mkVoiceBroadcast(room);
|
||||
await actFlushPromises();
|
||||
|
||||
expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy();
|
||||
|
||||
await voiceBroadcastPlaybacksStore.getCurrent()?.start();
|
||||
viewRoom(room2.roomId);
|
||||
renderPip();
|
||||
});
|
||||
|
||||
it("should render the small voice broadcast playback PiP", () => {
|
||||
// check for the „pause voice broadcast“ button
|
||||
expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument();
|
||||
// check for the absence of the „30s forward“ button
|
||||
expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when viewing a room with a live voice broadcast", () => {
|
||||
let startEvent!: MatrixEvent;
|
||||
|
||||
beforeEach(async () => {
|
||||
setUpRoomViewStore();
|
||||
viewRoom(room.roomId);
|
||||
startEvent = mkVoiceBroadcast(room);
|
||||
renderPip();
|
||||
await actFlushPromises();
|
||||
});
|
||||
|
||||
it("should render the voice broadcast playback pip", () => {
|
||||
// check for the „resume voice broadcast“ button
|
||||
expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and the broadcast stops", () => {
|
||||
beforeEach(async () => {
|
||||
const stopEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
alice.userId,
|
||||
client.getDeviceId() || "",
|
||||
startEvent,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
room.currentState.setStateEvents([stopEvent]);
|
||||
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
|
||||
{
|
||||
action: "MatrixActions.RoomState.events",
|
||||
event: stopEvent,
|
||||
state: room.currentState,
|
||||
lastStateEvent: stopEvent,
|
||||
},
|
||||
true,
|
||||
);
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render the voice broadcast playback pip", () => {
|
||||
// check for the „resume voice broadcast“ button
|
||||
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and leaving the room", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
viewRoom(room2.roomId);
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render the voice broadcast playback pip", () => {
|
||||
// check for the „resume voice broadcast“ button
|
||||
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 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 { ReleaseAnnouncement } from "../../../src/components/structures/ReleaseAnnouncement";
|
||||
import Modal, { ModalManagerEvent } from "../../../src/Modal";
|
||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||
|
||||
describe("ReleaseAnnouncement", () => {
|
||||
beforeEach(async () => {
|
||||
// Reset the singleton instance of the ReleaseAnnouncementStore
|
||||
// @ts-ignore
|
||||
ReleaseAnnouncementStore.internalInstance = new ReleaseAnnouncementStore();
|
||||
});
|
||||
|
||||
function renderReleaseAnnouncement() {
|
||||
return render(
|
||||
<ReleaseAnnouncement
|
||||
feature="threadsActivityCentre"
|
||||
header="header"
|
||||
description="description"
|
||||
closeLabel="close"
|
||||
>
|
||||
<div>content</div>
|
||||
</ReleaseAnnouncement>,
|
||||
);
|
||||
}
|
||||
|
||||
test("render the release announcement and close it", async () => {
|
||||
renderReleaseAnnouncement();
|
||||
|
||||
// The release announcement is displayed
|
||||
expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible();
|
||||
// Click on the close button in the release announcement
|
||||
screen.getByRole("button", { name: "close" }).click();
|
||||
// The release announcement should be hidden after the close button is clicked
|
||||
await waitFor(() => expect(screen.queryByRole("dialog", { name: "header" })).toBeNull());
|
||||
});
|
||||
|
||||
test("when a dialog is opened, the release announcement should not be displayed", async () => {
|
||||
renderReleaseAnnouncement();
|
||||
// The release announcement is displayed
|
||||
expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible();
|
||||
|
||||
// Open a dialog
|
||||
act(() => {
|
||||
Modal.emit(ModalManagerEvent.Opened);
|
||||
});
|
||||
// The release announcement should be hidden after the dialog is opened
|
||||
expect(screen.queryByRole("dialog", { name: "header" })).toBeNull();
|
||||
|
||||
// Close the dialog
|
||||
act(() => {
|
||||
Modal.emit(ModalManagerEvent.Closed);
|
||||
});
|
||||
// The release announcement should be displayed after the dialog is closed
|
||||
expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible();
|
||||
});
|
||||
});
|
153
test/unit-tests/components/structures/RightPanel-test.tsx
Normal file
153
test/unit-tests/components/structures/RightPanel-test.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
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, waitFor } from "jest-matrix-react";
|
||||
import { jest } from "@jest/globals";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import _RightPanel from "../../../src/components/structures/RightPanel";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { stubClient, wrapInMatrixClientContext, mkRoom, wrapInSdkContext } from "../../test-utils";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore";
|
||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
|
||||
const RightPanelBase = wrapInMatrixClientContext(_RightPanel);
|
||||
|
||||
describe("RightPanel", () => {
|
||||
const resizeNotifier = new ResizeNotifier();
|
||||
|
||||
let cli: MockedObject<MatrixClient>;
|
||||
let context: SdkContextClass;
|
||||
let RightPanel: React.ComponentType<React.ComponentProps<typeof RightPanelBase>>;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.safeGet());
|
||||
DMRoomMap.makeShared(cli);
|
||||
context = new SdkContextClass();
|
||||
context.client = cli;
|
||||
RightPanel = wrapInSdkContext(RightPanelBase, context);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const roomChanged = new Promise<void>((resolve) => {
|
||||
const ref = dis.register((payload) => {
|
||||
if (payload.action === Action.ActiveRoomChanged) {
|
||||
dis.unregister(ref);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
dis.fire(Action.ViewHomePage); // Stop viewing any rooms
|
||||
await roomChanged;
|
||||
|
||||
dis.fire(Action.OnLoggedOut, true); // Shut down the stores
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const spinUpStores = async () => {
|
||||
// Selectively spin up the stores we need
|
||||
WidgetLayoutStore.instance.useUnitTestClient(cli);
|
||||
// @ts-ignore
|
||||
// This is private but it's the only way to selectively enable stores
|
||||
await WidgetLayoutStore.instance.onReady();
|
||||
|
||||
// Make sure we start with a clean store
|
||||
RightPanelStore.instance.reset();
|
||||
RightPanelStore.instance.useUnitTestClient(cli);
|
||||
// @ts-ignore
|
||||
await RightPanelStore.instance.onReady();
|
||||
};
|
||||
|
||||
const waitForRpsUpdate = () => new Promise<void>((resolve) => RightPanelStore.instance.once(UPDATE_EVENT, resolve));
|
||||
|
||||
it("renders info from only one room during room changes", async () => {
|
||||
const r1 = mkRoom(cli, "r1");
|
||||
const r2 = mkRoom(cli, "r2");
|
||||
|
||||
cli.getRoom.mockImplementation((roomId) => {
|
||||
if (roomId === "r1") return r1;
|
||||
if (roomId === "r2") return r2;
|
||||
return null;
|
||||
});
|
||||
|
||||
// Set up right panel state
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
|
||||
if (roomId === "r1") {
|
||||
return {
|
||||
history: [{ phase: RightPanelPhases.RoomMemberList }],
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
||||
if (roomId === "r2") {
|
||||
return {
|
||||
history: [{ phase: RightPanelPhases.RoomSummary }],
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
await spinUpStores();
|
||||
|
||||
// Run initial render with room 1, and also running lifecycle methods
|
||||
const { container, rerender } = render(
|
||||
<RightPanel
|
||||
room={r1}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
|
||||
/>,
|
||||
);
|
||||
// Wait for RPS room 1 updates to fire
|
||||
const rpsUpdated = waitForRpsUpdate();
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r1",
|
||||
});
|
||||
await rpsUpdated;
|
||||
await waitFor(() => expect(screen.queryByTestId("spinner")).not.toBeInTheDocument());
|
||||
|
||||
// room one will be in the RoomMemberList phase - confirm this is rendered
|
||||
expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1);
|
||||
|
||||
// wait for RPS room 2 updates to fire, then rerender
|
||||
const _rpsUpdated = waitForRpsUpdate();
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r2",
|
||||
});
|
||||
await _rpsUpdated;
|
||||
rerender(
|
||||
<RightPanel
|
||||
room={r2}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
|
||||
/>,
|
||||
);
|
||||
|
||||
// After all that setup, now to the interesting part...
|
||||
// We want to verify that as we change to room 2, we should always have
|
||||
// the correct right panel state for whichever room we are showing, so we
|
||||
// confirm we do not have the MemberList class on the page and that we have
|
||||
// the expected room title
|
||||
expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(0);
|
||||
expect(screen.getByRole("heading", { name: "r2" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
576
test/unit-tests/components/structures/RoomSearchView-test.tsx
Normal file
576
test/unit-tests/components/structures/RoomSearchView-test.tsx
Normal file
|
@ -0,0 +1,576 @@
|
|||
/*
|
||||
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 { render, screen } from "jest-matrix-react";
|
||||
import {
|
||||
Room,
|
||||
MatrixClient,
|
||||
IEvent,
|
||||
MatrixEvent,
|
||||
EventType,
|
||||
SearchResult,
|
||||
ISearchResults,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { RoomSearchView } from "../../../src/components/structures/RoomSearchView";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { searchPagination, SearchScope } from "../../../src/Searching";
|
||||
|
||||
jest.mock("../../../src/Searching", () => ({
|
||||
searchPagination: jest.fn(),
|
||||
SearchScope: jest.requireActual("../../../src/Searching").SearchScope,
|
||||
}));
|
||||
|
||||
describe("<RoomSearchView/>", () => {
|
||||
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
|
||||
const resizeNotifier = new ResizeNotifier();
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(async () => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
room = new Room("!room:server", client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should show a spinner before the promise resolves", async () => {
|
||||
const deferred = defer<ISearchResults>();
|
||||
|
||||
render(
|
||||
<RoomSearchView
|
||||
inProgress={true}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByTestId("messagePanelSearchSpinner");
|
||||
});
|
||||
|
||||
it("should render results when the promise resolves", async () => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$2",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Foo Test Bar", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$1",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Before", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
events_after: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$3",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "After", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: [],
|
||||
count: 1,
|
||||
})}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await screen.findByText("Before");
|
||||
await screen.findByText("Foo Test Bar");
|
||||
await screen.findByText("After");
|
||||
});
|
||||
|
||||
it("should highlight words correctly", async () => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.Room}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$2",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Foo Test Bar", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: ["test"],
|
||||
count: 1,
|
||||
})}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const text = await screen.findByText("Test");
|
||||
expect(text).toHaveClass("mx_EventTile_searchHighlight");
|
||||
});
|
||||
|
||||
it("should show spinner above results when backpaginating", async () => {
|
||||
const searchResults: ISearchResults = {
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$2",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Foo Test Bar", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: ["test"],
|
||||
next_batch: "next_batch",
|
||||
count: 2,
|
||||
};
|
||||
|
||||
mocked(searchPagination).mockResolvedValue({
|
||||
...searchResults,
|
||||
results: [
|
||||
...searchResults.results,
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$4",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 4,
|
||||
content: { body: "Potato", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
next_batch: undefined,
|
||||
});
|
||||
const onUpdate = jest.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={true}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await screen.findByRole("progressbar");
|
||||
await screen.findByText("Potato");
|
||||
expect(onUpdate).toHaveBeenCalledWith(false, expect.objectContaining({}));
|
||||
|
||||
rerender(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("progressbar")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should handle resolutions after unmounting sanely", async () => {
|
||||
const deferred = defer<ISearchResults>();
|
||||
|
||||
const { unmount } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
unmount();
|
||||
deferred.resolve({
|
||||
results: [],
|
||||
highlights: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle rejections after unmounting sanely", async () => {
|
||||
const deferred = defer<ISearchResults>();
|
||||
|
||||
const { unmount } = render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
unmount();
|
||||
deferred.reject({
|
||||
results: [],
|
||||
highlights: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should show modal if error is encountered", async () => {
|
||||
const deferred = defer<ISearchResults>();
|
||||
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={deferred.promise}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
deferred.reject(new Error("Some error"));
|
||||
|
||||
await screen.findByText("Search failed");
|
||||
await screen.findByText("Some error");
|
||||
});
|
||||
|
||||
it("should combine search results when the query is present in multiple sucessive messages", async () => {
|
||||
const searchResults: ISearchResults = {
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$4",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Foo2", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$3",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Between", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
events_after: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$5",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "After", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$2",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Foo", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$1",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Before", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
events_after: [
|
||||
{
|
||||
room_id: room.roomId,
|
||||
event_id: "$3",
|
||||
sender: client.getUserId() ?? "",
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Between", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: [],
|
||||
next_batch: "",
|
||||
count: 1,
|
||||
};
|
||||
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve(searchResults)}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const beforeNode = await screen.findByText("Before");
|
||||
const fooNode = await screen.findByText("Foo");
|
||||
const betweenNode = await screen.findByText("Between");
|
||||
const foo2Node = await screen.findByText("Foo2");
|
||||
const afterNode = await screen.findByText("After");
|
||||
|
||||
expect((await screen.findAllByText("Between")).length).toBe(1);
|
||||
|
||||
expect(beforeNode.compareDocumentPosition(fooNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(fooNode.compareDocumentPosition(betweenNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(betweenNode.compareDocumentPosition(foo2Node) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(foo2Node.compareDocumentPosition(afterNode) == Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should pass appropriate permalink creator for all rooms search", async () => {
|
||||
const room2 = new Room("!room2:server", client, client.getSafeUserId());
|
||||
const room3 = new Room("!room3:server", client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockImplementation(
|
||||
(roomId) => [room, room2, room3].find((r) => r.roomId === roomId) ?? null,
|
||||
);
|
||||
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomSearchView
|
||||
inProgress={false}
|
||||
term="search term"
|
||||
scope={SearchScope.All}
|
||||
promise={Promise.resolve<ISearchResults>({
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
room_id: room.roomId,
|
||||
event_id: "$2",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Room 1", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 2,
|
||||
result: {
|
||||
room_id: room2.roomId,
|
||||
event_id: "$22",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Room 2", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 2,
|
||||
result: {
|
||||
room_id: room2.roomId,
|
||||
event_id: "$23",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 2,
|
||||
content: { body: "Room 2 message 2", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 3,
|
||||
result: {
|
||||
room_id: room3.roomId,
|
||||
event_id: "$32",
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 1,
|
||||
content: { body: "Room 3", msgtype: "m.text" },
|
||||
type: EventType.RoomMessage,
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: [],
|
||||
count: 1,
|
||||
})}
|
||||
resizeNotifier={resizeNotifier}
|
||||
className="someClass"
|
||||
onUpdate={jest.fn()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const event1 = await screen.findByText("Room 1");
|
||||
expect(event1.closest(".mx_EventTile_line")!.querySelector("a")).toHaveAttribute(
|
||||
"href",
|
||||
`https://matrix.to/#/${room.roomId}/$2`,
|
||||
);
|
||||
|
||||
const event2 = await screen.findByText("Room 2");
|
||||
expect(event2.closest(".mx_EventTile_line")!.querySelector("a")).toHaveAttribute(
|
||||
"href",
|
||||
`https://matrix.to/#/${room2.roomId}/$22`,
|
||||
);
|
||||
|
||||
const event2Message2 = await screen.findByText("Room 2 message 2");
|
||||
expect(event2Message2.closest(".mx_EventTile_line")!.querySelector("a")).toHaveAttribute(
|
||||
"href",
|
||||
`https://matrix.to/#/${room2.roomId}/$23`,
|
||||
);
|
||||
|
||||
const event3 = await screen.findByText("Room 3");
|
||||
expect(event3.closest(".mx_EventTile_line")!.querySelector("a")).toHaveAttribute(
|
||||
"href",
|
||||
`https://matrix.to/#/${room3.roomId}/$32`,
|
||||
);
|
||||
});
|
||||
});
|
150
test/unit-tests/components/structures/RoomStatusBar-test.tsx
Normal file
150
test/unit-tests/components/structures/RoomStatusBar-test.tsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
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 {
|
||||
MatrixClient,
|
||||
PendingEventOrdering,
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
MatrixError,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import RoomStatusBar, { getUnsentMessages } from "../../../src/components/structures/RoomStatusBar";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { mkEvent, stubClient } from "../../test-utils/test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
|
||||
describe("RoomStatusBar", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
client.getSyncStateData = jest.fn().mockReturnValue({});
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
});
|
||||
|
||||
const getComponent = () =>
|
||||
render(<RoomStatusBar room={room} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
describe("getUnsentMessages", () => {
|
||||
it("returns no unsent messages", () => {
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("checks the event status", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(1);
|
||||
event.status = EventStatus.SENT;
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("only returns events related to a thread", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
const { rootEvent, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
rootEvent.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(rootEvent, rootEvent.getId()!);
|
||||
for (const event of events) {
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(event, Date.now() + Math.random() + "");
|
||||
}
|
||||
|
||||
const pendingEvents = getUnsentMessages(room, rootEvent.getId());
|
||||
|
||||
expect(pendingEvents[0].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[1].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[2].threadRootId).toBe(rootEvent.getId());
|
||||
|
||||
// Filters out the non thread events
|
||||
expect(pendingEvents.every((ev) => ev.getId() !== event.getId())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RoomStatusBar />", () => {
|
||||
it("should render nothing when room has no error or unsent messages", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container.firstChild).toBe(null);
|
||||
});
|
||||
|
||||
describe("unsent messages", () => {
|
||||
it("should render warning when messages are unsent due to consent", () => {
|
||||
const unsentMessage = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
unsentMessage.status = EventStatus.NOT_SENT;
|
||||
unsentMessage.error = new MatrixError({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
data: { consent_uri: "terms.com" },
|
||||
});
|
||||
|
||||
room.addPendingEvent(unsentMessage, "123");
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render warning when messages are unsent due to resource limit", () => {
|
||||
const unsentMessage = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
unsentMessage.status = EventStatus.NOT_SENT;
|
||||
unsentMessage.error = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: { limit_type: "monthly_active_user" },
|
||||
});
|
||||
|
||||
room.addPendingEvent(unsentMessage, "123");
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 { RoomStatusBarUnsentMessages } from "../../../src/components/structures/RoomStatusBarUnsentMessages";
|
||||
import { StaticNotificationState } from "../../../src/stores/notifications/StaticNotificationState";
|
||||
|
||||
describe("RoomStatusBarUnsentMessages", () => {
|
||||
const title = "test title";
|
||||
const description = "test description";
|
||||
const buttonsText = "test buttons";
|
||||
const buttons = <div>{buttonsText}</div>;
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={title}
|
||||
description={description}
|
||||
buttons={buttons}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the values passed as props", () => {
|
||||
screen.getByText(title);
|
||||
screen.getByText(description);
|
||||
screen.getByText(buttonsText);
|
||||
// notification state
|
||||
screen.getByText("!");
|
||||
});
|
||||
});
|
686
test/unit-tests/components/structures/RoomView-test.tsx
Normal file
686
test/unit-tests/components/structures/RoomView-test.tsx
Normal file
|
@ -0,0 +1,686 @@
|
|||
/*
|
||||
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, { createRef, RefObject } from "react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import {
|
||||
ClientEvent,
|
||||
MatrixClient,
|
||||
Room,
|
||||
RoomEvent,
|
||||
EventType,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
RoomStateEvent,
|
||||
MatrixEvent,
|
||||
SearchResult,
|
||||
IEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
mockPlatformPeg,
|
||||
unmockPlatformPeg,
|
||||
wrapInMatrixClientContext,
|
||||
flushPromises,
|
||||
mkEvent,
|
||||
setupAsyncStoreWithClient,
|
||||
filterConsole,
|
||||
mkRoomMemberJoinEvent,
|
||||
mkThirdPartyInviteEvent,
|
||||
emitPromise,
|
||||
createTestClient,
|
||||
untilDispatch,
|
||||
} from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import dis, { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { NotificationState } from "../../../src/stores/notifications/NotificationState";
|
||||
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom";
|
||||
import { DirectoryMember } from "../../../src/utils/direct-messages";
|
||||
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
|
||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||
import { SDKContext, SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import VoipUserMapper from "../../../src/VoipUserMapper";
|
||||
import WidgetUtils from "../../../src/utils/WidgetUtils";
|
||||
import { WidgetType } from "../../../src/widgets/WidgetType";
|
||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||
import { ViewRoomErrorPayload } from "../../../src/dispatcher/payloads/ViewRoomErrorPayload";
|
||||
import { SearchScope } from "../../../src/Searching";
|
||||
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../src/utils/crypto";
|
||||
|
||||
const RoomView = wrapInMatrixClientContext(_RoomView);
|
||||
|
||||
describe("RoomView", () => {
|
||||
let cli: MockedObject<MatrixClient>;
|
||||
let room: Room;
|
||||
let rooms: Map<string, Room>;
|
||||
let roomCount = 0;
|
||||
let stores: SdkContextClass;
|
||||
|
||||
// mute some noise
|
||||
filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability");
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg({ reload: () => {} });
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
|
||||
jest.spyOn(room, "findPredecessor");
|
||||
room.getPendingEvents = () => [];
|
||||
rooms = new Map();
|
||||
rooms.set(room.roomId, room);
|
||||
cli.getRoom.mockImplementation((roomId: string | undefined) => rooms.get(roomId || "") || null);
|
||||
// Re-emit certain events on the mocked client
|
||||
room.on(RoomEvent.Timeline, (...args) => cli.emit(RoomEvent.Timeline, ...args));
|
||||
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
|
||||
|
||||
DMRoomMap.makeShared(cli);
|
||||
stores = new SdkContextClass();
|
||||
stores.client = cli;
|
||||
stores.rightPanelStore.useUnitTestClient(cli);
|
||||
|
||||
jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockPlatformPeg();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountRoomView = async (ref?: RefObject<_RoomView>): Promise<RenderResult> => {
|
||||
if (stores.roomViewStore.getRoomId() !== room.roomId) {
|
||||
const switchedRoom = new Promise<void>((resolve) => {
|
||||
const subFn = () => {
|
||||
if (stores.roomViewStore.getRoomId()) {
|
||||
stores.roomViewStore.off(UPDATE_EVENT, subFn);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
stores.roomViewStore.on(UPDATE_EVENT, subFn);
|
||||
});
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
await switchedRoom;
|
||||
}
|
||||
|
||||
const roomView = render(
|
||||
<SDKContext.Provider value={stores}>
|
||||
<RoomView
|
||||
// threepidInvite should be optional on RoomView props
|
||||
// it is treated as optional in RoomView
|
||||
threepidInvite={undefined as any}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
forceTimeline={false}
|
||||
wrappedRef={ref as any}
|
||||
/>
|
||||
</SDKContext.Provider>,
|
||||
);
|
||||
await flushPromises();
|
||||
return roomView;
|
||||
};
|
||||
|
||||
const renderRoomView = async (switchRoom = true): Promise<ReturnType<typeof render>> => {
|
||||
if (switchRoom && stores.roomViewStore.getRoomId() !== room.roomId) {
|
||||
const switchedRoom = new Promise<void>((resolve) => {
|
||||
const subFn = () => {
|
||||
if (stores.roomViewStore.getRoomId()) {
|
||||
stores.roomViewStore.off(UPDATE_EVENT, subFn);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
stores.roomViewStore.on(UPDATE_EVENT, subFn);
|
||||
});
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
await switchedRoom;
|
||||
}
|
||||
|
||||
const roomView = render(
|
||||
<SDKContext.Provider value={stores}>
|
||||
<RoomView
|
||||
// threepidInvite should be optional on RoomView props
|
||||
// it is treated as optional in RoomView
|
||||
threepidInvite={undefined as any}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
forceTimeline={false}
|
||||
onRegistered={jest.fn()}
|
||||
/>
|
||||
</SDKContext.Provider>,
|
||||
);
|
||||
await flushPromises();
|
||||
return roomView;
|
||||
};
|
||||
const getRoomViewInstance = async (): Promise<_RoomView> => {
|
||||
const ref = createRef<_RoomView>();
|
||||
await mountRoomView(ref);
|
||||
return ref.current!;
|
||||
};
|
||||
|
||||
it("when there is no room predecessor, getHiddenHighlightCount should return 0", async () => {
|
||||
const instance = await getRoomViewInstance();
|
||||
expect(instance.getHiddenHighlightCount()).toBe(0);
|
||||
});
|
||||
|
||||
describe("when there is an old room", () => {
|
||||
let instance: _RoomView;
|
||||
let oldRoom: Room;
|
||||
|
||||
beforeEach(async () => {
|
||||
instance = await getRoomViewInstance();
|
||||
oldRoom = new Room("!old:example.com", cli, cli.getSafeUserId());
|
||||
rooms.set(oldRoom.roomId, oldRoom);
|
||||
jest.spyOn(room, "findPredecessor").mockReturnValue({ roomId: oldRoom.roomId });
|
||||
});
|
||||
|
||||
it("and it has 0 unreads, getHiddenHighlightCount should return 0", async () => {
|
||||
jest.spyOn(oldRoom, "getUnreadNotificationCount").mockReturnValue(0);
|
||||
expect(instance.getHiddenHighlightCount()).toBe(0);
|
||||
// assert that msc3946ProcessDynamicPredecessor is false by default
|
||||
expect(room.findPredecessor).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("and it has 23 unreads, getHiddenHighlightCount should return 23", async () => {
|
||||
jest.spyOn(oldRoom, "getUnreadNotificationCount").mockReturnValue(23);
|
||||
expect(instance.getHiddenHighlightCount()).toBe(23);
|
||||
});
|
||||
|
||||
describe("and feature_dynamic_room_predecessors is enabled", () => {
|
||||
beforeEach(() => {
|
||||
instance.setState({ msc3946ProcessDynamicPredecessor: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
instance.setState({ msc3946ProcessDynamicPredecessor: false });
|
||||
});
|
||||
|
||||
it("should pass the setting to findPredecessor", async () => {
|
||||
expect(instance.getHiddenHighlightCount()).toBe(0);
|
||||
expect(room.findPredecessor).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("updates url preview visibility on encryption state change", async () => {
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
// we should be starting unencrypted
|
||||
expect(cli.isCryptoEnabled()).toEqual(false);
|
||||
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
|
||||
|
||||
const roomViewInstance = await getRoomViewInstance();
|
||||
|
||||
// in a default (non-encrypted room, it should start out with url previews enabled)
|
||||
// This is a white-box test in that we're asserting things about the state, which
|
||||
// is not ideal, but asserting that a URL preview just isn't there could risk the
|
||||
// test being invalid because the previews just hasn't rendered yet. This feels
|
||||
// like the safest way I think?
|
||||
// This also relies on the default settings being URL previews on normally and
|
||||
// off for e2e rooms because 1) it's probably useful to assert this and
|
||||
// 2) SettingsStore is a static class and so very hard to mock out.
|
||||
expect(roomViewInstance.state.showUrlPreview).toBe(true);
|
||||
|
||||
// now enable encryption
|
||||
cli.isCryptoEnabled.mockReturnValue(true);
|
||||
cli.isRoomEncrypted.mockReturnValue(true);
|
||||
|
||||
// and fake an encryption event into the room to prompt it to re-check
|
||||
room.addLiveEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.room.encryption",
|
||||
sender: cli.getUserId()!,
|
||||
content: {},
|
||||
event_id: "someid",
|
||||
room_id: room.roomId,
|
||||
}),
|
||||
]);
|
||||
|
||||
// URL previews should now be disabled
|
||||
expect(roomViewInstance.state.showUrlPreview).toBe(false);
|
||||
});
|
||||
|
||||
it("updates live timeline when a timeline reset happens", async () => {
|
||||
const roomViewInstance = await getRoomViewInstance();
|
||||
const oldTimeline = roomViewInstance.state.liveTimeline;
|
||||
|
||||
room.getUnfilteredTimelineSet().resetLiveTimeline();
|
||||
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
|
||||
});
|
||||
|
||||
describe("with virtual rooms", () => {
|
||||
it("checks for a virtual room on initial load", async () => {
|
||||
const { container } = await renderRoomView();
|
||||
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
|
||||
|
||||
// quick check that rendered without error
|
||||
expect(container.querySelector(".mx_ErrorBoundary")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("checks for a virtual room on room event", async () => {
|
||||
await renderRoomView();
|
||||
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
|
||||
|
||||
cli.emit(ClientEvent.Room, room);
|
||||
|
||||
// called again after room event
|
||||
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("video rooms", () => {
|
||||
beforeEach(async () => {
|
||||
// Make it a video room
|
||||
room.isElementVideoRoom = () => true;
|
||||
await SettingsStore.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
||||
});
|
||||
|
||||
it("normally doesn't open the chat panel", async () => {
|
||||
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(false);
|
||||
await mountRoomView();
|
||||
expect(stores.rightPanelStore.isOpen).toEqual(false);
|
||||
});
|
||||
|
||||
it("opens the chat panel if there are unread messages", async () => {
|
||||
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(true);
|
||||
await mountRoomView();
|
||||
expect(stores.rightPanelStore.isOpen).toEqual(true);
|
||||
expect(stores.rightPanelStore.currentCard.phase).toEqual(RightPanelPhases.Timeline);
|
||||
});
|
||||
|
||||
it("should render joined video room view", async () => {
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
||||
const { asFragment } = await mountRoomView();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a local room", () => {
|
||||
let localRoom: LocalRoom;
|
||||
|
||||
beforeEach(async () => {
|
||||
localRoom = room = await createDmLocalRoom(cli, [new DirectoryMember({ user_id: "@user:example.com" })]);
|
||||
rooms.set(localRoom.roomId, localRoom);
|
||||
cli.store.storeRoom(room);
|
||||
});
|
||||
|
||||
it("should remove the room from the store on unmount", async () => {
|
||||
const { unmount } = await renderRoomView();
|
||||
unmount();
|
||||
expect(cli.store.removeRoom).toHaveBeenCalledWith(room.roomId);
|
||||
});
|
||||
|
||||
describe("in state NEW", () => {
|
||||
it("should match the snapshot", async () => {
|
||||
const { container } = await renderRoomView();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("that is encrypted", () => {
|
||||
beforeEach(() => {
|
||||
mocked(cli.isRoomEncrypted).mockReturnValue(true);
|
||||
localRoom.encrypted = true;
|
||||
localRoom.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
event_id: `~${localRoom.roomId}:${cli.makeTxnId()}`,
|
||||
type: EventType.RoomEncryption,
|
||||
content: {
|
||||
algorithm: MEGOLM_ENCRYPTION_ALGORITHM,
|
||||
},
|
||||
sender: cli.getUserId()!,
|
||||
state_key: "",
|
||||
room_id: localRoom.roomId,
|
||||
origin_server_ts: Date.now(),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should match the snapshot", async () => {
|
||||
const { container } = await renderRoomView();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("in state CREATING should match the snapshot", async () => {
|
||||
localRoom.state = LocalRoomState.CREATING;
|
||||
const { container } = await renderRoomView();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("in state ERROR", () => {
|
||||
beforeEach(async () => {
|
||||
localRoom.state = LocalRoomState.ERROR;
|
||||
});
|
||||
|
||||
it("should match the snapshot", async () => {
|
||||
const { container } = await renderRoomView();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("clicking retry should set the room state to new dispatch a local room event", async () => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch");
|
||||
const { getByText } = await renderRoomView();
|
||||
fireEvent.click(getByText("Retry"));
|
||||
expect(localRoom.state).toBe(LocalRoomState.NEW);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "local_room_event",
|
||||
roomId: room.roomId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a DM room with a single third-party invite", () => {
|
||||
beforeEach(async () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId),
|
||||
mkThirdPartyInviteEvent(cli.getSafeUserId(), "user@example.com", room.roomId),
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
|
||||
mocked(cli).isRoomEncrypted.mockReturnValue(true);
|
||||
await renderRoomView();
|
||||
});
|
||||
|
||||
it("should render the »waiting for third-party« view", () => {
|
||||
expect(screen.getByText("Waiting for users to join Element")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// no message composer
|
||||
expect(screen.queryByText("Send an encrypted message…")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Send a message…")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a RoomView", () => {
|
||||
const widget1Id = "widget1";
|
||||
const widget2Id = "widget2";
|
||||
const otherUserId = "@other:example.com";
|
||||
|
||||
const addJitsiWidget = async (id: string, user: string, ts?: number): Promise<void> => {
|
||||
const widgetEvent = mkEvent({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
user,
|
||||
type: "im.vector.modular.widgets",
|
||||
content: {
|
||||
id,
|
||||
name: "Jitsi",
|
||||
type: WidgetType.JITSI.preferred,
|
||||
url: "https://example.com",
|
||||
},
|
||||
skey: id,
|
||||
ts,
|
||||
});
|
||||
room.addLiveEvents([widgetEvent]);
|
||||
room.currentState.setStateEvents([widgetEvent]);
|
||||
cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null);
|
||||
await flushPromises();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(WidgetUtils, "setRoomWidget");
|
||||
const widgetStore = WidgetStore.instance;
|
||||
await setupAsyncStoreWithClient(widgetStore, cli);
|
||||
getRoomViewInstance();
|
||||
});
|
||||
|
||||
const itShouldNotRemoveTheLastWidget = (): void => {
|
||||
it("should not remove the last widget", (): void => {
|
||||
expect(WidgetUtils.setRoomWidget).not.toHaveBeenCalledWith(room.roomId, widget2Id);
|
||||
});
|
||||
};
|
||||
|
||||
describe("and there is a Jitsi widget from another user", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget1Id, otherUserId, 10_000);
|
||||
});
|
||||
|
||||
describe("and the current user adds a Jitsi widget after 10s", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 20_000);
|
||||
});
|
||||
|
||||
it("the last Jitsi widget should be removed", () => {
|
||||
expect(WidgetUtils.setRoomWidget).toHaveBeenCalledWith(cli, room.roomId, widget2Id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the current user adds a Jitsi widget after two minutes", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 130_000);
|
||||
});
|
||||
|
||||
itShouldNotRemoveTheLastWidget();
|
||||
});
|
||||
|
||||
describe("and the current user adds a Jitsi widget without timestamp", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget2Id, cli.getSafeUserId());
|
||||
});
|
||||
|
||||
itShouldNotRemoveTheLastWidget();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is a Jitsi widget from another user without timestamp", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget1Id, otherUserId);
|
||||
});
|
||||
|
||||
describe("and the current user adds a Jitsi widget", () => {
|
||||
beforeEach(async () => {
|
||||
await addJitsiWidget(widget2Id, cli.getSafeUserId(), 10_000);
|
||||
});
|
||||
|
||||
itShouldNotRemoveTheLastWidget();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error view if failed to look up room alias", async () => {
|
||||
const { asFragment, findByText } = await renderRoomView(false);
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomErrorPayload>({
|
||||
action: Action.ViewRoomError,
|
||||
room_alias: "#addy:server",
|
||||
room_id: null,
|
||||
err: new MatrixError({ errcode: "M_NOT_FOUND" }),
|
||||
});
|
||||
await emitPromise(stores.roomViewStore, UPDATE_EVENT);
|
||||
|
||||
await findByText("Are you sure you're at the right place?");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("knock rooms", () => {
|
||||
const client = createTestClient();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
jest.spyOn(dis, "dispatch");
|
||||
});
|
||||
|
||||
it("allows to request to join", async () => {
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
|
||||
jest.spyOn(client, "knockRoom").mockResolvedValue({ room_id: room.roomId });
|
||||
|
||||
await mountRoomView();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Request access" }));
|
||||
await untilDispatch(Action.SubmitAskToJoin, dis);
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "submit_ask_to_join",
|
||||
roomId: room.roomId,
|
||||
opts: { reason: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows to cancel a join request", async () => {
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
|
||||
jest.spyOn(client, "leave").mockResolvedValue({});
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock);
|
||||
|
||||
await mountRoomView();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel request" }));
|
||||
await untilDispatch(Action.CancelAskToJoin, dis);
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId });
|
||||
});
|
||||
});
|
||||
|
||||
it("should close search results when edit is clicked", async () => {
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
|
||||
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
|
||||
|
||||
const roomViewRef = createRef<_RoomView>();
|
||||
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
|
||||
// @ts-ignore - triggering a search organically is a lot of work
|
||||
roomViewRef.current!.setState({
|
||||
search: {
|
||||
searchId: 1,
|
||||
roomId: room.roomId,
|
||||
term: "search term",
|
||||
scope: SearchScope.Room,
|
||||
promise: Promise.resolve({
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
content: {
|
||||
body: "search term",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId",
|
||||
sender: cli.getSafeUserId(),
|
||||
origin_server_ts: 123456789,
|
||||
room_id: room.roomId,
|
||||
},
|
||||
context: {
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: [],
|
||||
count: 1,
|
||||
}),
|
||||
inProgress: false,
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
|
||||
});
|
||||
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
|
||||
|
||||
await userEvent.hover(getByText("search term"));
|
||||
await userEvent.click(await findByLabelText("Edit"));
|
||||
|
||||
await prom;
|
||||
});
|
||||
|
||||
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
|
||||
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
|
||||
rooms.set(room2.roomId, room2);
|
||||
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
|
||||
|
||||
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
|
||||
|
||||
const roomViewRef = createRef<_RoomView>();
|
||||
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
|
||||
// @ts-ignore - triggering a search organically is a lot of work
|
||||
roomViewRef.current!.setState({
|
||||
search: {
|
||||
searchId: 1,
|
||||
roomId: room.roomId,
|
||||
term: "search term",
|
||||
scope: SearchScope.All,
|
||||
promise: Promise.resolve({
|
||||
results: [
|
||||
SearchResult.fromJson(
|
||||
{
|
||||
rank: 1,
|
||||
result: {
|
||||
content: {
|
||||
body: "search term",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
type: "m.room.message",
|
||||
event_id: "$eventId",
|
||||
sender: cli.getSafeUserId(),
|
||||
origin_server_ts: 123456789,
|
||||
room_id: room2.roomId,
|
||||
},
|
||||
context: {
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
profile_info: {},
|
||||
},
|
||||
},
|
||||
eventMapper,
|
||||
),
|
||||
],
|
||||
highlights: [],
|
||||
count: 1,
|
||||
}),
|
||||
inProgress: false,
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
|
||||
});
|
||||
const prom = untilDispatch(Action.ViewRoom, dis);
|
||||
|
||||
await userEvent.hover(getByText("search term"));
|
||||
await userEvent.click(await findByLabelText("Edit"));
|
||||
|
||||
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
|
||||
});
|
||||
|
||||
it("fires Action.RoomLoaded", async () => {
|
||||
jest.spyOn(dis, "dispatch");
|
||||
await mountRoomView();
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
|
||||
});
|
||||
});
|
328
test/unit-tests/components/structures/SpaceHierarchy-test.tsx
Normal file
328
test/unit-tests/components/structures/SpaceHierarchy-test.tsx
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
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 { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { HierarchyRoom, JoinRule, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { mkStubRoom, stubClient } from "../../test-utils";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import SpaceHierarchy, { showRoom, toLocalRoom } from "../../../src/components/structures/SpaceHierarchy";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
|
||||
describe("SpaceHierarchy", () => {
|
||||
describe("showRoom", () => {
|
||||
let client: MatrixClient;
|
||||
let hierarchy: RoomHierarchy;
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
room = new Room("room-id", client, "@alice:example.com");
|
||||
hierarchy = new RoomHierarchy(room);
|
||||
|
||||
jest.spyOn(client, "isGuest").mockReturnValue(false);
|
||||
|
||||
jest.spyOn(hierarchy.roomMap, "get").mockReturnValue({
|
||||
children_state: [],
|
||||
room_id: "room-id2",
|
||||
canonical_alias: "canonical-alias",
|
||||
aliases: ["uncanonical-alias", "canonical-alias"],
|
||||
world_readable: true,
|
||||
guest_can_join: false,
|
||||
num_joined_members: 35,
|
||||
});
|
||||
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("shows room", () => {
|
||||
showRoom(client, hierarchy, "room-id2");
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
should_peek: true,
|
||||
room_alias: "canonical-alias",
|
||||
room_id: "room-id2",
|
||||
via_servers: [],
|
||||
oob_data: {
|
||||
avatarUrl: undefined,
|
||||
name: "canonical-alias",
|
||||
},
|
||||
roomType: undefined,
|
||||
metricsTrigger: "RoomDirectory",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toLocalRoom", () => {
|
||||
stubClient();
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const roomV1 = mkStubRoom("room-id-1", "Room V1", client);
|
||||
const roomV2 = mkStubRoom("room-id-2", "Room V2", client);
|
||||
const roomV3 = mkStubRoom("room-id-3", "Room V3", client);
|
||||
jest.spyOn(client, "getRoomUpgradeHistory").mockReturnValue([roomV1, roomV2, roomV3]);
|
||||
|
||||
it("grabs last room that is in hierarchy when latest version is in hierarchy", () => {
|
||||
const hierarchy = {
|
||||
roomMap: new Map([
|
||||
[roomV1.roomId, { room_id: roomV1.roomId } as HierarchyRoom],
|
||||
[roomV2.roomId, { room_id: roomV2.roomId } as HierarchyRoom],
|
||||
[roomV3.roomId, { room_id: roomV3.roomId } as HierarchyRoom],
|
||||
]),
|
||||
} as RoomHierarchy;
|
||||
const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV1.room_id).toEqual(roomV3.roomId);
|
||||
const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV2.room_id).toEqual(roomV3.roomId);
|
||||
const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV3.room_id).toEqual(roomV3.roomId);
|
||||
});
|
||||
|
||||
it("grabs last room that is in hierarchy when latest version is *not* in hierarchy", () => {
|
||||
const hierarchy = {
|
||||
roomMap: new Map([
|
||||
[roomV1.roomId, { room_id: roomV1.roomId } as HierarchyRoom],
|
||||
[roomV2.roomId, { room_id: roomV2.roomId } as HierarchyRoom],
|
||||
]),
|
||||
} as RoomHierarchy;
|
||||
const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV1.room_id).toEqual(roomV2.roomId);
|
||||
const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV2.room_id).toEqual(roomV2.roomId);
|
||||
const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV3.room_id).toEqual(roomV2.roomId);
|
||||
});
|
||||
|
||||
it("returns specified room when none of the versions is in hierarchy", () => {
|
||||
const hierarchy = { roomMap: new Map([]) } as RoomHierarchy;
|
||||
const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV1.room_id).toEqual(roomV1.roomId);
|
||||
const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV2.room_id).toEqual(roomV2.roomId);
|
||||
const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(localRoomV3.room_id).toEqual(roomV3.roomId);
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
});
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
mocked(client.getRoomUpgradeHistory).mockClear();
|
||||
const hierarchy = { roomMap: new Map([]) } as RoomHierarchy;
|
||||
toLocalRoom(client, { room_id: roomV1.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(roomV1.roomId, true, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is enabled", () => {
|
||||
beforeEach(() => {
|
||||
// Turn on feature_dynamic_room_predecessors setting
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_dynamic_room_predecessors",
|
||||
);
|
||||
});
|
||||
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
mocked(client.getRoomUpgradeHistory).mockClear();
|
||||
const hierarchy = { roomMap: new Map([]) } as RoomHierarchy;
|
||||
toLocalRoom(client, { room_id: roomV1.roomId } as HierarchyRoom, hierarchy);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(roomV1.roomId, true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SpaceHierarchy />", () => {
|
||||
beforeEach(() => {
|
||||
// IntersectionObserver isn't available in test environment
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null,
|
||||
} as ResizeObserver);
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
});
|
||||
|
||||
stubClient();
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
|
||||
const root = mkStubRoom("space-id-1", "Space 1", client);
|
||||
const room1 = mkStubRoom("room-id-2", "Room 1", client);
|
||||
const room2 = mkStubRoom("room-id-3", "Room 2", client);
|
||||
const space1 = mkStubRoom("space-id-4", "Space 2", client);
|
||||
const room3 = mkStubRoom("room-id-5", "Room 3", client);
|
||||
mocked(client.getRooms).mockReturnValue([root]);
|
||||
mocked(client.getRoom).mockImplementation(
|
||||
(roomId) => client.getRooms().find((room) => room.roomId === roomId) ?? null,
|
||||
);
|
||||
[room1, room2, space1, room3].forEach((r) => mocked(r.getMyMembership).mockReturnValue(KnownMembership.Leave));
|
||||
|
||||
const hierarchyRoot: HierarchyRoom = {
|
||||
room_id: root.roomId,
|
||||
num_joined_members: 1,
|
||||
room_type: "m.space",
|
||||
children_state: [
|
||||
{
|
||||
state_key: room1.roomId,
|
||||
content: { order: "1" },
|
||||
origin_server_ts: 111,
|
||||
type: "m.space.child",
|
||||
sender: "@other:server",
|
||||
},
|
||||
{
|
||||
state_key: room2.roomId,
|
||||
content: { order: "2" },
|
||||
origin_server_ts: 111,
|
||||
type: "m.space.child",
|
||||
sender: "@other:server",
|
||||
},
|
||||
{
|
||||
state_key: space1.roomId,
|
||||
content: { order: "3" },
|
||||
origin_server_ts: 111,
|
||||
type: "m.space.child",
|
||||
sender: "@other:server",
|
||||
},
|
||||
{
|
||||
state_key: "!knock1:server",
|
||||
content: { order: "4" },
|
||||
origin_server_ts: 111,
|
||||
type: "m.space.child",
|
||||
sender: "@other:server",
|
||||
},
|
||||
],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
};
|
||||
const hierarchyRoom1: HierarchyRoom = {
|
||||
room_id: room1.roomId,
|
||||
num_joined_members: 2,
|
||||
children_state: [],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
};
|
||||
const hierarchyRoom2: HierarchyRoom = {
|
||||
room_id: room2.roomId,
|
||||
num_joined_members: 3,
|
||||
children_state: [],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
};
|
||||
const hierarchyRoom3: HierarchyRoom = {
|
||||
name: "Nested room",
|
||||
room_id: room3.roomId,
|
||||
num_joined_members: 3,
|
||||
children_state: [],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
};
|
||||
const hierarchySpace1: HierarchyRoom = {
|
||||
room_id: space1.roomId,
|
||||
name: "Nested space",
|
||||
num_joined_members: 1,
|
||||
room_type: "m.space",
|
||||
children_state: [
|
||||
{
|
||||
state_key: room3.roomId,
|
||||
content: { order: "1" },
|
||||
origin_server_ts: 111,
|
||||
type: "m.space.child",
|
||||
sender: "@other:server",
|
||||
},
|
||||
],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
};
|
||||
const hierarchyKnockRoom1: HierarchyRoom = {
|
||||
room_id: "!knock1:server",
|
||||
name: "Knock room",
|
||||
num_joined_members: 3,
|
||||
children_state: [],
|
||||
world_readable: true,
|
||||
guest_can_join: true,
|
||||
join_rule: JoinRule.Knock,
|
||||
};
|
||||
|
||||
mocked(client.getRoomHierarchy).mockResolvedValue({
|
||||
rooms: [
|
||||
hierarchyRoot,
|
||||
hierarchyRoom1,
|
||||
hierarchyRoom2,
|
||||
hierarchySpace1,
|
||||
hierarchyRoom3,
|
||||
hierarchyKnockRoom1,
|
||||
],
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
space: root,
|
||||
showRoom: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}): React.ReactElement => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<SpaceHierarchy {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
it("renders", async () => {
|
||||
const { asFragment } = render(getComponent());
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should join subspace when joining nested room", async () => {
|
||||
mocked(client.joinRoom).mockResolvedValue({} as Room);
|
||||
|
||||
const { getByText } = render(getComponent());
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
const button = getByText("Nested room")!.closest("li")!.querySelector(".mx_AccessibleButton_kind_primary")!;
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(client.joinRoom).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
// Joins subspace
|
||||
expect(client.joinRoom).toHaveBeenCalledWith(space1.roomId, expect.any(Object));
|
||||
expect(client.joinRoom).toHaveBeenCalledWith(room3.roomId, expect.any(Object));
|
||||
});
|
||||
|
||||
it("should take user to view room for unjoined knockable rooms", async () => {
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
const { getByText } = render(getComponent());
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
const button = getByText("Knock room")!
|
||||
.closest("li")!
|
||||
.querySelector(".mx_AccessibleButton_kind_primary_outline")!;
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(defaultProps.showRoom).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
hierarchyKnockRoom1.room_id,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
74
test/unit-tests/components/structures/TabbedView-test.tsx
Normal file
74
test/unit-tests/components/structures/TabbedView-test.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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 TabbedView, { Tab, TabLocation } from "../../../src/components/structures/TabbedView";
|
||||
import { NonEmptyArray } from "../../../src/@types/common";
|
||||
import { _t } from "../../../src/languageHandler";
|
||||
|
||||
describe("<TabbedView />", () => {
|
||||
const generalTab = new Tab("GENERAL", "common|general", "general", <div>general</div>);
|
||||
const labsTab = new Tab("LABS", "common|labs", "labs", <div>labs</div>);
|
||||
const securityTab = new Tab("SECURITY", "common|security", "security", <div>security</div>);
|
||||
const defaultProps = {
|
||||
tabLocation: TabLocation.LEFT,
|
||||
tabs: [generalTab, labsTab, securityTab] as NonEmptyArray<Tab<any>>,
|
||||
onChange: () => {},
|
||||
};
|
||||
const getComponent = (
|
||||
props: {
|
||||
activeTabId: "GENERAL" | "LABS" | "SECURITY";
|
||||
onChange?: () => any;
|
||||
tabs?: NonEmptyArray<Tab<any>>;
|
||||
} = {
|
||||
activeTabId: "GENERAL",
|
||||
},
|
||||
): React.ReactElement => <TabbedView {...defaultProps} {...props} />;
|
||||
|
||||
const getTabTestId = (tab: Tab<string>): string => `settings-tab-${tab.id}`;
|
||||
const getActiveTab = (container: HTMLElement): Element | undefined =>
|
||||
container.getElementsByClassName("mx_TabbedView_tabLabel_active")[0];
|
||||
const getActiveTabBody = (container: HTMLElement): Element | undefined =>
|
||||
container.getElementsByClassName("mx_TabbedView_tabPanel")[0];
|
||||
|
||||
it("renders tabs", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders activeTabId tab as active when valid", () => {
|
||||
const { container } = render(getComponent({ activeTabId: securityTab.id }));
|
||||
expect(getActiveTab(container)?.textContent).toEqual(_t(securityTab.label));
|
||||
expect(getActiveTabBody(container)?.textContent).toEqual("security");
|
||||
});
|
||||
|
||||
it("calls onchange on on tab click", () => {
|
||||
const onChange = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ activeTabId: "GENERAL", onChange }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId(getTabTestId(securityTab)));
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(securityTab.id);
|
||||
});
|
||||
|
||||
it("keeps same tab active when order of tabs changes", () => {
|
||||
// start with middle tab active
|
||||
const { container, rerender } = render(getComponent({ activeTabId: labsTab.id }));
|
||||
|
||||
expect(getActiveTab(container)?.textContent).toEqual(_t(labsTab.label));
|
||||
|
||||
rerender(getComponent({ tabs: [labsTab, generalTab, securityTab], activeTabId: labsTab.id }));
|
||||
|
||||
// labs tab still active
|
||||
expect(getActiveTab(container)?.textContent).toEqual(_t(labsTab.label));
|
||||
});
|
||||
});
|
288
test/unit-tests/components/structures/ThreadPanel-test.tsx
Normal file
288
test/unit-tests/components/structures/ThreadPanel-test.tsx
Normal file
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021-2023 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, fireEvent, waitFor, getByRole } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
Room,
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../src/components/structures/ThreadPanel";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../src/contexts/RoomContext";
|
||||
import { _t } from "../../../src/languageHandler";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
import { IRoomState } from "../../../src/components/structures/RoomView";
|
||||
|
||||
jest.mock("../../../src/utils/Feedback");
|
||||
|
||||
describe("ThreadPanel", () => {
|
||||
describe("Header", () => {
|
||||
it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => {
|
||||
const { asFragment } = render(
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => {
|
||||
const { asFragment } = render(
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.My} setFilterOption={() => undefined} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => {
|
||||
const { container } = render(
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||
);
|
||||
const found = container.querySelector(".mx_ThreadPanel_dropdown");
|
||||
expect(found).toBeTruthy();
|
||||
expect(screen.queryByRole("menu")).toBeFalsy();
|
||||
fireEvent.click(found!);
|
||||
expect(screen.queryByRole("menu")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => {
|
||||
const { container } = render(
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||
);
|
||||
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
|
||||
const found = screen.queryAllByRole("menuitemradio");
|
||||
expect(found).toHaveLength(2);
|
||||
const foundButton = screen.queryByRole("menuitemradio", { checked: true });
|
||||
expect(foundButton?.textContent).toEqual(
|
||||
`${_t("threads|all_threads")}${_t("threads|all_threads_description")}`,
|
||||
);
|
||||
expect(foundButton).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("sends an unthreaded read receipt when the Mark All Threads Read button is clicked", async () => {
|
||||
const mockClient = createTestClient();
|
||||
const mockEvent = {} as MatrixEvent;
|
||||
const mockRoom = mkRoom(mockClient, "!roomId:example.org");
|
||||
mockRoom.getLastLiveEvent.mockReturnValue(mockEvent);
|
||||
const roomContextObject = {
|
||||
room: mockRoom,
|
||||
} as unknown as IRoomState;
|
||||
const { container } = render(
|
||||
<RoomContext.Provider value={roomContextObject}>
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
|
||||
</MatrixClientContext.Provider>
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
||||
await waitFor(() =>
|
||||
expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(mockEvent, expect.anything(), true),
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't send a receipt if no room is in context", async () => {
|
||||
const mockClient = createTestClient();
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
||||
await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filtering", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
const SENDER = "@alice:example.org";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const TestThreadPanel = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider
|
||||
value={getRoomContext(room, {
|
||||
canSendMessages: true,
|
||||
})}
|
||||
>
|
||||
<ThreadPanel
|
||||
roomId={ROOM_ID}
|
||||
onClose={jest.fn()}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockPlatformPeg();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(room, "fetchRoomThreads").mockReturnValue(Promise.resolve());
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
await room.createThreadsTimelineSets();
|
||||
const [allThreads, myThreads] = room.threadsTimelineSets;
|
||||
jest.spyOn(room, "createThreadsTimelineSets").mockReturnValue(Promise.resolve([allThreads!, myThreads!]));
|
||||
});
|
||||
|
||||
function toggleThreadFilter(container: HTMLElement, newFilter: ThreadFilterType) {
|
||||
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
|
||||
const found = screen.queryAllByRole("menuitemradio");
|
||||
expect(found).toHaveLength(2);
|
||||
|
||||
const allThreadsContent = `${_t("threads|all_threads")}${_t("threads|all_threads_description")}`;
|
||||
const myThreadsContent = `${_t("threads|my_threads")}${_t("threads|my_threads_description")}`;
|
||||
|
||||
const allThreadsOption = found.find((it) => it.textContent === allThreadsContent);
|
||||
const myThreadsOption = found.find((it) => it.textContent === myThreadsContent);
|
||||
expect(allThreadsOption).toBeTruthy();
|
||||
expect(myThreadsOption).toBeTruthy();
|
||||
|
||||
const toSelect = newFilter === ThreadFilterType.My ? myThreadsOption : allThreadsOption;
|
||||
fireEvent.click(toSelect!);
|
||||
}
|
||||
|
||||
type EventData = { sender: string | null; content: string | null };
|
||||
|
||||
function findEvents(container: HTMLElement): EventData[] {
|
||||
return Array.from(container.querySelectorAll(".mx_EventTile")).map((el) => {
|
||||
const sender = el.querySelector(".mx_DisambiguatedProfile_displayName")?.textContent ?? null;
|
||||
const content = el.querySelector(".mx_EventTile_body")?.textContent ?? null;
|
||||
return { sender, content };
|
||||
});
|
||||
}
|
||||
|
||||
function toEventData(event: MatrixEvent): EventData {
|
||||
return { sender: event.event.sender ?? null, content: event.event.content?.body ?? null };
|
||||
}
|
||||
|
||||
it("correctly filters Thread List with multiple threads", async () => {
|
||||
const otherThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const mixedThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [SENDER, mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const ownThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const threadRoots = [otherThread.rootEvent, mixedThread.rootEvent, ownThread.rootEvent];
|
||||
jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => {
|
||||
const event = threadRoots.find((it) => it.getId() === eventId)?.event;
|
||||
return event ? Promise.resolve(event) : Promise.reject();
|
||||
});
|
||||
const [allThreads, myThreads] = room.threadsTimelineSets;
|
||||
allThreads!.addLiveEvent(otherThread.rootEvent);
|
||||
allThreads!.addLiveEvent(mixedThread.rootEvent);
|
||||
allThreads!.addLiveEvent(ownThread.rootEvent);
|
||||
myThreads!.addLiveEvent(mixedThread.rootEvent);
|
||||
myThreads!.addLiveEvent(ownThread.rootEvent);
|
||||
|
||||
let events: EventData[] = [];
|
||||
const renderResult = render(<TestThreadPanel />);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(3);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(2);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(ownThread.rootEvent));
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(3);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
|
||||
});
|
||||
|
||||
it("correctly filters Thread List with a single, unparticipated thread", async () => {
|
||||
const otherThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const threadRoots = [otherThread.rootEvent];
|
||||
jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => {
|
||||
const event = threadRoots.find((it) => it.getId() === eventId)?.event;
|
||||
return event ? Promise.resolve(event) : Promise.reject();
|
||||
});
|
||||
const [allThreads] = room.threadsTimelineSets;
|
||||
allThreads!.addLiveEvent(otherThread.rootEvent);
|
||||
|
||||
let events: EventData[] = [];
|
||||
const renderResult = render(<TestThreadPanel />);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(1);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(0);
|
||||
});
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(1);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
});
|
||||
});
|
||||
});
|
212
test/unit-tests/components/structures/ThreadView-test.tsx
Normal file
212
test/unit-tests/components/structures/ThreadView-test.tsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 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 { act, getByTestId, render, RenderResult, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import {
|
||||
MsgType,
|
||||
RelationType,
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
MatrixClient,
|
||||
PendingEventOrdering,
|
||||
THREAD_RELATION_TYPE,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import ThreadView from "../../../src/components/structures/ThreadView";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../src/contexts/RoomContext";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { mockPlatformPeg } from "../../test-utils/platform";
|
||||
import { getRoomContext } from "../../test-utils/room";
|
||||
import { mkMessage, stubClient } from "../../test-utils/test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
|
||||
describe("ThreadView", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
const SENDER = "@alice:example.org";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let rootEvent: MatrixEvent;
|
||||
|
||||
let changeEvent: (event: MatrixEvent) => void;
|
||||
|
||||
function TestThreadView({ initialEvent }: { initialEvent?: MatrixEvent }) {
|
||||
const [event, setEvent] = useState(rootEvent);
|
||||
changeEvent = setEvent;
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider
|
||||
value={getRoomContext(room, {
|
||||
canSendMessages: true,
|
||||
})}
|
||||
>
|
||||
<ThreadView
|
||||
room={room}
|
||||
onClose={jest.fn()}
|
||||
mxEvent={event}
|
||||
initialEvent={initialEvent}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
,
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
async function getComponent(initialEvent?: MatrixEvent): Promise<RenderResult> {
|
||||
const renderResult = render(<TestThreadView initialEvent={initialEvent} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(renderResult.container, "spinner")).toThrow();
|
||||
});
|
||||
|
||||
return renderResult;
|
||||
}
|
||||
|
||||
async function sendMessage(container: HTMLElement, text: string): Promise<void> {
|
||||
const composer = getByTestId(container, "basicmessagecomposer");
|
||||
await userEvent.click(composer);
|
||||
await userEvent.keyboard(text);
|
||||
const sendMessageBtn = getByTestId(container, "sendmessagebtn");
|
||||
await userEvent.click(sendMessageBtn);
|
||||
}
|
||||
|
||||
function expectedMessageBody(rootEvent: MatrixEvent, message: string) {
|
||||
return {
|
||||
"body": message,
|
||||
"m.relates_to": {
|
||||
"event_id": rootEvent.getId(),
|
||||
"is_falling_back": true,
|
||||
"m.in_reply_to": {
|
||||
event_id: rootEvent
|
||||
.getThread()!
|
||||
.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name);
|
||||
})!
|
||||
.getId(),
|
||||
},
|
||||
"rel_type": RelationType.Thread,
|
||||
},
|
||||
"msgtype": MsgType.Text,
|
||||
"m.mentions": {},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockPlatformPeg();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true);
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const res = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
rootEvent = res.rootEvent;
|
||||
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(SENDER);
|
||||
});
|
||||
|
||||
it("does not include pending root event in the timeline twice", async () => {
|
||||
rootEvent = mkMessage({
|
||||
user: mockClient.getUserId()!,
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "root event message " + Math.random(),
|
||||
});
|
||||
|
||||
rootEvent.status = EventStatus.SENDING;
|
||||
rootEvent.setTxnId("1234");
|
||||
room.addPendingEvent(rootEvent, "1234");
|
||||
room.updatePendingEvent(rootEvent, EventStatus.SENT, rootEvent.getId());
|
||||
|
||||
const { container } = await getComponent();
|
||||
const tiles = container.getElementsByClassName("mx_EventTile");
|
||||
expect(tiles.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("sends a message with the correct fallback", async () => {
|
||||
const { container } = await getComponent();
|
||||
|
||||
await sendMessage(container, "Hello world!");
|
||||
|
||||
expect(mockClient.sendMessage).toHaveBeenCalledWith(
|
||||
ROOM_ID,
|
||||
rootEvent.getId(),
|
||||
expectedMessageBody(rootEvent, "Hello world!"),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a thread message with the correct fallback", async () => {
|
||||
const { container } = await getComponent();
|
||||
|
||||
const { rootEvent: rootEvent2 } = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
act(() => {
|
||||
changeEvent(rootEvent2);
|
||||
});
|
||||
|
||||
await sendMessage(container, "yolo");
|
||||
|
||||
expect(mockClient.sendMessage).toHaveBeenCalledWith(
|
||||
ROOM_ID,
|
||||
rootEvent2.getId(),
|
||||
expectedMessageBody(rootEvent2, "yolo"),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the correct thread in the room view store", async () => {
|
||||
// expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull();
|
||||
const { unmount } = await getComponent();
|
||||
waitFor(() => {
|
||||
expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId());
|
||||
});
|
||||
|
||||
unmount();
|
||||
await waitFor(() => expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull());
|
||||
});
|
||||
|
||||
it("clears highlight message in the room view store", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(room.roomId);
|
||||
const mock = jest.spyOn(dispatcher, "dispatch");
|
||||
const { unmount } = await getComponent(rootEvent);
|
||||
mock.mockClear();
|
||||
unmount();
|
||||
expect(mock).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
});
|
1005
test/unit-tests/components/structures/TimelinePanel-test.tsx
Normal file
1005
test/unit-tests/components/structures/TimelinePanel-test.tsx
Normal file
File diff suppressed because it is too large
Load diff
44
test/unit-tests/components/structures/UploadBar-test.tsx
Normal file
44
test/unit-tests/components/structures/UploadBar-test.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { jest } from "@jest/globals";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { stubClient } from "../../test-utils";
|
||||
import ContentMessages from "../../../src/ContentMessages";
|
||||
import { RoomUpload } from "../../../src/models/RoomUpload";
|
||||
import UploadBar from "../../../src/components/structures/UploadBar";
|
||||
|
||||
describe("UploadBar", () => {
|
||||
const client = stubClient();
|
||||
const room = new Room("!room;server", client, client.getSafeUserId());
|
||||
|
||||
it("should pluralise 5 files correctly", () => {
|
||||
jest.spyOn(ContentMessages.sharedInstance(), "getCurrentUploads").mockReturnValue([
|
||||
new RoomUpload(room.roomId, "file1.jpg", undefined, 420),
|
||||
new RoomUpload(room.roomId, "file2"),
|
||||
new RoomUpload(room.roomId, "file3"),
|
||||
new RoomUpload(room.roomId, "file4"),
|
||||
new RoomUpload(room.roomId, "file5"),
|
||||
]);
|
||||
|
||||
const { getByText } = render(<UploadBar room={room} />);
|
||||
expect(getByText("Uploading file1.jpg and 4 others (420 B)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a single upload correctly", () => {
|
||||
jest.spyOn(ContentMessages.sharedInstance(), "getCurrentUploads").mockReturnValue([
|
||||
new RoomUpload(room.roomId, "file1.zip", undefined, 420000000),
|
||||
]);
|
||||
|
||||
const { getByText } = render(<UploadBar room={room} />);
|
||||
expect(getByText("Uploading file1.zip (400.54 MB)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
224
test/unit-tests/components/structures/UserMenu-test.tsx
Normal file
224
test/unit-tests/components/structures/UserMenu-test.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
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, RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import UnwrappedUserMenu from "../../../src/components/structures/UserMenu";
|
||||
import { stubClient, wrapInSdkContext } from "../../test-utils";
|
||||
import {
|
||||
VoiceBroadcastInfoState,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingsStore,
|
||||
} from "../../../src/voice-broadcast";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
|
||||
import { TestSdkContext } from "../../TestSdkContext";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import LogoutDialog from "../../../src/components/views/dialogs/LogoutDialog";
|
||||
import Modal from "../../../src/Modal";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { Features } from "../../../src/settings/Settings";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { UserTab } from "../../../src/components/views/dialogs/UserTab";
|
||||
|
||||
describe("<UserMenu>", () => {
|
||||
let client: MatrixClient;
|
||||
let renderResult: RenderResult;
|
||||
let sdkContext: TestSdkContext;
|
||||
|
||||
beforeEach(() => {
|
||||
sdkContext = new TestSdkContext();
|
||||
});
|
||||
|
||||
describe("<UserMenu> when video broadcast", () => {
|
||||
let voiceBroadcastInfoEvent: MatrixEvent;
|
||||
let voiceBroadcastRecording: VoiceBroadcastRecording;
|
||||
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
"!room:example.com",
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId() || "",
|
||||
client.getDeviceId() || "",
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
|
||||
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
|
||||
|
||||
voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
|
||||
});
|
||||
|
||||
describe("when rendered", () => {
|
||||
beforeEach(() => {
|
||||
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
|
||||
renderResult = render(<UserMenu isPanelCollapsed={true} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and a live voice broadcast starts", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the live voice broadcast avatar addon", () => {
|
||||
expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and the broadcast ends", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
voiceBroadcastRecordingsStore.clearCurrent();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render the live voice broadcast avatar addon", () => {
|
||||
expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("<UserMenu> logout", () => {
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("should logout directly if no crypto", async () => {
|
||||
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
|
||||
renderResult = render(<UserMenu isPanelCollapsed={true} />);
|
||||
|
||||
mocked(client.getRooms).mockReturnValue([
|
||||
{
|
||||
roomId: "!room0",
|
||||
} as unknown as Room,
|
||||
{
|
||||
roomId: "!room1",
|
||||
} as unknown as Room,
|
||||
]);
|
||||
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
|
||||
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
screen.getByRole("button", { name: /User menu/i }).click();
|
||||
(await screen.findByRole("menuitem", { name: /Sign out/i })).click();
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledWith({ action: "logout" });
|
||||
});
|
||||
});
|
||||
|
||||
it("should logout directly if no encrypted rooms", async () => {
|
||||
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
|
||||
renderResult = render(<UserMenu isPanelCollapsed={true} />);
|
||||
|
||||
mocked(client.getRooms).mockReturnValue([
|
||||
{
|
||||
roomId: "!room0",
|
||||
} as unknown as Room,
|
||||
{
|
||||
roomId: "!room1",
|
||||
} as unknown as Room,
|
||||
]);
|
||||
const crypto = client.getCrypto()!;
|
||||
|
||||
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
screen.getByRole("button", { name: /User menu/i }).click();
|
||||
(await screen.findByRole("menuitem", { name: /Sign out/i })).click();
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledWith({ action: "logout" });
|
||||
});
|
||||
});
|
||||
|
||||
it("should show dialog if some encrypted rooms", async () => {
|
||||
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
|
||||
renderResult = render(<UserMenu isPanelCollapsed={true} />);
|
||||
|
||||
mocked(client.getRooms).mockReturnValue([
|
||||
{
|
||||
roomId: "!room0",
|
||||
} as unknown as Room,
|
||||
{
|
||||
roomId: "!room1",
|
||||
} as unknown as Room,
|
||||
]);
|
||||
const crypto = client.getCrypto()!;
|
||||
|
||||
jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockImplementation(async (roomId: string) => {
|
||||
return roomId === "!room0";
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
screen.getByRole("button", { name: /User menu/i }).click();
|
||||
(await screen.findByRole("menuitem", { name: /Sign out/i })).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledWith(LogoutDialog);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render 'Link new device' button in OIDC native mode", async () => {
|
||||
sdkContext.client = stubClient();
|
||||
mocked(sdkContext.client.getAuthIssuer).mockResolvedValue({ issuer: "https://issuer/" });
|
||||
const openIdMetadata = mockOpenIdConfiguration("https://issuer/");
|
||||
openIdMetadata.grant_types_supported.push(DEVICE_CODE_SCOPE);
|
||||
fetchMock.get("https://issuer/.well-known/openid-configuration", openIdMetadata);
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
mocked(sdkContext.client.getVersions).mockResolvedValue({
|
||||
versions: [],
|
||||
unstable_features: {
|
||||
"org.matrix.msc4108": true,
|
||||
},
|
||||
});
|
||||
mocked(sdkContext.client.waitForClientWellKnown).mockResolvedValue({});
|
||||
mocked(sdkContext.client.getCrypto).mockReturnValue({
|
||||
isCrossSigningReady: jest.fn().mockResolvedValue(true),
|
||||
exportSecretsBundle: jest.fn().mockResolvedValue({}),
|
||||
} as unknown as CryptoApi);
|
||||
await SettingsStore.setValue(Features.OidcNativeFlow, null, SettingLevel.DEVICE, true);
|
||||
const spy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
|
||||
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
|
||||
render(<UserMenu isPanelCollapsed={true} />);
|
||||
|
||||
screen.getByRole("button", { name: /User menu/i }).click();
|
||||
await expect(screen.findByText("Link new device")).resolves.toBeInTheDocument();
|
||||
|
||||
// Assert the QR code is shown directly
|
||||
screen.getByRole("menuitem", { name: "Link new device" }).click();
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.SessionManager,
|
||||
props: { showMsc4108QrCode: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
63
test/unit-tests/components/structures/ViewSource-test.tsx
Normal file
63
test/unit-tests/components/structures/ViewSource-test.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { render } from "jest-matrix-react";
|
||||
import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import ViewSource from "../../../src/components/structures/ViewSource";
|
||||
import { mkEvent, stubClient, mkMessage } from "../../test-utils/test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
|
||||
describe("ViewSource", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
const SENDER = "@alice:example.org";
|
||||
|
||||
let redactedMessageEvent: MatrixEvent;
|
||||
|
||||
const redactionEvent = mkEvent({
|
||||
user: SENDER,
|
||||
event: true,
|
||||
type: EventType.RoomRedaction,
|
||||
content: {},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
redactedMessageEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
room_id: ROOM_ID,
|
||||
sender: SENDER,
|
||||
content: {},
|
||||
state_key: undefined,
|
||||
});
|
||||
redactedMessageEvent.makeRedacted(redactionEvent, new Room(ROOM_ID, stubClient(), SENDER));
|
||||
});
|
||||
|
||||
beforeEach(stubClient);
|
||||
|
||||
// See https://github.com/vector-im/element-web/issues/24165
|
||||
it("doesn't error when viewing redacted encrypted messages", () => {
|
||||
// Sanity checks
|
||||
expect(redactedMessageEvent.isEncrypted()).toBeTruthy();
|
||||
// @ts-ignore clearEvent is private, but it's being used directly <ViewSource />
|
||||
expect(redactedMessageEvent.clearEvent).toBe(undefined);
|
||||
|
||||
expect(() => render(<ViewSource mxEvent={redactedMessageEvent} onFinished={() => {}} />)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should show edit button if we are the sender and can post an edit", () => {
|
||||
const event = mkMessage({
|
||||
msg: "Test",
|
||||
user: MatrixClientPeg.get()!.getSafeUserId(),
|
||||
room: ROOM_ID,
|
||||
event: true,
|
||||
});
|
||||
const { getByRole } = render(<ViewSource mxEvent={event} onFinished={() => {}} />);
|
||||
expect(getByRole("button", { name: "Edit" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePanel renders empty state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard mx_FilePanel"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_BaseCard_header_title_heading"
|
||||
role="heading"
|
||||
>
|
||||
Files
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomView_messagePanel mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_empty"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_EmptyState"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="32px"
|
||||
viewBox="0 0 24 24"
|
||||
width="32px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 20V4c0-.55.196-1.02.588-1.413A1.926 1.926 0 0 1 6 2h7.175a1.975 1.975 0 0 1 1.4.575l4.85 4.85a1.975 1.975 0 0 1 .575 1.4V20c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 22H6Zm7-14V4H6v16h12V9h-4a.968.968 0 0 1-.713-.287A.967.967 0 0 1 13 8Z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
|
||||
>
|
||||
No files visible in this room
|
||||
</p>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||
>
|
||||
Attach files from chat or just drag and drop them anywhere in a room.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,61 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MainSplit/> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MainSplit"
|
||||
>
|
||||
<div>
|
||||
Child
|
||||
<span>
|
||||
Foo
|
||||
</span>
|
||||
Bar
|
||||
</div>
|
||||
<div
|
||||
class="mx_RightPanel_ResizeWrapper"
|
||||
style="position: relative; user-select: auto; width: 320px; height: 100%; max-width: 50%; min-width: 264px; box-sizing: border-box; flex-shrink: 0;"
|
||||
>
|
||||
<div>
|
||||
Right panel
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_ResizeHandle--horizontal"
|
||||
style="position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; left: -5px; cursor: col-resize;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<MainSplit/> respects defaultSize prop 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MainSplit"
|
||||
>
|
||||
<div>
|
||||
Child
|
||||
<span>
|
||||
Foo
|
||||
</span>
|
||||
Bar
|
||||
</div>
|
||||
<div
|
||||
class="mx_RightPanel_ResizeWrapper"
|
||||
style="position: relative; user-select: auto; width: 500px; height: 100%; max-width: 50%; min-width: 264px; box-sizing: border-box; flex-shrink: 0;"
|
||||
>
|
||||
<div>
|
||||
Right panel
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_ResizeHandle--horizontal"
|
||||
style="position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; left: -5px; cursor: col-resize;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,430 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens after a session is restored 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens during crypto init 1`] = `
|
||||
<body
|
||||
style="--emoji-font-family: Twemoji;"
|
||||
>
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens while we are checking the sync store 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens while we were waiting for the lock ourselves 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ConfirmSessionLockTheftView"
|
||||
>
|
||||
<div
|
||||
class="mx_ConfirmSessionLockTheftView_body"
|
||||
>
|
||||
<p>
|
||||
Test is open in another window. Click "Continue" to use Test here and disconnect the other window.
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 3`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
>
|
||||
<div
|
||||
class="mx_Welcome"
|
||||
data-testid="mx_welcome_screen"
|
||||
>
|
||||
<div
|
||||
class="mx_WelcomePage mx_WelcomePage_loggedIn"
|
||||
>
|
||||
<div
|
||||
class="mx_WelcomePage_body"
|
||||
>
|
||||
<h1>
|
||||
Hello
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> should render spinner while app is loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logout page 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthHeader"
|
||||
>
|
||||
<aside
|
||||
class="mx_AuthHeaderLogo"
|
||||
>
|
||||
Matrix
|
||||
</aside>
|
||||
<div
|
||||
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main
|
||||
class="mx_AuthBody"
|
||||
>
|
||||
<h1>
|
||||
You're signed out
|
||||
</h1>
|
||||
<h2>
|
||||
Sign in
|
||||
</h2>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Enter your password to sign in and regain access to your account.
|
||||
</p>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
Sign in
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Forgotten your password?
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<h2>
|
||||
Clear personal data
|
||||
</h2>
|
||||
<p>
|
||||
Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Clear all data
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = `
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Leave room
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to leave the room '!room:server.org'?
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Leave
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a space should launch a confirmation modal 1`] = `
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Leave space
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to leave the space '!spaceRoom:server.org'?
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Leave
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,141 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MessagePanel should handle large numbers of hidden events quickly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
style="height: 400px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MessagePanel should handle lots of membership events quickly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
style="height: 400px;"
|
||||
>
|
||||
<li
|
||||
data-scroll-tokens="__scroll_tokens__"
|
||||
data-testid="__testid__"
|
||||
>
|
||||
<div
|
||||
aria-label="Thu, Jan 1, 1970"
|
||||
class="mx_TimelineSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Thu, Jan 1, 1970
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<div
|
||||
class="mx_EventTileBubble mx_HistoryTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTileBubble_title"
|
||||
>
|
||||
You can't see earlier messages
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
class="mx_GenericEventListSummary"
|
||||
data-expanded="false"
|
||||
data-layout="group"
|
||||
data-scroll-tokens="__scroll_tokens__"
|
||||
data-testid="__testid__"
|
||||
>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
class="mx_AccessibleButton mx_GenericEventListSummary_toggle mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Expand
|
||||
</div>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_info"
|
||||
>
|
||||
<span
|
||||
class="mx_GenericEventListSummary_avatars"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 14px;"
|
||||
title="@user:id"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_TextualEvent mx_GenericEventListSummary_summary"
|
||||
>
|
||||
<span>
|
||||
@user:id made no changes 100 times
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MessagePanel should handle lots of room creation events quickly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
style="height: 400px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,56 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and 2 should render both contents 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
style="transform: translateX(680px) translateY(478px);"
|
||||
>
|
||||
<div>
|
||||
content 1
|
||||
</div>
|
||||
<div>
|
||||
content 2
|
||||
<br />
|
||||
content 2.2
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and rendering PiP content 2 should update the PiP content 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
style="transform: translateX(680px) translateY(478px);"
|
||||
>
|
||||
<div>
|
||||
content 2
|
||||
<br />
|
||||
content 2.2
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and rerendering PiP content 1 should not change the PiP content: pip-content-1 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
style="transform: translateX(680px) translateY(478px);"
|
||||
>
|
||||
<div>
|
||||
content 1
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 should render the PiP content: pip-content-1 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
style="transform: translateX(680px) translateY(478px);"
|
||||
>
|
||||
<div>
|
||||
content 1
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,126 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomStatusBar <RoomStatusBar /> unsent messages should render warning when messages are unsent due to consent 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_2char cpd-theme-light"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentTitle"
|
||||
>
|
||||
<span>
|
||||
You can't send any messages until you review and agree to
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
our terms and conditions
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentDescription"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_RoomStatusBar_unsentCancelAllBtn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Delete all
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Retry all
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomStatusBar <RoomStatusBar /> unsent messages should render warning when messages are unsent due to resource limit 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_2char cpd-theme-light"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentTitle"
|
||||
>
|
||||
Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentDescription"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_RoomStatusBar_unsentCancelAllBtn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Delete all
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Retry all
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,426 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SpaceHierarchy <SpaceHierarchy /> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SearchBox mx_textinput"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="mx_textinput_icon mx_textinput_search mx_SpaceHierarchy_search mx_textinput_icon mx_textinput_search"
|
||||
data-testid="searchbox-input"
|
||||
placeholder="Search names and descriptions"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SearchBox_closeButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_listHeader"
|
||||
>
|
||||
<h4
|
||||
class="mx_SpaceHierarchy_listHeader_header"
|
||||
>
|
||||
Rooms and spaces
|
||||
</h4>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_listHeader_buttons"
|
||||
>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
aria-label="Remove"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
aria-label="Mark as not suggested"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Mark as not suggested
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
aria-label="Space"
|
||||
class="mx_SpaceHierarchy_list"
|
||||
role="tree"
|
||||
>
|
||||
<li
|
||||
class="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SpaceHierarchy_roomTile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_item"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
U
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_name"
|
||||
>
|
||||
Unnamed Room
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_info"
|
||||
>
|
||||
2 members
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Join
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="checkbox_vY7Q4uEh9K"
|
||||
tabindex="0"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_vY7Q4uEh9K"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SpaceHierarchy_roomTile"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_item"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_avatar"
|
||||
>
|
||||
<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: 20px;"
|
||||
>
|
||||
U
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_name"
|
||||
>
|
||||
Unnamed Room
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_info"
|
||||
>
|
||||
3 members
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Join
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="checkbox_38QgU2Pomx"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_38QgU2Pomx"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SpaceHierarchy_roomTile"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_item"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_avatar"
|
||||
>
|
||||
<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: 20px;"
|
||||
>
|
||||
K
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_name"
|
||||
>
|
||||
Knock room
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_info"
|
||||
>
|
||||
3 members
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
View
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="checkbox_wKpa6hpi3Y"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_wKpa6hpi3Y"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-expanded="true"
|
||||
class="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SpaceHierarchy_roomTile mx_SpaceHierarchy_subspace"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_item"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
N
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_name"
|
||||
>
|
||||
Nested space
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_info"
|
||||
>
|
||||
1 member · 1 room
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Join
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="checkbox_EetmBG4yVC"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_EetmBG4yVC"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_subspace_toggle mx_SpaceHierarchy_subspace_toggle_shown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_subspace_children"
|
||||
role="group"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="mx_SpaceHierarchy_roomTileWrapper"
|
||||
role="treeitem"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SpaceHierarchy_roomTile"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_item"
|
||||
>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
N
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_name"
|
||||
>
|
||||
Nested room
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_roomTile_info"
|
||||
>
|
||||
3 members
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SpaceHierarchy_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
Join
|
||||
</div>
|
||||
<span
|
||||
aria-labelledby="floating-ui-9"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
disabled=""
|
||||
id="checkbox_eEefiPqpMR"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_eEefiPqpMR"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,84 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<TabbedView /> renders tabs 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_TabbedView mx_TabbedView_tabsOnLeft"
|
||||
>
|
||||
<ul
|
||||
aria-orientation="vertical"
|
||||
class="mx_TabbedView_tabLabels"
|
||||
role="tablist"
|
||||
>
|
||||
<li
|
||||
aria-controls="mx_tabpanel_GENERAL"
|
||||
aria-selected="true"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
|
||||
data-testid="settings-tab-GENERAL"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon general"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_GENERAL_label"
|
||||
>
|
||||
General
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
aria-controls="mx_tabpanel_LABS"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-LABS"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon labs"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_LABS_label"
|
||||
>
|
||||
Labs
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
aria-controls="mx_tabpanel_SECURITY"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-SECURITY"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon security"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_SECURITY_label"
|
||||
>
|
||||
Security
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
aria-labelledby="mx_tabpanel_GENERAL_label"
|
||||
class="mx_TabbedView_tabPanel"
|
||||
id="mx_tabpanel_GENERAL"
|
||||
>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_TabbedView_tabPanelContent"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
general
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,87 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ThreadPanel Header expect that All filter for ThreadPanelHeader properly renders Show: All threads 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="mx_ThreadPanel_vertical_separator"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="mx_AccessibleButton mx_ThreadPanel_dropdown"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show: All threads
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly renders Show: My threads 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<button
|
||||
aria-labelledby="floating-ui-6"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="mx_ThreadPanel_vertical_separator"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="mx_AccessibleButton mx_ThreadPanel_dropdown"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show: My threads
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = `
|
||||
<div
|
||||
aria-checked="true"
|
||||
class="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
|
||||
role="menuitemradio"
|
||||
tabindex="0"
|
||||
>
|
||||
<span>
|
||||
All threads
|
||||
</span>
|
||||
<span>
|
||||
Shows all threads from current room
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserMenu> <UserMenu> when video broadcast when rendered should render as expected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserMenu"
|
||||
>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="User menu"
|
||||
class="mx_AccessibleButton mx_UserMenu_contextMenuButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_UserMenu_userAvatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
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 { mocked } from "jest-mock";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import CompleteSecurity from "../../../../src/components/structures/auth/CompleteSecurity";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { Phase, SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
|
||||
class MockSetupEncryptionStore extends EventEmitter {
|
||||
public phase: Phase = Phase.Intro;
|
||||
public lostKeys(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public start: () => void = jest.fn();
|
||||
public stop: () => void = jest.fn();
|
||||
}
|
||||
|
||||
describe("CompleteSecurity", () => {
|
||||
beforeEach(() => {
|
||||
const client = stubClient();
|
||||
const deviceIdToDevice = new Map();
|
||||
deviceIdToDevice.set("DEVICE_ID", {
|
||||
deviceId: "DEVICE_ID",
|
||||
userId: "USER_ID",
|
||||
});
|
||||
const userIdToDevices = new Map();
|
||||
userIdToDevices.set("USER_ID", deviceIdToDevice);
|
||||
mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices);
|
||||
|
||||
const mockSetupEncryptionStore = new MockSetupEncryptionStore();
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(
|
||||
mockSetupEncryptionStore as SetupEncryptionStore,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("Renders with a cancel button by default", () => {
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders with a cancel button if forceVerification false", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "forceVerification") {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders without a cancel button if forceVerification true", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "force_verification") {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Skip verification for now" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,427 @@
|
|||
/*
|
||||
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, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ForgotPassword from "../../../../src/components/structures/auth/ForgotPassword";
|
||||
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
||||
import {
|
||||
clearAllModals,
|
||||
filterConsole,
|
||||
flushPromisesWithFakeTimers,
|
||||
stubClient,
|
||||
waitEnoughCyclesForModal,
|
||||
} from "../../../test-utils";
|
||||
import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||
createClient: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<ForgotPassword>", () => {
|
||||
const testEmail = "user@example.com";
|
||||
const testSid = "sid42";
|
||||
const testPassword = "cRaZyP4ssw0rd!";
|
||||
let client: MatrixClient;
|
||||
let serverConfig: ValidatedServerConfig;
|
||||
let onComplete: () => void;
|
||||
let onLoginClick: () => void;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const typeIntoField = async (label: string, value: string): Promise<void> => {
|
||||
await act(async () => {
|
||||
await userEvent.type(screen.getByLabelText(label), value, { delay: null });
|
||||
// the message is shown after some time
|
||||
jest.advanceTimersByTime(500);
|
||||
});
|
||||
};
|
||||
|
||||
const click = async (element: Element): Promise<void> => {
|
||||
await act(async () => {
|
||||
await userEvent.click(element, { delay: null });
|
||||
});
|
||||
};
|
||||
|
||||
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
||||
it("should close the dialog and show the password input", () => {
|
||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
filterConsole(
|
||||
// not implemented by js-dom https://github.com/jsdom/jsdom/issues/1937
|
||||
"Not implemented: HTMLFormElement.prototype.requestSubmit",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
mocked(createClient).mockReturnValue(client);
|
||||
|
||||
serverConfig = { hsName: "example.com" } as ValidatedServerConfig;
|
||||
|
||||
onComplete = jest.fn();
|
||||
onLoginClick = jest.fn();
|
||||
|
||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// clean up modals
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("when starting a password reset flow", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the email input and mention the homeserver", () => {
|
||||
expect(screen.queryByLabelText("Email address")).toBeInTheDocument();
|
||||
expect(screen.queryByText("example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and updating the server config", () => {
|
||||
beforeEach(() => {
|
||||
serverConfig.hsName = "example2.com";
|
||||
renderResult.rerender(
|
||||
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the new homeserver server name", () => {
|
||||
expect(screen.queryByText("example2.com")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Sign in instead«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Sign in instead"));
|
||||
});
|
||||
|
||||
it("should call onLoginClick()", () => {
|
||||
expect(onLoginClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and entering a non-email value", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", "not en email");
|
||||
});
|
||||
|
||||
it("should show a message about the wrong format", () => {
|
||||
expect(screen.getByText("The email address doesn't appear to be valid.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting an unknown email", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
errcode: "M_THREEPID_NOT_FOUND",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show an email not found message", () => {
|
||||
expect(screen.getByText("This email address was not found")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and a connection error occurs", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
name: "ConnectionError",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show an info about that", () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Cannot reach homeserver: " +
|
||||
"Ensure you have a stable internet connection, or get in touch with the server admin",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the server liveness check fails", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({});
|
||||
mocked(AutoDiscoveryUtils.authComponentStateForError).mockReturnValue({
|
||||
serverErrorIsFatal: true,
|
||||
serverIsAlive: false,
|
||||
serverDeadError: "server down",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show the server error", () => {
|
||||
expect(screen.queryByText("server down")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting an known email", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
||||
sid: testSid,
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should send the mail and show the check email view", () => {
|
||||
expect(client.requestPasswordEmailToken).toHaveBeenCalledWith(
|
||||
testEmail,
|
||||
expect.any(String),
|
||||
1, // second send attempt
|
||||
);
|
||||
expect(screen.getByText("Check your email to continue")).toBeInTheDocument();
|
||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and clicking »Re-enter email address«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Re-enter email address"));
|
||||
});
|
||||
|
||||
it("go back to the email input", () => {
|
||||
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Resend«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Resend"));
|
||||
// the message is shown after some time
|
||||
jest.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
it("should should resend the mail and show the tooltip", () => {
|
||||
expect(client.requestPasswordEmailToken).toHaveBeenCalledWith(
|
||||
testEmail,
|
||||
expect.any(String),
|
||||
2, // second send attempt
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("tooltip", { name: "Verification link email resent!" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Next«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Next"));
|
||||
});
|
||||
|
||||
it("should show the password input view", () => {
|
||||
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and entering different passwords", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("New Password", testPassword);
|
||||
await typeIntoField("Confirm new password", testPassword + "asd");
|
||||
});
|
||||
|
||||
it("should show an info about that", () => {
|
||||
expect(screen.getByText("New passwords must match each other.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and entering a new password", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 });
|
||||
await typeIntoField("New Password", testPassword);
|
||||
await typeIntoField("Confirm new password", testPassword);
|
||||
});
|
||||
|
||||
describe("and submitting it running into rate limiting", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockRejectedValue({
|
||||
message: "rate limit reached",
|
||||
httpStatus: 429,
|
||||
data: {
|
||||
retry_after_ms: (13 * 60 + 37) * 1000,
|
||||
},
|
||||
});
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should show the rate limit error message", () => {
|
||||
expect(
|
||||
screen.getByText("Too many attempts in a short time. Retry after 13:37."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and confirm the email link and submitting the new password", () => {
|
||||
beforeEach(async () => {
|
||||
// fake link confirmed by resolving client.setPassword instead of raising an error
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should send the new password (once)", () => {
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
false,
|
||||
);
|
||||
|
||||
// be sure that the next attempt to set the password would have been sent
|
||||
jest.advanceTimersByTime(3000);
|
||||
// it should not retry to set the password
|
||||
expect(client.setPassword).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting it", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Reset password"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should send the new password and show the click validation link dialog", () => {
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
false,
|
||||
);
|
||||
expect(screen.getByText("Verify your email to continue")).toBeInTheDocument();
|
||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and dismissing the dialog by clicking the background", () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByTestId("dialog-background"), { delay: null });
|
||||
});
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||
});
|
||||
|
||||
describe("and dismissing the dialog", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByLabelText("Close dialog"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||
});
|
||||
|
||||
describe("and clicking »Re-enter email address«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Re-enter email address"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should close the dialog and go back to the email input", () => {
|
||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and validating the link from the mail", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
// be sure the next set password attempt was sent
|
||||
jest.advanceTimersByTime(3000);
|
||||
// quad flush promises for the modal to disappear
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
it("should display the confirm reset view and now show the dialog", () => {
|
||||
expect(screen.queryByText("Your password has been reset.")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Sign out of all devices« and »Reset password«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Sign out of all devices"));
|
||||
await click(screen.getByText("Reset password"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the sign out warning dialog", async () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// confirm dialog
|
||||
await click(screen.getByText("Continue"));
|
||||
|
||||
// expect setPassword with logoutDevices = true
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
473
test/unit-tests/components/structures/auth/Login-test.tsx
Normal file
473
test/unit-tests/components/structures/auth/Login-test.tsx
Normal file
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
Copyright 2019-2024 New Vector Ltd.
|
||||
|
||||
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, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand, OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import * as Matrix from "matrix-js-sdk/src/matrix";
|
||||
import { OidcError } from "matrix-js-sdk/src/oidc/error";
|
||||
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils";
|
||||
import Login from "../../../../src/components/structures/auth/Login";
|
||||
import BasePlatform from "../../../../src/BasePlatform";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { Features } from "../../../../src/settings/Settings";
|
||||
import * as registerClientUtils from "../../../../src/utils/oidc/registerClient";
|
||||
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
const oidcStaticClientsConfig = {
|
||||
"https://staticallyregisteredissuer.org/": {
|
||||
client_id: "static-clientId-123",
|
||||
},
|
||||
};
|
||||
|
||||
describe("Login", function () {
|
||||
let platform: MockedObject<BasePlatform>;
|
||||
|
||||
const mockClient = mocked({
|
||||
login: jest.fn().mockResolvedValue({}),
|
||||
loginFlows: jest.fn(),
|
||||
} as unknown as Matrix.MatrixClient);
|
||||
|
||||
beforeEach(function () {
|
||||
SdkConfig.put({
|
||||
brand: "test-brand",
|
||||
disable_custom_urls: true,
|
||||
oidc_static_clients: oidcStaticClientsConfig,
|
||||
});
|
||||
mockClient.login.mockClear().mockResolvedValue({
|
||||
access_token: "TOKEN",
|
||||
device_id: "IAMADEVICE",
|
||||
user_id: "@user:server",
|
||||
});
|
||||
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||
jest.spyOn(Matrix, "createClient").mockImplementation((opts) => {
|
||||
mockClient.idBaseUrl = opts.idBaseUrl;
|
||||
mockClient.baseUrl = opts.baseUrl;
|
||||
return mockClient;
|
||||
});
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.resetHistory();
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
platform = mockPlatformPeg({
|
||||
startSingleSignOn: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.restore();
|
||||
SdkConfig.reset(); // we touch the config, so clean up
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
function getRawComponent(
|
||||
hsUrl = "https://matrix.org",
|
||||
isUrl = "https://vector.im",
|
||||
delegatedAuthentication?: OidcClientConfig,
|
||||
) {
|
||||
return (
|
||||
<Login
|
||||
serverConfig={mkServerConfig(hsUrl, isUrl, delegatedAuthentication)}
|
||||
onLoggedIn={() => {}}
|
||||
onRegisterClick={() => {}}
|
||||
onServerConfigChange={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: OidcClientConfig) {
|
||||
return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication));
|
||||
}
|
||||
|
||||
it("should show form with change server link", async () => {
|
||||
SdkConfig.put({
|
||||
brand: "test-brand",
|
||||
disable_custom_urls: false,
|
||||
});
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
|
||||
expect(container.querySelector(".mx_ServerPicker_change")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show form without change server link when custom URLs disabled", async () => {
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
expect(container.querySelectorAll(".mx_ServerPicker_change")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should show SSO button if that flow is available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.sso" }] });
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show both SSO button and username+password if both are available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }, { type: "m.login.sso" }] });
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show multiple SSO buttons if multiple identity_providers are available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
identity_providers: [
|
||||
{
|
||||
id: "a",
|
||||
name: "Provider 1",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
name: "Provider 2",
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
name: "Provider 3",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should show single SSO button if identity_providers is null", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle serverConfig updates correctly", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(platform.startSingleSignOn.mock.calls[0][0].baseUrl).toBe("https://matrix.org");
|
||||
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(platform.startSingleSignOn.mock.calls[1][0].baseUrl).toBe("https://server2");
|
||||
});
|
||||
|
||||
it("should handle updating to a server with no supported flows", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// update the mock for the new server with no supported flows
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "just something weird",
|
||||
},
|
||||
],
|
||||
});
|
||||
// render with a new server
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("This homeserver doesn't offer any login flows that are supported by this client."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// no sso button because server2 doesnt support sso
|
||||
expect(container.querySelector(".mx_SSOButton")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show single Continue button if OIDC MSC3824 compatibility is given by server", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]: true,
|
||||
},
|
||||
{
|
||||
type: "m.login.password",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
|
||||
expect(ssoButtons.length).toBe(1);
|
||||
expect(ssoButtons[0].textContent).toBe("Continue");
|
||||
|
||||
// no password form visible
|
||||
expect(container.querySelector("form")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should show branded SSO buttons", async () => {
|
||||
const idpsWithIcons = Object.values(IdentityProviderBrand).map((brand) => ({
|
||||
id: brand,
|
||||
brand,
|
||||
name: `Provider ${brand}`,
|
||||
}));
|
||||
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
identity_providers: [
|
||||
...idpsWithIcons,
|
||||
{
|
||||
id: "foo",
|
||||
name: "Provider foo",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
for (const idp of idpsWithIcons) {
|
||||
const ssoButton = container.querySelector(`.mx_SSOButton.mx_SSOButton_brand_${idp.brand}`);
|
||||
expect(ssoButton).toBeTruthy();
|
||||
expect(ssoButton?.querySelector(`img[alt="${idp.brand}"]`)).toBeTruthy();
|
||||
}
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(idpsWithIcons.length + 1);
|
||||
});
|
||||
|
||||
it("should display an error when homeserver doesn't offer any supported login flows", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "just something weird",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("This homeserver doesn't offer any login flows that are supported by this client."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a connection error when getting login flows fails", async () => {
|
||||
mockClient.loginFlows.mockRejectedValue("oups");
|
||||
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("There was a problem communicating with the homeserver, please try again later."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display an error when homeserver fails liveliness check", async () => {
|
||||
fetchMock.resetBehavior();
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
status: 0,
|
||||
});
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error displayed
|
||||
expect(screen.getByText("Cannot reach homeserver")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should reset liveliness error when server config changes", async () => {
|
||||
fetchMock.resetBehavior();
|
||||
// matrix.org is not alive
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
status: 400,
|
||||
});
|
||||
// but server2 is
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
const { rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error displayed
|
||||
expect(screen.getByText("Cannot reach homeserver")).toBeInTheDocument();
|
||||
|
||||
rerender(getRawComponent("https://server2"));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error cleared
|
||||
expect(screen.queryByText("Cannot reach homeserver")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("OIDC native flow", () => {
|
||||
const hsUrl = "https://matrix.org";
|
||||
const isUrl = "https://vector.im";
|
||||
const issuer = "https://test.com/";
|
||||
const delegatedAuth = makeDelegatedAuthConfig(issuer);
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, "error");
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === Features.OidcNativeFlow,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
it("should not attempt registration when oidc native flow setting is disabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// didn't try to register
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
|
||||
// continued with normal setup
|
||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||
// normal password login rendered
|
||||
expect(screen.getByLabelText("Username")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should attempt to register oidc client", async () => {
|
||||
// dont mock, spy so we can check config values were correctly passed
|
||||
jest.spyOn(registerClientUtils, "getOidcClientId");
|
||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// tried to register
|
||||
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
|
||||
// called with values from config
|
||||
expect(registerClientUtils.getOidcClientId).toHaveBeenCalledWith(delegatedAuth, oidcStaticClientsConfig);
|
||||
});
|
||||
|
||||
it("should fallback to normal login when client registration fails", async () => {
|
||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { status: 500 });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// tried to register
|
||||
expect(fetchMock).toHaveBeenCalledWith(delegatedAuth.registrationEndpoint, expect.any(Object));
|
||||
expect(logger.error).toHaveBeenCalledWith(new Error(OidcError.DynamicRegistrationFailed));
|
||||
|
||||
// continued with normal setup
|
||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||
// normal password login rendered
|
||||
expect(screen.getByLabelText("Username")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// short term during active development, UI will be added in next PRs
|
||||
it("should show continue button when oidc native flow is correctly configured", async () => {
|
||||
fetchMock.post(delegatedAuth.registrationEndpoint!, { client_id: "abc123" });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// did not continue with matrix login
|
||||
expect(mockClient.loginFlows).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Continue")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* Oidc-aware flows still work while the oidc-native feature flag is disabled
|
||||
*/
|
||||
it("should show oidc-aware flow for oidc-enabled homeserver when oidc native flow setting is disabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]: true,
|
||||
},
|
||||
{
|
||||
type: "m.login.password",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// didn't try to register
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(delegatedAuth.registrationEndpoint);
|
||||
// continued with normal setup
|
||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||
// oidc-aware 'continue' button displayed
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(1);
|
||||
expect(ssoButtons[0].textContent).toBe("Continue");
|
||||
// no password form visible
|
||||
expect(container.querySelector("form")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { act, render, RenderResult } from "jest-matrix-react";
|
||||
import React, { ComponentProps } from "react";
|
||||
import EventEmitter from "events";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { LoginSplashView } from "../../../../src/components/structures/auth/LoginSplashView";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
describe("<LoginSplashView />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
beforeEach(() => {
|
||||
matrixClient = new EventEmitter() as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof LoginSplashView>> = {}): RenderResult {
|
||||
const defaultProps = {
|
||||
matrixClient,
|
||||
onLogoutClick: () => {},
|
||||
syncError: null,
|
||||
};
|
||||
return render(<LoginSplashView {...defaultProps} {...props} />);
|
||||
}
|
||||
|
||||
it("Renders a spinner", () => {
|
||||
const rendered = getComponent();
|
||||
expect(rendered.getByTestId("spinner")).toBeInTheDocument();
|
||||
expect(rendered.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Renders an error message", () => {
|
||||
const rendered = getComponent({ syncError: new Error("boohoo") });
|
||||
expect(rendered.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Calls onLogoutClick", () => {
|
||||
const onLogoutClick = jest.fn();
|
||||
const rendered = getComponent({ onLogoutClick });
|
||||
expect(onLogoutClick).not.toHaveBeenCalled();
|
||||
rendered.getByRole("button", { name: "Logout" }).click();
|
||||
expect(onLogoutClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Shows migration progress", async () => {
|
||||
const rendered = getComponent();
|
||||
|
||||
act(() => {
|
||||
matrixClient.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, 5, 10);
|
||||
});
|
||||
rendered.getByText("Hang tight.", { exact: false });
|
||||
|
||||
// Wait for the animation to update
|
||||
await act(() => sleep(500));
|
||||
|
||||
const progress = rendered.getByRole("progressbar");
|
||||
expect(progress.getAttribute("value")).toEqual("5");
|
||||
expect(progress.getAttribute("max")).toEqual("10");
|
||||
});
|
||||
});
|
249
test/unit-tests/components/structures/auth/Registration-test.tsx
Normal file
249
test/unit-tests/components/structures/auth/Registration-test.tsx
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { createClient, MatrixClient, MatrixError, OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig";
|
||||
import { getMockClientWithEventEmitter, mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils";
|
||||
import Registration from "../../../../src/components/structures/auth/Registration";
|
||||
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { Features } from "../../../../src/settings/Settings";
|
||||
import { startOidcLogin } from "../../../../src/utils/oidc/authorize";
|
||||
|
||||
jest.mock("../../../../src/utils/oidc/authorize", () => ({
|
||||
startOidcLogin: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||
createClient: jest.fn(),
|
||||
}));
|
||||
|
||||
/** The matrix versions our mock server claims to support */
|
||||
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
|
||||
|
||||
describe("Registration", function () {
|
||||
let mockClient!: MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(function () {
|
||||
SdkConfig.put({
|
||||
...DEFAULTS,
|
||||
disable_custom_urls: true,
|
||||
});
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
registerRequest: jest.fn(),
|
||||
loginFlows: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }),
|
||||
});
|
||||
mockClient.registerRequest.mockRejectedValueOnce(
|
||||
new MatrixError(
|
||||
{
|
||||
flows: [{ stages: [] }],
|
||||
},
|
||||
401,
|
||||
),
|
||||
);
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||
mocked(createClient).mockImplementation((opts) => {
|
||||
mockClient.idBaseUrl = opts.idBaseUrl;
|
||||
mockClient.baseUrl = opts.baseUrl;
|
||||
return mockClient;
|
||||
});
|
||||
fetchMock.catch(404);
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
mockPlatformPeg({
|
||||
startSingleSignOn: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jest.restoreAllMocks();
|
||||
fetchMock.restore();
|
||||
SdkConfig.reset(); // we touch the config, so clean up
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
defaultDeviceDisplayName: "test-device-display-name",
|
||||
onLoggedIn: jest.fn(),
|
||||
onLoginClick: jest.fn(),
|
||||
onServerConfigChange: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultHsUrl = "https://matrix.org";
|
||||
const defaultIsUrl = "https://vector.im";
|
||||
|
||||
function getRawComponent(
|
||||
hsUrl = defaultHsUrl,
|
||||
isUrl = defaultIsUrl,
|
||||
authConfig?: OidcClientConfig,
|
||||
mobileRegister?: boolean,
|
||||
) {
|
||||
return (
|
||||
<Registration
|
||||
{...defaultProps}
|
||||
serverConfig={mkServerConfig(hsUrl, isUrl, authConfig)}
|
||||
mobileRegister={mobileRegister}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getComponent(hsUrl?: string, isUrl?: string, authConfig?: OidcClientConfig, mobileRegister?: boolean) {
|
||||
return render(getRawComponent(hsUrl, isUrl, authConfig, mobileRegister));
|
||||
}
|
||||
|
||||
it("should show server picker", async function () {
|
||||
const { container } = getComponent();
|
||||
expect(container.querySelector(".mx_ServerPicker")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show form when custom URLs disabled", async function () {
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show SSO options if those are available", async () => {
|
||||
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.sso" }] });
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle serverConfig updates correctly", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(mockClient.baseUrl).toBe("https://matrix.org");
|
||||
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(mockClient.baseUrl).toBe("https://server2");
|
||||
});
|
||||
|
||||
describe("when delegated authentication is configured and enabled", () => {
|
||||
const authConfig = makeDelegatedAuthConfig();
|
||||
const clientId = "test-client-id";
|
||||
// @ts-ignore
|
||||
authConfig.metadata["prompt_values_supported"] = ["create"];
|
||||
|
||||
beforeEach(() => {
|
||||
// mock a statically registered client to avoid dynamic registration
|
||||
SdkConfig.put({
|
||||
oidc_static_clients: {
|
||||
[authConfig.metadata.issuer]: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, {
|
||||
issuer: authConfig.metadata.issuer,
|
||||
});
|
||||
fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata);
|
||||
fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] });
|
||||
});
|
||||
|
||||
describe("when oidc native flow is not enabled in settings", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should display user/pass registration form", async () => {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||
expect(mockClient.registerRequest).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when oidc native flow is enabled in settings", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => key === Features.OidcNativeFlow);
|
||||
});
|
||||
|
||||
it("should display oidc-native continue button", async () => {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
// no form
|
||||
expect(container.querySelector("form")).toBeFalsy();
|
||||
|
||||
expect(await screen.findByText("Continue")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should start OIDC login flow as registration on button click", async () => {
|
||||
getComponent(defaultHsUrl, defaultIsUrl, authConfig);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(await screen.findByText("Continue"));
|
||||
|
||||
expect(startOidcLogin).toHaveBeenCalledWith(
|
||||
authConfig,
|
||||
clientId,
|
||||
defaultHsUrl,
|
||||
defaultIsUrl,
|
||||
// isRegistration
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when is mobile registeration", () => {
|
||||
it("should not show server picker", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
expect(container.querySelector(".mx_ServerPicker")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should show username field with autocaps disabled", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector("#mx_RegistrationForm_username")).toHaveAttribute(
|
||||
"autocapitalize",
|
||||
"none",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show password and confirm password fields in separate rows", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
|
||||
await waitFor(() => expect(container.querySelector("#mx_RegistrationForm_username")).toBeTruthy());
|
||||
// when password and confirm password fields are in separate rows there should be 4 rather than 3
|
||||
expect(container.querySelectorAll(".mx_AuthBody_fieldRow")).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LoginSplashView /> Renders a spinner 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginSplashView_splashButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LoginSplashView /> Renders an error message 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginSplashView_syncError"
|
||||
>
|
||||
<div>
|
||||
Unable to connect to Homeserver. Retrying…
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginSplashView_splashButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
27
test/unit-tests/components/views/Validation-test.ts
Normal file
27
test/unit-tests/components/views/Validation-test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
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 withValidation from "../../../src/components/views/elements/Validation";
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should handle 0 rules", () => {
|
||||
const handler = withValidation({
|
||||
rules: [],
|
||||
});
|
||||
return expect(
|
||||
handler({
|
||||
value: "value",
|
||||
focused: true,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
valid: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { EmojiMapping } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { tEmoji } from "../../../src/components/views/verification/VerificationShowSas";
|
||||
|
||||
describe("tEmoji", () => {
|
||||
it.each([
|
||||
["en-GB", "Dog"],
|
||||
["en", "Dog"],
|
||||
["de-DE", "Hund"],
|
||||
["pt", "Cachorro"],
|
||||
])("should handle locale %s", (locale, expectation) => {
|
||||
const emoji: EmojiMapping = ["🐶", "Dog"];
|
||||
expect(tEmoji(emoji, locale)).toEqual(expectation);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import RecordingPlayback, { PlaybackLayout } from "../../../../src/components/views/audio_messages/RecordingPlayback";
|
||||
import { Playback } from "../../../../src/audio/Playback";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { createAudioContext } from "../../../../src/audio/compat";
|
||||
import { flushPromises } from "../../../test-utils";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
|
||||
jest.mock("../../../../src/WorkerManager", () => ({
|
||||
WorkerManager: jest.fn(() => ({
|
||||
call: jest.fn().mockResolvedValue({ waveform: [0, 0, 1, 1] }),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/audio/compat", () => ({
|
||||
createAudioContext: jest.fn(),
|
||||
decodeOgg: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
describe("<RecordingPlayback />", () => {
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
currentTime: 0,
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
};
|
||||
|
||||
const mockAudioBuffer = {
|
||||
duration: 99,
|
||||
getChannelData: jest.fn(),
|
||||
};
|
||||
|
||||
const mockChannelData = new Float32Array();
|
||||
|
||||
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
|
||||
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
|
||||
render(
|
||||
<RoomContext.Provider value={room}>
|
||||
<RecordingPlayback {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData);
|
||||
mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer));
|
||||
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
|
||||
});
|
||||
|
||||
const getPlayButton = (component: RenderResult) => component.getByTestId("play-pause-button");
|
||||
|
||||
it("renders recording playback", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables play button while playback is decoding", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
expect(getPlayButton(component)).toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("enables play button when playback is finished decoding", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
await flushPromises();
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("displays error when playback decoding fails", async () => {
|
||||
// stub logger to keep console clean from expected error
|
||||
jest.spyOn(logger, "error").mockReturnValue(undefined);
|
||||
jest.spyOn(logger, "warn").mockReturnValue(undefined);
|
||||
mockAudioContext.decodeAudioData.mockImplementation((_b, _cb, error) => error(new Error("oh no")));
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
await flushPromises();
|
||||
expect(component.container.querySelector(".text-warning")).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays pre-prepared playback with correct playback phase", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
await playback.prepare();
|
||||
const component = getComponent({ playback });
|
||||
// playback already decoded, button is not disabled
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
|
||||
expect(component.container.querySelector(".text-warning")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("toggles playback on play pause button click", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
jest.spyOn(playback, "toggle").mockResolvedValue(undefined);
|
||||
await playback.prepare();
|
||||
const component = getComponent({ playback });
|
||||
|
||||
fireEvent.click(getPlayButton(component));
|
||||
|
||||
expect(playback.toggle).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Composer Layout", () => {
|
||||
it("should have a waveform, no seek bar, and clock", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback, layout: PlaybackLayout.Composer });
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Timeline Layout", () => {
|
||||
it("should have a waveform, a seek bar, and clock", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should be the default", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback }); // no layout set for test
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
125
test/unit-tests/components/views/audio_messages/SeekBar-test.tsx
Normal file
125
test/unit-tests/components/views/audio_messages/SeekBar-test.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
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, { createRef, RefObject } from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import { Playback } from "../../../../src/audio/Playback";
|
||||
import { createTestPlayback } from "../../../test-utils/audio";
|
||||
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
|
||||
|
||||
describe("SeekBar", () => {
|
||||
let playback: Playback;
|
||||
let renderResult: RenderResult;
|
||||
let frameRequestCallback: FrameRequestCallback;
|
||||
let seekBarRef: RefObject<SeekBar>;
|
||||
|
||||
beforeEach(() => {
|
||||
seekBarRef = createRef();
|
||||
jest.spyOn(window, "requestAnimationFrame").mockImplementation((callback: FrameRequestCallback) => {
|
||||
frameRequestCallback = callback;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(window.requestAnimationFrame).mockRestore();
|
||||
});
|
||||
|
||||
describe("when rendering a SeekBar for an empty playback", () => {
|
||||
beforeEach(() => {
|
||||
playback = createTestPlayback({
|
||||
durationSeconds: 0,
|
||||
timeSeconds: 0,
|
||||
});
|
||||
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a SeekBar", () => {
|
||||
beforeEach(() => {
|
||||
playback = createTestPlayback();
|
||||
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render the initial position", () => {
|
||||
// expected value 3141 / 31415 ~ 0.099984084
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and the playback proceeds", () => {
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore
|
||||
playback.timeSeconds = 6969;
|
||||
act(() => {
|
||||
playback.liveData.update([playback.timeSeconds, playback.durationSeconds]);
|
||||
frameRequestCallback(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
// expected value 6969 / 31415 ~ 0.221836702
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and seeking position with the slider", () => {
|
||||
beforeEach(() => {
|
||||
const rangeInput = renderResult.container.querySelector("[type='range']");
|
||||
act(() => {
|
||||
fireEvent.change(rangeInput!, { target: { value: 0.5 } });
|
||||
});
|
||||
});
|
||||
|
||||
it("should update the playback", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(0.5 * playback.durationSeconds);
|
||||
});
|
||||
|
||||
describe("and seeking left", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.skipTo).mockClear();
|
||||
act(() => {
|
||||
seekBarRef.current!.left();
|
||||
});
|
||||
});
|
||||
|
||||
it("should skip to minus 5 seconds", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds - 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and seeking right", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.skipTo).mockClear();
|
||||
act(() => {
|
||||
seekBarRef.current!.right();
|
||||
});
|
||||
});
|
||||
|
||||
it("should skip to plus 5 seconds", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds + 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a disabled SeekBar", () => {
|
||||
beforeEach(async () => {
|
||||
renderResult = render(<SeekBar disabled={true} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar and the playback proceeds should render as expected 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.22183670221231896;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.22183670221231896"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar for an empty playback should render correctly 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar should render the initial position 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.0999840840362884;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.0999840840362884"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a disabled SeekBar should render as expected 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
disabled=""
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.0999840840362884;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.0999840840362884"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 userEvent from "@testing-library/user-event";
|
||||
|
||||
import CountryDropdown from "../../../../src/components/views/auth/CountryDropdown";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
|
||||
describe("CountryDropdown", () => {
|
||||
describe("default_country_code", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["GB", 44],
|
||||
["IE", 353],
|
||||
["ES", 34],
|
||||
["FR", 33],
|
||||
["PL", 48],
|
||||
["DE", 49],
|
||||
])("should respect configured default country code for %s", (config, defaultCountryCode) => {
|
||||
SdkConfig.add({
|
||||
default_country_code: config,
|
||||
});
|
||||
|
||||
const fn = jest.fn();
|
||||
render(<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />);
|
||||
expect(fn).toHaveBeenCalledWith(expect.objectContaining({ prefix: defaultCountryCode.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultCountry", () => {
|
||||
it.each([
|
||||
["en-GB", 44],
|
||||
["en-ie", 353],
|
||||
["es-ES", 34],
|
||||
["fr", 33],
|
||||
["pl", 48],
|
||||
["de-DE", 49],
|
||||
])("should pick appropriate default country for %s", (language, defaultCountryCode) => {
|
||||
Object.defineProperty(navigator, "language", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return language;
|
||||
},
|
||||
});
|
||||
|
||||
const fn = jest.fn();
|
||||
render(<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />);
|
||||
expect(fn).toHaveBeenCalledWith(expect.objectContaining({ prefix: defaultCountryCode.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow filtering", async () => {
|
||||
const fn = jest.fn();
|
||||
const { getByRole, findByText } = render(
|
||||
<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />,
|
||||
);
|
||||
|
||||
const dropdown = getByRole("button");
|
||||
fireEvent.click(dropdown);
|
||||
|
||||
await userEvent.keyboard("Al");
|
||||
|
||||
await expect(findByText("Albania (+355)")).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 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, waitFor, act, fireEvent } from "jest-matrix-react";
|
||||
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
EmailIdentityAuthEntry,
|
||||
MasUnlockCrossSigningAuthEntry,
|
||||
} from "../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
||||
import { createTestClient } from "../../../test-utils";
|
||||
|
||||
describe("<EmailIdentityAuthEntry/>", () => {
|
||||
const renderIdentityAuth = () => {
|
||||
const matrixClient = createTestClient();
|
||||
|
||||
return render(
|
||||
<EmailIdentityAuthEntry
|
||||
matrixClient={matrixClient}
|
||||
loginType={AuthType.Email}
|
||||
onPhaseChange={jest.fn()}
|
||||
submitAuthDict={jest.fn()}
|
||||
fail={jest.fn()}
|
||||
clientSecret="my secret"
|
||||
showContinue={true}
|
||||
inputs={{ emailAddress: "alice@example.xyz" }}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
test("should render", () => {
|
||||
const { container } = renderIdentityAuth();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should clear the requested state when the button tooltip is hidden", async () => {
|
||||
renderIdentityAuth();
|
||||
|
||||
// After a click on the resend button, the button should display the resent label
|
||||
screen.getByRole("button", { name: "Resend" }).click();
|
||||
await waitFor(() => expect(screen.queryByRole("button", { name: "Resent!" })).toBeInTheDocument());
|
||||
expect(screen.queryByRole("button", { name: "Resend" })).toBeNull();
|
||||
|
||||
const resentButton = screen.getByRole("button", { name: "Resent!" });
|
||||
// Hover briefly the button and wait for the tooltip to be displayed
|
||||
await userEvent.hover(resentButton);
|
||||
await waitFor(() => expect(screen.getByRole("tooltip", { name: "Resent!" })).toBeInTheDocument());
|
||||
|
||||
// On unhover, it should display again the resend button
|
||||
await act(() => userEvent.unhover(resentButton));
|
||||
await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
describe("<MasUnlockCrossSigningAuthEntry/>", () => {
|
||||
const renderAuth = (props = {}) => {
|
||||
const matrixClient = createTestClient();
|
||||
|
||||
return render(
|
||||
<MasUnlockCrossSigningAuthEntry
|
||||
matrixClient={matrixClient}
|
||||
loginType={AuthType.Email}
|
||||
onPhaseChange={jest.fn()}
|
||||
submitAuthDict={jest.fn()}
|
||||
fail={jest.fn()}
|
||||
clientSecret="my secret"
|
||||
showContinue={true}
|
||||
stageParams={{ url: "https://example.com" }}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
test("should render", () => {
|
||||
const { container } = renderAuth();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should open idp in new tab on click", async () => {
|
||||
const spy = jest.spyOn(global.window, "open");
|
||||
renderAuth();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Go to your account" }));
|
||||
expect(spy).toHaveBeenCalledWith("https://example.com", "_blank");
|
||||
});
|
||||
|
||||
test("should retry uia request on click", async () => {
|
||||
const submitAuthDict = jest.fn();
|
||||
renderAuth({ submitAuthDict });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||
expect(submitAuthDict).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 Callum Brown
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import InteractiveAuthComponent from "../../../../src/components/structures/InteractiveAuth";
|
||||
import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
|
||||
|
||||
describe("InteractiveAuthComponent", function () {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
matrixClient: mockClient,
|
||||
makeRequest: jest.fn().mockResolvedValue(undefined),
|
||||
onAuthFinished: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => render(<InteractiveAuthComponent {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockClientPeg();
|
||||
});
|
||||
|
||||
const getSubmitButton = ({ container }: RenderResult) =>
|
||||
container.querySelector(".mx_AccessibleButton_kind_primary");
|
||||
const getRegistrationTokenInput = ({ container }: RenderResult) =>
|
||||
container.querySelector('input[name="registrationTokenField"]');
|
||||
|
||||
it("Should successfully complete a registration token flow", async () => {
|
||||
const onAuthFinished = jest.fn();
|
||||
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
|
||||
|
||||
const authData = {
|
||||
session: "sess",
|
||||
flows: [{ stages: ["m.login.registration_token"] }],
|
||||
};
|
||||
|
||||
const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
|
||||
|
||||
const registrationTokenNode = getRegistrationTokenInput(wrapper);
|
||||
const submitNode = getSubmitButton(wrapper);
|
||||
const formNode = wrapper.container.querySelector("form");
|
||||
|
||||
expect(registrationTokenNode).toBeTruthy();
|
||||
expect(submitNode).toBeTruthy();
|
||||
expect(formNode).toBeTruthy();
|
||||
|
||||
// submit should be disabled
|
||||
expect(submitNode).toHaveAttribute("disabled");
|
||||
expect(submitNode).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
// put something in the registration token box
|
||||
fireEvent.change(registrationTokenNode!, { target: { value: "s3kr3t" } });
|
||||
|
||||
expect(getRegistrationTokenInput(wrapper)).toHaveValue("s3kr3t");
|
||||
expect(submitNode).not.toHaveAttribute("disabled");
|
||||
expect(submitNode).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
// hit enter; that should trigger a request
|
||||
fireEvent.submit(formNode!);
|
||||
|
||||
// wait for auth request to resolve
|
||||
await flushPromises();
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledTimes(1);
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
session: "sess",
|
||||
type: "m.login.registration_token",
|
||||
token: "s3kr3t",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onAuthFinished).toHaveBeenCalledTimes(1);
|
||||
expect(onAuthFinished).toHaveBeenCalledWith(
|
||||
true,
|
||||
{ a: 1 },
|
||||
{ clientSecret: "t35tcl1Ent5ECr3T", emailSid: undefined },
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EmailIdentityAuthEntry/> should render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_InteractiveAuthEntryComponents_emailWrapper"
|
||||
>
|
||||
<p>
|
||||
<span>
|
||||
To create your account, open the link in the email we just sent to
|
||||
<strong>
|
||||
alice@example.xyz
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="secondary"
|
||||
>
|
||||
<span>
|
||||
Did not receive it?
|
||||
<div
|
||||
aria-label="Resend"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend it
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||
>
|
||||
Reset your identity through your account provider and then come back and click “Retry”.
|
||||
</p>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton _has-icon_i91xf_66"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2Z"
|
||||
/>
|
||||
</svg>
|
||||
Go to your account
|
||||
</button>
|
||||
<button
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { JoinRule, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import DecoratedRoomAvatar from "../../../../src/components/views/avatars/DecoratedRoomAvatar";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
|
||||
jest.mock("../../../../src/utils/presence", () => ({ isPresenceEnabled: jest.fn().mockReturnValue(true) }));
|
||||
|
||||
jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([0, 1]),
|
||||
}));
|
||||
|
||||
describe("DecoratedRoomAvatar", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function renderComponent() {
|
||||
return render(<DecoratedRoomAvatar room={room} size="32px" />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows an avatar with globe icon and tooltip for public room", async () => {
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
room.getJoinRule = jest.fn().mockReturnValue(JoinRule.Public);
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const globe = container.querySelector(".mx_DecoratedRoomAvatar_icon_globe")!;
|
||||
expect(globe).toBeVisible();
|
||||
await userEvent.hover(globe!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(globe.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent("This room is public");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows the presence indicator in a DM room that also has functional members", async () => {
|
||||
const DM_USER_ID = "@bob:foo.bar";
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: () => {
|
||||
return DM_USER_ID;
|
||||
},
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
jest.spyOn(DecoratedRoomAvatar.prototype as any, "getPresenceIcon").mockImplementation(() => "ONLINE");
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const presence = container.querySelector(".mx_DecoratedRoomAvatar_icon")!;
|
||||
expect(presence).toBeVisible();
|
||||
await userEvent.hover(presence!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent("Online");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
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 { getByTestId, render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
|
||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { getRoomContext } from "../../../test-utils/room";
|
||||
import { stubClient } from "../../../test-utils/test-utils";
|
||||
|
||||
describe("MemberAvatar", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let member: RoomMember;
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof MemberAvatar>>) {
|
||||
return (
|
||||
<RoomContext.Provider value={getRoomContext(room, {})}>
|
||||
<MemberAvatar member={null} size="35px" {...props} />
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
member = new RoomMember(ROOM_ID, "@bob:example.org");
|
||||
jest.spyOn(room, "getMember").mockReturnValue(member);
|
||||
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
|
||||
});
|
||||
|
||||
it("shows an avatar for useOnlyCurrentProfiles", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
return settingName === "useOnlyCurrentProfiles";
|
||||
});
|
||||
|
||||
const { container } = render(getComponent({}));
|
||||
|
||||
let avatar: HTMLElement;
|
||||
await waitFor(() => {
|
||||
avatar = getByTestId(container, "avatar-img");
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(avatar!.getAttribute("src")).not.toBe("");
|
||||
});
|
||||
});
|
67
test/unit-tests/components/views/avatars/RoomAvatar-test.tsx
Normal file
67
test/unit-tests/components/views/avatars/RoomAvatar-test.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import RoomAvatar from "../../../../src/components/views/avatars/RoomAvatar";
|
||||
import { filterConsole, stubClient } from "../../../test-utils";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||
import * as AvatarModule from "../../../../src/Avatar";
|
||||
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||
|
||||
describe("RoomAvatar", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
filterConsole(
|
||||
// unrelated for this test
|
||||
"Room !room:example.com does not have an m.room.create event",
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
|
||||
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
|
||||
});
|
||||
|
||||
it("should render as expected for a Room", () => {
|
||||
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||
room.name = "test room";
|
||||
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render as expected for a DM room", () => {
|
||||
const userId = "@dm_user@example.com";
|
||||
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||
room.name = "DM room";
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
|
||||
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render as expected for a LocalRoom", () => {
|
||||
const userId = "@local_room_user@example.com";
|
||||
const localRoom = new LocalRoom("!room:example.com", client, client.getSafeUserId());
|
||||
localRoom.name = "local test room";
|
||||
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
|
||||
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 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 { render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, Room, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import WithPresenceIndicator from "../../../../src/components/views/avatars/WithPresenceIndicator";
|
||||
import { isPresenceEnabled } from "../../../../src/utils/presence";
|
||||
|
||||
jest.mock("../../../../src/utils/presence");
|
||||
|
||||
jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]),
|
||||
}));
|
||||
|
||||
describe("WithPresenceIndicator", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<WithPresenceIndicator room={room} size="32px">
|
||||
<span />
|
||||
</WithPresenceIndicator>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders only child if presence is disabled", async () => {
|
||||
mocked(isPresenceEnabled).mockReturnValue(false);
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.children).toHaveLength(1);
|
||||
expect(container.children[0].tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["online", "Online"],
|
||||
["offline", "Offline"],
|
||||
["unavailable", "Away"],
|
||||
])("renders presence indicator with tooltip for DM rooms", async (presenceStr, renderedStr) => {
|
||||
mocked(isPresenceEnabled).mockReturnValue(true);
|
||||
const DM_USER_ID = "@bob:foo.bar";
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: () => {
|
||||
return DM_USER_ID;
|
||||
},
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
room.getMember = jest.fn((userId) => {
|
||||
const member = new RoomMember(room.roomId, userId);
|
||||
member.user = new User(userId);
|
||||
member.user.presence = presenceStr;
|
||||
return member;
|
||||
});
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!;
|
||||
expect(presence).toBeVisible();
|
||||
await userEvent.hover(presence!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent(renderedStr);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DecoratedRoomAvatar shows an avatar with globe icon and tooltip for public room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar mx_DecoratedRoomAvatar_cutout"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<div
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_globe"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`DecoratedRoomAvatar shows the presence indicator in a DM room that also has functional members 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar mx_DecoratedRoomAvatar_cutout"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<div
|
||||
aria-labelledby="floating-ui-6"
|
||||
class="mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_online"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomAvatar should render as expected for a DM room 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
D
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
l
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomAvatar should render as expected for a Room 1`] = `
|
||||
<div>
|
||||
<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;"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_online"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-6"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_offline"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 3`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-12"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_away"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
183
test/unit-tests/components/views/beacon/BeaconListItem-test.tsx
Normal file
183
test/unit-tests/components/views/beacon/BeaconListItem-test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
136
test/unit-tests/components/views/beacon/BeaconMarker-test.tsx
Normal file
136
test/unit-tests/components/views/beacon/BeaconMarker-test.tsx
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
155
test/unit-tests/components/views/beacon/OwnBeaconStatus-test.tsx
Normal file
155
test/unit-tests/components/views/beacon/OwnBeaconStatus-test.tsx
Normal 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();
|
||||
});
|
||||
});
|
118
test/unit-tests/components/views/beacon/RoomCallBanner-test.tsx
Normal file
118
test/unit-tests/components/views/beacon/RoomCallBanner-test.tsx
Normal 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)
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<StyledLiveBeaconIcon /> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
73
test/unit-tests/components/views/beta/BetaCard-test.tsx
Normal file
73
test/unit-tests/components/views/beta/BetaCard-test.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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 { render, screen } from "jest-matrix-react";
|
||||
|
||||
import { shouldShowFeedback } from "../../../../src/utils/Feedback";
|
||||
import BetaCard from "../../../../src/components/views/beta/BetaCard";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { TranslationKey } from "../../../../src/languageHandler";
|
||||
|
||||
jest.mock("../../../../src/utils/Feedback");
|
||||
jest.mock("../../../../src/settings/SettingsStore");
|
||||
|
||||
describe("<BetaCard />", () => {
|
||||
describe("Feedback prompt", () => {
|
||||
const featureId = "featureId";
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackLabel: "feedbackLabel",
|
||||
feedbackSubheading: "feedbackSubheading" as TranslationKey,
|
||||
});
|
||||
mocked(SettingsStore).getValue.mockReturnValue(true);
|
||||
mocked(shouldShowFeedback).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should show feedback prompt", () => {
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if beta is disabled", () => {
|
||||
mocked(SettingsStore).getValue.mockReturnValue(false);
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if label is unset", () => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackSubheading: "feedbackSubheading" as TranslationKey,
|
||||
});
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if subheading is unset", () => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackLabel: "feedbackLabel",
|
||||
});
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if feedback is disabled", () => {
|
||||
mocked(shouldShowFeedback).mockReturnValue(false);
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
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 ContextMenu, { ChevronFace } from "../../../../src/components/structures/ContextMenu";
|
||||
import UIStore from "../../../../src/stores/UIStore";
|
||||
import Modal from "../../../../src/Modal";
|
||||
import BaseDialog from "../../../../src/components/views/dialogs/BaseDialog";
|
||||
|
||||
describe("<ContextMenu />", () => {
|
||||
// Hardcode window and menu dimensions
|
||||
const windowSize = 300;
|
||||
const menuSize = 200;
|
||||
jest.spyOn(UIStore, "instance", "get").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
windowWidth: windowSize,
|
||||
windowHeight: windowSize,
|
||||
}) as unknown as UIStore,
|
||||
);
|
||||
window.Element.prototype.getBoundingClientRect = jest.fn().mockReturnValue({
|
||||
width: menuSize,
|
||||
height: menuSize,
|
||||
});
|
||||
|
||||
const targetChevronOffset = 25;
|
||||
|
||||
it("near top edge of window", () => {
|
||||
const targetY = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={windowSize - targetY - menuSize}
|
||||
right={menuSize}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Left}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_left")!;
|
||||
|
||||
const bottomStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("bottom"),
|
||||
);
|
||||
const actualY = windowSize - bottomStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near right edge of window", () => {
|
||||
const targetX = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={0}
|
||||
onFinished={onFinished}
|
||||
left={targetX}
|
||||
chevronFace={ChevronFace.Top}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_top")!;
|
||||
|
||||
const actualX = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("left"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("near bottom edge of window", () => {
|
||||
const targetY = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={targetY}
|
||||
left={0}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Right}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_right")!;
|
||||
|
||||
const actualY = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("top"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near left edge of window", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_bottom")!;
|
||||
|
||||
const rightStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("right"),
|
||||
);
|
||||
const actualX = windowSize - rightStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("should automatically close when a modal is opened", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not automatically close when a modal is opened under the existing one", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
Modal.createDialog(BaseDialog);
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog, {}, "", false, true);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.appendDialog(BaseDialog);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { _t } from "../../../../src/languageHandler";
|
||||
import EmbeddedPage from "../../../../src/components/structures/EmbeddedPage";
|
||||
|
||||
jest.mock("../../../../src/languageHandler", () => ({
|
||||
_t: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<EmbeddedPage />", () => {
|
||||
it("should translate _t strings", async () => {
|
||||
mocked(_t).mockReturnValue("Przeglądaj pokoje");
|
||||
fetchMock.get("https://home.page", {
|
||||
body: '<h1>_t("Explore rooms")</h1>',
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://home.page" />);
|
||||
await screen.findByText("Przeglądaj pokoje");
|
||||
expect(_t).toHaveBeenCalledWith("Explore rooms");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show error if unable to load", async () => {
|
||||
mocked(_t).mockReturnValue("Couldn't load page");
|
||||
fetchMock.get("https://other.page", {
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://other.page" />);
|
||||
await screen.findByText("Couldn't load page");
|
||||
expect(_t).toHaveBeenCalledWith("cant_load_page");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render nothing if no url given", () => {
|
||||
const { asFragment } = render(<EmbeddedPage />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,553 @@
|
|||
/*
|
||||
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, RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
PendingEventOrdering,
|
||||
BeaconIdentifier,
|
||||
Beacon,
|
||||
getBeaconInfoIdentifier,
|
||||
EventType,
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
EventTimeline,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import { canEditContent } from "../../../../src/utils/EventUtils";
|
||||
import { copyPlaintext, getSelectedText } from "../../../../src/utils/strings";
|
||||
import MessageContextMenu from "../../../../src/components/views/context_menus/MessageContextMenu";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent, makeLocationEvent, stubClient } from "../../../test-utils";
|
||||
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";
|
||||
import { createMessageEventContent } from "../../../test-utils/events";
|
||||
|
||||
jest.mock("../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn(),
|
||||
getSelectedText: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../src/utils/EventUtils", () => ({
|
||||
...(jest.requireActual("../../../../src/utils/EventUtils") as object),
|
||||
canEditContent: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../src/dispatcher/dispatcher");
|
||||
|
||||
const roomId = "roomid";
|
||||
|
||||
describe("MessageContextMenu", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("does show copy link button when supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const props = {
|
||||
link: "https://google.com/",
|
||||
};
|
||||
createMenuWithContent(eventContent, props);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toHaveAttribute("href", props.link);
|
||||
});
|
||||
|
||||
it("does not show copy link button when not supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("message pinning", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = makeDefaultRoom();
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
});
|
||||
|
||||
it("does not show pin option when user does not have rights to pin", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
// mock permission to disallow adding pinned messages to room
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
createMenu(event, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not show pin option for beacon_info event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice:server.org", roomId, { isLive: false });
|
||||
|
||||
createMenu(deadBeaconEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows pin option when pinning feature is enabled", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.getByRole("menuitem", { name: "Pin" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("pins event on pin option click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: ["!1", "!2"] }),
|
||||
});
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { onFinished, rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Pin" }));
|
||||
|
||||
// added to account data
|
||||
await waitFor(() =>
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
// from account data
|
||||
"!1",
|
||||
"!2",
|
||||
pinnableEvent.getId(),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// add to room's pins
|
||||
await waitFor(() =>
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{
|
||||
pinned: ["!1", "!2", pinnableEvent.getId()],
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unpins event on pin option click when event is pinned", async () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// make the event already pinned in the room
|
||||
const pinEvent = new MatrixEvent({
|
||||
type: EventType.RoomPinnedEvents,
|
||||
room_id: roomId,
|
||||
state_key: "",
|
||||
content: { pinned: [pinnableEvent.getId(), "!another-event"] },
|
||||
});
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.setStateEvents([pinEvent]);
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Unpin" }));
|
||||
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
|
||||
// add to room's pins
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
// pinnableEvent's id removed, other pins intact
|
||||
{ pinned: ["!another-event"] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message forwarding", () => {
|
||||
it("allows forwarding a room message", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a poll", () => {
|
||||
const eventContent = PollStartEvent.from("why?", ["42"], M_POLL_KIND_DISCLOSED);
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not allow forwarding a voice broadcast", () => {
|
||||
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ABC123",
|
||||
);
|
||||
createMenu(broadcastStartEvent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("forwarding beacons", () => {
|
||||
const aliceId = "@alice:server.org";
|
||||
|
||||
it("does not allow forwarding a beacon that is not live", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a beacon that is not live but has a latestLocation", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: deadBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a live beacon that does not have a latestLocation", () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
|
||||
const beacon = new Beacon(beaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(beaconEvent), beacon);
|
||||
createMenu(beaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows forwarding a live beacon that has a location", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens forward dialog with correct event", () => {
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
|
||||
fireEvent.click(document.querySelector('li[aria-label="Forward"]')!);
|
||||
|
||||
// called with forwardableEvent, not beaconInfo event
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: beaconLocation,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("open as map link", () => {
|
||||
it("does not allow opening a plain message in open street maps", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow opening a beacon that does not have a shareable location event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows opening a location event in open street map", () => {
|
||||
const locationEvent = makeLocationEvent("geo:50,50");
|
||||
createMenu(locationEvent);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=50&mlon=50#map=16/50/50",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows opening a beacon that has a shareable location event", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent("@alice", {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("right click", () => {
|
||||
it("copy button does work as expected", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue(text);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]')!;
|
||||
fireEvent.mouseDown(copyButton);
|
||||
expect(copyPlaintext).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it("copy button is not shown when there is nothing to copy", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue("");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]');
|
||||
expect(copyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows edit button when we can edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(true);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show edit button when we cannot edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(false);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows reply button when we can reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show reply button when we cannot reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
const unsentMessage = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
// queued messages are not actionable
|
||||
unsentMessage.setStatus(EventStatus.QUEUED);
|
||||
|
||||
createMenu(unsentMessage, {}, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows react button when we can react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show react button when we cannot react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: false,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows view in room button when the event is a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
mxEvent.getThread = () => ({ rootEvent: mxEvent }) as Thread;
|
||||
const props = {
|
||||
rightClick: true,
|
||||
};
|
||||
const context = {
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
};
|
||||
|
||||
createMenu(mxEvent, props, context);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show view in room button when the event is not a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("creates a new thread on reply in thread click", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
|
||||
createRightClickMenu(mxEvent, context);
|
||||
|
||||
const replyInThreadButton = document.querySelector('li[aria-label="Reply in thread"]')!;
|
||||
fireEvent.click(replyInThreadButton);
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: mxEvent,
|
||||
push: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenu(mxEvent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createMenuWithContent(
|
||||
eventContent: object,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context?: Partial<IRoomState>,
|
||||
): RenderResult {
|
||||
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||
// test is for the Message context menu, it's a fairly safe assumption.
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
return createMenu(mxEvent, props, context);
|
||||
}
|
||||
|
||||
function makeDefaultRoom(): Room {
|
||||
return new Room(roomId, MatrixClientPeg.safeGet(), "@user:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu(
|
||||
mxEvent: MatrixEvent,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context: Partial<IRoomState> = {},
|
||||
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
|
||||
room: Room = makeDefaultRoom(),
|
||||
): RenderResult {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// @ts-ignore illegally set private prop
|
||||
room.currentState.beacons = beacons;
|
||||
|
||||
mxEvent.setStatus(EventStatus.SENT);
|
||||
|
||||
client.getUserId = jest.fn().mockReturnValue("@user:example.com");
|
||||
client.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
return render(
|
||||
<RoomContext.Provider value={context as IRoomState}>
|
||||
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 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 { fireEvent, getByLabelText, render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { ReceiptType, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { ChevronFace } from "../../../../src/components/structures/ContextMenu";
|
||||
import {
|
||||
RoomGeneralContextMenu,
|
||||
RoomGeneralContextMenuProps,
|
||||
} from "../../../../src/components/views/context_menus/RoomGeneralContextMenu";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import RoomListStore from "../../../../src/stores/room-list/RoomListStore";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { mkMessage, stubClient } from "../../../test-utils/test-utils";
|
||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { clearAllModals } from "../../../test-utils";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomGeneralContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
function getComponent(props?: Partial<RoomGeneralContextMenuProps>) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomGeneralContextMenu
|
||||
room={room}
|
||||
onFinished={onFinished}
|
||||
{...props}
|
||||
managed={true}
|
||||
mountAsChild={true}
|
||||
left={1}
|
||||
top={1}
|
||||
chevronFace={ChevronFace.Left}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Favourite,
|
||||
]);
|
||||
|
||||
onFinished = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("renders an empty context menu for archived rooms", async () => {
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([DefaultTagID.Archived]);
|
||||
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the default context menu", async () => {
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not render invite menu item when UIComponent customisations disable room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite menu item when UIComponent customisations enables room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks the room as read", async () => {
|
||||
const event = mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: 1000,
|
||||
});
|
||||
room.addLiveEvents([event], {});
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsReadBtn = getByLabelText(container, "Mark as read");
|
||||
fireEvent.click(markAsReadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(event, ReceiptType.Read, true);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks the room as unread", async () => {
|
||||
room.updateMyMembership("join");
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsUnreadBtn = getByLabelText(container, "Mark as unread");
|
||||
fireEvent.click(markAsUnreadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: true,
|
||||
});
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when developer mode is disabled, it should not render the developer tools option", () => {
|
||||
getComponent();
|
||||
expect(screen.queryByText("Developer tools")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when developer mode is enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode");
|
||||
getComponent();
|
||||
});
|
||||
|
||||
it("should render the developer tools option", async () => {
|
||||
const developerToolsItem = screen.getByRole("menuitem", { name: "Developer tools" });
|
||||
expect(developerToolsItem).toBeInTheDocument();
|
||||
|
||||
// click open developer tools dialog
|
||||
await userEvent.click(developerToolsItem);
|
||||
|
||||
// assert that the dialog is displayed by searching some if its contents
|
||||
expect(await screen.findByText("Toolbox")).toBeInTheDocument();
|
||||
expect(await screen.findByText(`Room ID: ${ROOM_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { prettyDOM, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import SpaceContextMenu from "../../../../src/components/views/context_menus/SpaceContextMenu";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
shouldShowSpaceSettings,
|
||||
showCreateNewRoom,
|
||||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../../../src/utils/space";
|
||||
import { leaveSpace } from "../../../../src/utils/leave-behaviour";
|
||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/utils/space", () => ({
|
||||
shouldShowSpaceSettings: jest.fn(),
|
||||
showCreateNewRoom: jest.fn(),
|
||||
showCreateNewSubspace: jest.fn(),
|
||||
showSpaceInvite: jest.fn(),
|
||||
showSpacePreferences: jest.fn(),
|
||||
showSpaceSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/utils/leave-behaviour", () => ({
|
||||
leaveSpace: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<SpaceContextMenu />", () => {
|
||||
const userId = "@test:server";
|
||||
|
||||
const mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as Mocked<MatrixClient>;
|
||||
|
||||
const makeMockSpace = (props = {}) =>
|
||||
({
|
||||
name: "test space",
|
||||
getJoinRule: jest.fn(),
|
||||
canInvite: jest.fn(),
|
||||
currentState: {
|
||||
maySendStateEvent: jest.fn(),
|
||||
},
|
||||
client: mockClient,
|
||||
getMyMembership: jest.fn(),
|
||||
...props,
|
||||
}) as unknown as Room;
|
||||
|
||||
const defaultProps = {
|
||||
space: makeMockSpace(),
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}): RenderResult =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SpaceContextMenu {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockClient.getUserId.mockReturnValue(userId);
|
||||
mockClient.getSafeUserId.mockReturnValue(userId);
|
||||
});
|
||||
|
||||
it("renders menu correctly", () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(prettyDOM(baseElement)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders invite option when space is public", () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite option when user is has invite rights for space", () => {
|
||||
const space = makeMockSpace({
|
||||
canInvite: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(space.canInvite).toHaveBeenCalledWith(userId);
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens invite dialog when invite option is clicked", async () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("invite-option"));
|
||||
|
||||
expect(showSpaceInvite).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders space settings option when user has rights", () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
renderComponent();
|
||||
expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(screen.getByTestId("settings-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens space settings when space settings option is clicked", async () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("settings-option"));
|
||||
|
||||
expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders leave option when user does not have rights to see space settings", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId("leave-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("leaves space when leave option is clicked", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
await userEvent.click(screen.getByTestId("leave-option"));
|
||||
expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("add children section", () => {
|
||||
const space = makeMockSpace();
|
||||
|
||||
beforeEach(() => {
|
||||
// set space to allow adding children to space
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("does not render section when user does not have permission to add children", () => {
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render section when UIComponent customisations disable room and space creation", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms);
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add room button when UIComponent customisation allows CreateRoom", () => {
|
||||
// only allow CreateRoom
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-room-option")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add space button when UIComponent customisation allows CreateSpace", () => {
|
||||
// only allow CreateSpaces
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-subspace-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens create room dialog on add room button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-room-option"));
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens create space dialog on add space button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-subspace-option"));
|
||||
expect(showCreateNewSubspace).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 { getByTestId, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import ThreadListContextMenu, {
|
||||
ThreadListContextMenuProps,
|
||||
} from "../../../../src/components/views/context_menus/ThreadListContextMenu";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||
import { stubClient } from "../../../test-utils/test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("ThreadListContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
function getComponent(props: Partial<ThreadListContextMenuProps>) {
|
||||
return render(<ThreadListContextMenu mxEvent={event} {...props} />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const res = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
event = res.rootEvent;
|
||||
});
|
||||
|
||||
it("does not render the permalink", async () => {
|
||||
const { container } = getComponent({});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).toBeNull();
|
||||
});
|
||||
|
||||
it("does render the permalink", async () => {
|
||||
const { container } = getComponent({
|
||||
permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false),
|
||||
});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 { screen, render } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import { WidgetContextMenu } from "../../../../src/components/views/context_menus/WidgetContextMenu";
|
||||
import { IApp } from "../../../../src/stores/WidgetStore";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<WidgetContextMenu />", () => {
|
||||
const widgetId = "w1";
|
||||
const eventId = "e1";
|
||||
const roomId = "r1";
|
||||
const userId = "@user-id:server";
|
||||
|
||||
const app: IApp = {
|
||||
id: widgetId,
|
||||
eventId,
|
||||
roomId,
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: userId,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
onFinished = jest.fn();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
||||
mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
it("renders revoke button", async () => {
|
||||
const { rerender } = render(getComponent());
|
||||
|
||||
const revokeButton = screen.getByLabelText("Revoke permissions");
|
||||
expect(revokeButton).toBeInTheDocument();
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
rerender(getComponent());
|
||||
expect(revokeButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("revokes permissions", async () => {
|
||||
render(getComponent());
|
||||
await userEvent.click(screen.getByLabelText("Revoke permissions"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EmbeddedPage /> should render nothing if no url given 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
Couldn't load page
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should translate _t strings 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
<h1>
|
||||
Przeglądaj pokoje
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,97 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomGeneralContextMenu renders an empty context menu for archived rooms 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomGeneralContextMenu renders the default context menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
|
||||
"[36m<body>[39m
|
||||
[36m<div />[39m
|
||||
[36m<div[39m
|
||||
[33mid[39m=[32m"mx_ContextualMenu_Container"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_wrapper"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_background"[39m
|
||||
[36m/>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu"[39m
|
||||
[33mrole[39m=[32m"menu"[39m
|
||||
[36m>[39m
|
||||
[36m<ul[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"[39m
|
||||
[33mrole[39m=[32m"none"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_SpacePanel_contextMenu_header"[39m
|
||||
[36m>[39m
|
||||
[0mtest space[0m
|
||||
[36m</div>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_optionList"[39m
|
||||
[36m>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Space home"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"0"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mSpace home[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Explore rooms"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mExplore rooms[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Preferences"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mPreferences[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Leave space"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"[39m
|
||||
[33mdata-testid[39m=[32m"leave-option"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mLeave space[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m</div>[39m
|
||||
[36m</ul>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</body>[39m"
|
||||
`;
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020-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 { SecretStorage, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { act, fireEvent, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { mockPlatformPeg, stubClient } from "../../../test-utils";
|
||||
import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog";
|
||||
|
||||
const securityKey = "EsTc WKmb ivvk jLS7 Y1NH 5CcQ mP1E JJwj B3Fd pFWm t4Dp dbyu";
|
||||
|
||||
describe("AccessSecretStorageDialog", () => {
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
const defaultProps: ComponentProps<typeof AccessSecretStorageDialog> = {
|
||||
keyInfo: {} as any,
|
||||
onFinished: jest.fn(),
|
||||
checkPrivateKey: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}): void => {
|
||||
render(<AccessSecretStorageDialog {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
const enterSecurityKey = (placeholder = "Security Key"): void => {
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByPlaceholderText(placeholder), {
|
||||
target: {
|
||||
value: securityKey,
|
||||
},
|
||||
});
|
||||
// wait for debounce
|
||||
jest.advanceTimersByTime(250);
|
||||
});
|
||||
};
|
||||
|
||||
const submitDialog = async (): Promise<void> => {
|
||||
await userEvent.click(screen.getByText("Continue"), { delay: null });
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
mockPlatformPeg();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = stubClient();
|
||||
});
|
||||
|
||||
it("Closes the dialog when the form is submitted with a valid key", async () => {
|
||||
jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true);
|
||||
|
||||
const onFinished = jest.fn();
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||
renderComponent({ onFinished, checkPrivateKey });
|
||||
|
||||
// check that the input field is focused
|
||||
expect(screen.getByPlaceholderText("Security Key")).toHaveFocus();
|
||||
|
||||
await enterSecurityKey();
|
||||
await submitDialog();
|
||||
|
||||
expect(screen.getByText("Looks good!")).toBeInTheDocument();
|
||||
expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: securityKey });
|
||||
expect(onFinished).toHaveBeenCalledWith({ recoveryKey: securityKey });
|
||||
});
|
||||
|
||||
it("Notifies the user if they input an invalid Security Key", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||
renderComponent({ onFinished, checkPrivateKey });
|
||||
|
||||
jest.spyOn(mockClient.secretStorage, "checkKey").mockImplementation(() => {
|
||||
throw new Error("invalid key");
|
||||
});
|
||||
|
||||
await enterSecurityKey();
|
||||
await submitDialog();
|
||||
|
||||
expect(screen.getByText("Continue")).toBeDisabled();
|
||||
expect(screen.getByText("Invalid Security Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Notifies the user if they input an invalid passphrase", async function () {
|
||||
const keyInfo = {
|
||||
name: "test",
|
||||
algorithm: "test",
|
||||
iv: "test",
|
||||
mac: "1:2:3:4",
|
||||
passphrase: {
|
||||
// this type is weird in js-sdk
|
||||
// cast 'm.pbkdf2' to itself
|
||||
algorithm: "m.pbkdf2" as SecretStorage.PassphraseInfo["algorithm"],
|
||||
iterations: 2,
|
||||
salt: "nonempty",
|
||||
},
|
||||
};
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(false);
|
||||
renderComponent({ checkPrivateKey, keyInfo });
|
||||
|
||||
await enterSecurityKey("Security Phrase");
|
||||
expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey);
|
||||
await submitDialog();
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { AppDownloadDialog } from "../../../../src/components/views/dialogs/AppDownloadDialog";
|
||||
import SdkConfig, { ConfigOptions } from "../../../../src/SdkConfig";
|
||||
|
||||
describe("AppDownloadDialog", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it("should render with desktop, ios, android, fdroid buttons by default", () => {
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling fdroid build", () => {
|
||||
SdkConfig.add({
|
||||
mobile_builds: {
|
||||
fdroid: null,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling desktop build", () => {
|
||||
SdkConfig.add({
|
||||
desktop_builds: {
|
||||
available: false,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling mobile builds", () => {
|
||||
SdkConfig.add({
|
||||
mobile_builds: {
|
||||
ios: null,
|
||||
android: null,
|
||||
fdroid: null,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { getByText, render, RenderResult } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import AskInviteAnywayDialog, {
|
||||
AskInviteAnywayDialogProps,
|
||||
} from "../../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("AskInviteaAnywayDialog", () => {
|
||||
const onFinished: jest.Mock<any, any> = jest.fn();
|
||||
const onGiveUp: jest.Mock<any, any> = jest.fn();
|
||||
const onInviteAnyways: jest.Mock<any, any> = jest.fn();
|
||||
|
||||
function renderComponent(props: Partial<AskInviteAnywayDialogProps> = {}): RenderResult {
|
||||
return render(
|
||||
<AskInviteAnywayDialog
|
||||
onFinished={onFinished}
|
||||
onGiveUp={onGiveUp}
|
||||
onInviteAnyways={onInviteAnyways}
|
||||
unknownProfileUsers={[
|
||||
{
|
||||
userId: "@alice:localhost",
|
||||
errorText: "🤷♂️",
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("remembers to not warn again", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (): Promise<void> => {});
|
||||
|
||||
const neverWarnAgainBtn = getByText(container, /never warn/);
|
||||
await userEvent.click(neverWarnAgainBtn);
|
||||
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||
"promptBeforeInviteUnknownUsers",
|
||||
null,
|
||||
expect.any(String),
|
||||
false,
|
||||
);
|
||||
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("invites anyway", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
const inviteAnywayBtn = getByText(container, "Invite anyway");
|
||||
await userEvent.click(inviteAnywayBtn);
|
||||
|
||||
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("gives up", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
const closeBtn = getByText(container, /Close/);
|
||||
await userEvent.click(closeBtn);
|
||||
|
||||
expect(onGiveUp).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
|
||||
import ChangelogDialog from "../../../../src/components/views/dialogs/ChangelogDialog";
|
||||
|
||||
describe("<ChangelogDialog />", () => {
|
||||
it("should fetch github proxy url for each repo with old and new version strings", async () => {
|
||||
const webUrl = "https://riot.im/github/repos/element-hq/element-web/compare/oldsha1...newsha1";
|
||||
fetchMock.get(webUrl, {
|
||||
url: "https://api.github.com/repos/element-hq/element-web/compare/master...develop",
|
||||
html_url: "https://github.com/element-hq/element-web/compare/master...develop",
|
||||
permalink_url: "https://github.com/element-hq/element-web/compare/vector-im:72ca95e...vector-im:8891698",
|
||||
diff_url: "https://github.com/element-hq/element-web/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/element-hq/element-web/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 24,
|
||||
behind_by: 0,
|
||||
total_commits: 24,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha",
|
||||
html_url: "https://api.github.com/repos/element-hq/element-web/commit/commit-sha",
|
||||
commit: { message: "This is the first commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
const reactUrl = "https://riot.im/github/repos/element-hq/matrix-react-sdk/compare/oldsha2...newsha2";
|
||||
fetchMock.get(reactUrl, {
|
||||
url: "https://api.github.com/repos/element-hq/matrix-react-sdk/compare/master...develop",
|
||||
html_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop",
|
||||
permalink_url: "https://github.com/element-hq/matrix-react-sdk/compare/matrix-org:cdb00...matrix-org:4a926",
|
||||
diff_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 83,
|
||||
behind_by: 0,
|
||||
total_commits: 83,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha0",
|
||||
html_url: "https://api.github.com/repos/element-hq/matrix-react-sdk/commit/commit-sha",
|
||||
commit: { message: "This is a commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
const jsUrl = "https://riot.im/github/repos/matrix-org/matrix-js-sdk/compare/oldsha3...newsha3";
|
||||
fetchMock.get(jsUrl, {
|
||||
url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/compare/master...develop",
|
||||
html_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop",
|
||||
permalink_url: "https://github.com/matrix-org/matrix-js-sdk/compare/matrix-org:6166a8f...matrix-org:fec350",
|
||||
diff_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 48,
|
||||
behind_by: 0,
|
||||
total_commits: 48,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha1",
|
||||
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1",
|
||||
commit: { message: "This is a commit message" },
|
||||
},
|
||||
{
|
||||
sha: "commit-sha2",
|
||||
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2",
|
||||
commit: { message: "This is another commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
|
||||
const newVersion = "newsha1-react-newsha2-js-newsha3";
|
||||
const oldVersion = "oldsha1-react-oldsha2-js-oldsha3";
|
||||
const { asFragment } = render(
|
||||
<ChangelogDialog newVersion={newVersion} version={oldVersion} onFinished={jest.fn()} />,
|
||||
);
|
||||
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
|
||||
expect(fetchMock).toHaveFetched(webUrl);
|
||||
expect(fetchMock).toHaveFetched(reactUrl);
|
||||
expect(fetchMock).toHaveFetched(jsUrl);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
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 { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { flushPromises, mkEvent, stubClient } from "../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||
import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog";
|
||||
|
||||
describe("ConfirmRedactDialog", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let mxEvent: MatrixEvent;
|
||||
|
||||
const setUpVoiceBroadcastStartedEvent = () => {
|
||||
mxEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.deviceId!,
|
||||
);
|
||||
};
|
||||
|
||||
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
|
||||
createRedactEventDialog({ mxEvent });
|
||||
// double-flush promises required for the dialog to show up
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(screen.getByTestId("dialog-primary-button"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("should raise an error for an event without ID", async () => {
|
||||
mxEvent = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
jest.spyOn(mxEvent, "getId").mockReturnValue(undefined);
|
||||
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow("cannot redact event without ID");
|
||||
});
|
||||
|
||||
it("should raise an error for an event without room-ID", async () => {
|
||||
mxEvent = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
jest.spyOn(mxEvent, "getRoomId").mockReturnValue(undefined);
|
||||
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow(
|
||||
`cannot redact event ${mxEvent.getId()} without room ID`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when redacting a voice broadcast started event", () => {
|
||||
beforeEach(() => {
|
||||
setUpVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
describe("and the server does not support relation based redactions", () => {
|
||||
beforeEach(() => {
|
||||
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
describe("and displaying and confirm the dialog for a voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
await confirmDeleteVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
it("should call redact without `with_rel_types`", () => {
|
||||
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the server supports relation based redactions", () => {
|
||||
beforeEach(() => {
|
||||
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
|
||||
});
|
||||
|
||||
describe("and displaying and confirm the dialog for a voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
await confirmDeleteVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
it("should call redact with `with_rel_types`", () => {
|
||||
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
|
||||
with_rel_types: [RelationType.Reference],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import ConfirmUserActionDialog from "../../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
||||
import { mkRoomMember } from "../../../test-utils";
|
||||
|
||||
describe("ConfirmUserActionDialog", () => {
|
||||
it("renders", () => {
|
||||
const { asFragment } = render(
|
||||
<ConfirmUserActionDialog
|
||||
onFinished={jest.fn()}
|
||||
member={mkRoomMember("123", "@user:test.com", KnownMembership.Join)}
|
||||
action="Ban"
|
||||
title="Ban this " // eg. 'Ban this user?'
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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, screen, within } from "jest-matrix-react";
|
||||
import { JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import CreateRoomDialog from "../../../../src/components/views/dialogs/CreateRoomDialog";
|
||||
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<CreateRoomDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getDomain: jest.fn().mockReturnValue("server.org"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
doesServerForceEncryptionForPreset: jest.fn(),
|
||||
// make every alias available
|
||||
getRoomIdForAlias: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
});
|
||||
|
||||
const getE2eeEnableToggleInputElement = () => screen.getByLabelText("Enable end-to-end encryption");
|
||||
// labelled toggle switch doesn't set the disabled attribute, only aria-disabled
|
||||
const getE2eeEnableToggleIsDisabled = () =>
|
||||
getE2eeEnableToggleInputElement().getAttribute("aria-disabled") === "true";
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(false);
|
||||
mockClient.getClientWellKnown.mockReturnValue({});
|
||||
});
|
||||
|
||||
const getComponent = (props = {}) => render(<CreateRoomDialog onFinished={jest.fn()} {...props} />);
|
||||
|
||||
it("should default to private room", async () => {
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Create a private room")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should use defaultName from props", async () => {
|
||||
const defaultName = "My test room";
|
||||
getComponent({ defaultName });
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveDisplayValue(defaultName);
|
||||
});
|
||||
|
||||
describe("for a private room", () => {
|
||||
// default behaviour is a private room
|
||||
|
||||
it("should use server .well-known default for encryption setting", async () => {
|
||||
// default to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should use server .well-known force_disable for encryption setting", async () => {
|
||||
// force to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: true,
|
||||
force_disable: true,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should use defaultEncrypted prop", async () => {
|
||||
// default to off in server wk
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
// but pass defaultEncrypted prop
|
||||
getComponent({ defaultEncrypted: true });
|
||||
await flushPromises();
|
||||
// encryption enabled
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should use defaultEncrypted prop when it is false", async () => {
|
||||
// default to off in server wk
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
// but pass defaultEncrypted prop
|
||||
getComponent({ defaultEncrypted: false });
|
||||
await flushPromises();
|
||||
// encryption disabled
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
// not forced to off
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should override defaultEncrypted when server .well-known forces disabled encryption", async () => {
|
||||
// force to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: true,
|
||||
},
|
||||
});
|
||||
getComponent({ defaultEncrypted: true });
|
||||
await flushPromises();
|
||||
|
||||
// server forces encryption to disabled, even though defaultEncrypted is false
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should override defaultEncrypted when server forces enabled encryption", async () => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
|
||||
getComponent({ defaultEncrypted: false });
|
||||
await flushPromises();
|
||||
|
||||
// server forces encryption to enabled, even though defaultEncrypted is true
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should enable encryption toggle and disable field when server forces encryption", async () => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
|
||||
getComponent();
|
||||
|
||||
await flushPromises();
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
|
||||
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should warn when trying to create a room with an invalid form", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
// didn't submit room
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should create a private room", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
},
|
||||
encryption: true,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a knock room", () => {
|
||||
describe("when feature is disabled", () => {
|
||||
it("should not have the option to create a knock room", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
getComponent();
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when feature is enabled", () => {
|
||||
const onFinished = jest.fn();
|
||||
const roomName = "Test Room Name";
|
||||
|
||||
beforeEach(async () => {
|
||||
onFinished.mockReset();
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(setting) => setting === "feature_ask_to_join",
|
||||
);
|
||||
getComponent({ onFinished });
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
|
||||
});
|
||||
|
||||
it("should have a heading", () => {
|
||||
expect(screen.getByRole("heading")).toHaveTextContent("Create a room");
|
||||
});
|
||||
|
||||
it("should have a hint", () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should create a knock room with private visibility", async () => {
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
visibility: Visibility.Private,
|
||||
},
|
||||
encryption: true,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a knock room with public visibility", async () => {
|
||||
fireEvent.click(
|
||||
screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
visibility: Visibility.Public,
|
||||
},
|
||||
encryption: true,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a public room", () => {
|
||||
it("should set join rule to public defaultPublic is truthy", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ defaultPublic: true, onFinished });
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Create a public room")).toBeInTheDocument();
|
||||
|
||||
// e2e section is not rendered
|
||||
expect(screen.queryByText("Enable end-to-end encryption")).not.toBeInTheDocument();
|
||||
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
});
|
||||
|
||||
it("should not create a public room without an alias", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
// set to public
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
fireEvent.click(screen.getByText("Public room"));
|
||||
expect(within(screen.getByLabelText("Room visibility")).findByText("Public room")).toBeTruthy();
|
||||
expect(screen.getByText("Create a public room")).toBeInTheDocument();
|
||||
|
||||
// set name
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
// try to create the room
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
// alias field invalid
|
||||
expect(screen.getByLabelText("Room address").parentElement!).toHaveClass("mx_Field_invalid");
|
||||
|
||||
// didn't submit
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should create a public room", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished, defaultPublic: true });
|
||||
await flushPromises();
|
||||
|
||||
// set name
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
const roomAlias = "test";
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Room address"), { target: { value: roomAlias } });
|
||||
|
||||
// try to create the room
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
preset: Preset.PublicChat,
|
||||
room_alias_name: roomAlias,
|
||||
visibility: Visibility.Public,
|
||||
},
|
||||
guestAccess: false,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 { getByLabelText, getAllByLabelText, render } from "jest-matrix-react";
|
||||
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import DevtoolsDialog from "../../../../src/components/views/dialogs/DevtoolsDialog";
|
||||
|
||||
describe("DevtoolsDialog", () => {
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function getComponent(roomId: string, threadRootId: string | null = null, onFinished = () => true) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsDialog roomId={roomId} threadRootId={threadRootId} onFinished={onFinished} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
room = new Room("!id", cli, "@alice:matrix.org");
|
||||
|
||||
jest.spyOn(cli, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders the devtools dialog", () => {
|
||||
const { asFragment } = getComponent(room.roomId);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("copies the roomid", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(navigator.clipboard, "writeText");
|
||||
|
||||
const { container } = getComponent(room.roomId);
|
||||
|
||||
const copyBtn = getByLabelText(container, "Copy");
|
||||
await user.click(copyBtn);
|
||||
const copiedBtn = getByLabelText(container, "Copied!");
|
||||
|
||||
expect(copiedBtn).toBeInTheDocument();
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||
await expect(navigator.clipboard.readText()).resolves.toBe(room.roomId);
|
||||
});
|
||||
|
||||
it("copies the thread root id when provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(navigator.clipboard, "writeText");
|
||||
|
||||
const threadRootId = "$test_event_id_goes_here";
|
||||
const { container } = getComponent(room.roomId, threadRootId);
|
||||
|
||||
const copyBtn = getAllByLabelText(container, "Copy")[1];
|
||||
await user.click(copyBtn);
|
||||
const copiedBtn = getByLabelText(container, "Copied!");
|
||||
|
||||
expect(copiedBtn).toBeInTheDocument();
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||
await expect(navigator.clipboard.readText()).resolves.toBe(threadRootId);
|
||||
});
|
||||
});
|
326
test/unit-tests/components/views/dialogs/ExportDialog-test.tsx
Normal file
326
test/unit-tests/components/views/dialogs/ExportDialog-test.tsx
Normal file
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
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, RenderResult, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ExportDialog from "../../../../src/components/views/dialogs/ExportDialog";
|
||||
import { ExportType, ExportFormat } from "../../../../src/utils/exportUtils/exportUtils";
|
||||
import { createTestClient, mkStubRoom } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
|
||||
import ChatExport from "../../../../src/customisations/ChatExport";
|
||||
import PlainTextExporter from "../../../../src/utils/exportUtils/PlainTextExport";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const htmlExporterInstance = {
|
||||
export: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
const plainTextExporterInstance = {
|
||||
export: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
jest.mock("../../../../src/utils/exportUtils/HtmlExport", () => jest.fn());
|
||||
jest.mock("../../../../src/utils/exportUtils/PlainTextExport", () => jest.fn());
|
||||
|
||||
jest.mock("../../../../src/customisations/ChatExport", () => ({
|
||||
getForceChatExportParameters: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
const ChatExportMock = mocked(ChatExport);
|
||||
const HTMLExporterMock = mocked(HTMLExporter);
|
||||
const PlainTextExporterMock = mocked(PlainTextExporter);
|
||||
|
||||
describe("<ExportDialog />", () => {
|
||||
const mockClient = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
|
||||
const roomId = "test:test.org";
|
||||
const defaultProps = {
|
||||
room: mkStubRoom(roomId, "test", mockClient) as unknown as Room,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => render(<ExportDialog {...defaultProps} {...props} />);
|
||||
|
||||
const getSizeInput = ({ container }: RenderResult) => container.querySelector('input[id="size-limit"]')!;
|
||||
const getExportTypeInput = ({ container }: RenderResult) => container.querySelector('select[id="export-type"]')!;
|
||||
const getAttachmentsCheckbox = ({ container }: RenderResult) =>
|
||||
container.querySelector('input[id="include-attachments"]')!;
|
||||
const getMessageCountInput = ({ container }: RenderResult) => container.querySelector('input[id="message-count"]')!;
|
||||
const getExportFormatInput = ({ container }: RenderResult, format: ExportFormat) =>
|
||||
container.querySelector(`input[id="exportFormat-${format}"]`)!;
|
||||
const getPrimaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-primary-button")!;
|
||||
const getSecondaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-cancel-button")!;
|
||||
|
||||
const submitForm = async (component: RenderResult) => fireEvent.click(getPrimaryButton(component));
|
||||
const selectExportFormat = async (component: RenderResult, format: ExportFormat) =>
|
||||
fireEvent.click(getExportFormatInput(component, format));
|
||||
const selectExportType = async (component: RenderResult, type: ExportType) =>
|
||||
fireEvent.change(getExportTypeInput(component), { target: { value: type } });
|
||||
const setMessageCount = async (component: RenderResult, count: number) =>
|
||||
fireEvent.change(getMessageCountInput(component), { target: { value: count } });
|
||||
|
||||
const setSizeLimit = async (component: RenderResult, limit: number) =>
|
||||
fireEvent.change(getSizeInput(component), { target: { value: limit } });
|
||||
|
||||
beforeEach(() => {
|
||||
HTMLExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(htmlExporterInstance));
|
||||
PlainTextExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(plainTextExporterInstance));
|
||||
htmlExporterInstance.export.mockClear();
|
||||
plainTextExporterInstance.export.mockClear();
|
||||
|
||||
// default setting value
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockClear().mockReturnValue({});
|
||||
});
|
||||
|
||||
it("renders export dialog", () => {
|
||||
const component = getComponent();
|
||||
expect(component.container.querySelector(".mx_ExportDialog")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onFinished when cancel button is clicked", () => {
|
||||
const onFinished = jest.fn();
|
||||
const component = getComponent({ onFinished });
|
||||
fireEvent.click(getSecondaryButton(component));
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("exports room on submit", async () => {
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
// 4th arg is an component function
|
||||
const exportConstructorProps = HTMLExporterMock.mock.calls[0].slice(0, 3);
|
||||
expect(exportConstructorProps).toEqual([
|
||||
defaultProps.room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 8388608, // 8MB to bytes
|
||||
numberOfMessages: 100,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports room using values set from ForceRoomExportParameters", async () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
format: ExportFormat.PlainText,
|
||||
range: ExportType.Beginning,
|
||||
sizeMb: 7000,
|
||||
numberOfMessages: 30,
|
||||
includeAttachments: true,
|
||||
});
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
// 4th arg is an component function
|
||||
const exportConstructorProps = PlainTextExporterMock.mock.calls[0].slice(0, 3);
|
||||
expect(exportConstructorProps).toEqual([
|
||||
defaultProps.room,
|
||||
ExportType.Beginning,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 7000 * 1024 * 1024,
|
||||
numberOfMessages: 30,
|
||||
},
|
||||
]);
|
||||
expect(plainTextExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders success screen when export is finished", async () => {
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(component.container.querySelector(".mx_InfoDialog .mx_Dialog_content")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("export format", () => {
|
||||
it("renders export format with html selected by default", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
|
||||
});
|
||||
|
||||
it("sets export format on radio button click", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportFormat(component, ExportFormat.PlainText);
|
||||
expect(getExportFormatInput(component, ExportFormat.PlainText)).toBeChecked();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("hides export format input when format is valid in ForceRoomExportParameters", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not render export format when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
format: ExportFormat.PlainText,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("export type", () => {
|
||||
it("renders export type with timeline selected by default", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportTypeInput(component)).toHaveValue(ExportType.Timeline);
|
||||
});
|
||||
|
||||
it("sets export type on change", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.Beginning);
|
||||
expect(getExportTypeInput(component)).toHaveValue(ExportType.Beginning);
|
||||
});
|
||||
|
||||
it("does not render export type when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
range: ExportType.Beginning,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getExportTypeInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not render message count input", async () => {
|
||||
const component = getComponent();
|
||||
expect(getMessageCountInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders message count input with default value 100 when export type is lastNMessages", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
expect(getMessageCountInput(component)).toHaveValue(100);
|
||||
});
|
||||
|
||||
it("sets message count on change", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 10);
|
||||
expect(getMessageCountInput(component)).toHaveValue(10);
|
||||
});
|
||||
|
||||
it("does not export when export type is lastNMessages and message count is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 0);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not export when export type is lastNMessages and message count is more than max", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 99999999999);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports when export type is NOT lastNMessages and message count is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 0);
|
||||
await selectExportType(component, ExportType.Timeline);
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("size limit", () => {
|
||||
it("renders size limit input with default value", () => {
|
||||
const component = getComponent();
|
||||
expect(getSizeInput(component)).toHaveValue(8);
|
||||
});
|
||||
|
||||
it("updates size limit on change", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 20);
|
||||
expect(getSizeInput(component)).toHaveValue(20);
|
||||
});
|
||||
|
||||
it("does not export when size limit is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 0);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not export when size limit is larger than max", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 2001);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports when size limit is max", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 2000);
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render size limit input when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
sizeMb: 10000,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getSizeInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
/**
|
||||
* 2000mb size limit does not apply when higher limit is configured in config
|
||||
*/
|
||||
it("exports when size limit set in ForceRoomExportParameters is larger than 2000", async () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
sizeMb: 10000,
|
||||
});
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("include attachments", () => {
|
||||
it("renders input with default value of false", () => {
|
||||
const component = getComponent();
|
||||
expect(getAttachmentsCheckbox(component)).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("updates include attachments on change", async () => {
|
||||
const component = getComponent();
|
||||
fireEvent.click(getAttachmentsCheckbox(component));
|
||||
expect(getAttachmentsCheckbox(component)).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not render input when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
includeAttachments: false,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getAttachmentsCheckbox(component)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 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 SdkConfig from "../../../../src/SdkConfig";
|
||||
import FeedbackDialog from "../../../../src/components/views/dialogs/FeedbackDialog";
|
||||
|
||||
describe("FeedbackDialog", () => {
|
||||
it("should respect feedback config", () => {
|
||||
SdkConfig.put({
|
||||
feedback: {
|
||||
existing_issues_url: "http://existing?foo=bar",
|
||||
new_issue_url: "https://new.issue.url?foo=bar",
|
||||
},
|
||||
});
|
||||
|
||||
const { asFragment } = render(<FeedbackDialog onFinished={jest.fn()} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue