Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
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>
|
||||
`;
|
||||
Reference in New Issue
Block a user