Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0f670b8dc0
commit
b084ff2313
807 changed files with 0 additions and 0 deletions
27
test/unit-tests/components/views/Validation-test.ts
Normal file
27
test/unit-tests/components/views/Validation-test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import withValidation from "../../../src/components/views/elements/Validation";
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should handle 0 rules", () => {
|
||||
const handler = withValidation({
|
||||
rules: [],
|
||||
});
|
||||
return expect(
|
||||
handler({
|
||||
value: "value",
|
||||
focused: true,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
valid: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EmojiMapping } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { tEmoji } from "../../../src/components/views/verification/VerificationShowSas";
|
||||
|
||||
describe("tEmoji", () => {
|
||||
it.each([
|
||||
["en-GB", "Dog"],
|
||||
["en", "Dog"],
|
||||
["de-DE", "Hund"],
|
||||
["pt", "Cachorro"],
|
||||
])("should handle locale %s", (locale, expectation) => {
|
||||
const emoji: EmojiMapping = ["🐶", "Dog"];
|
||||
expect(tEmoji(emoji, locale)).toEqual(expectation);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import RecordingPlayback, { PlaybackLayout } from "../../../../src/components/views/audio_messages/RecordingPlayback";
|
||||
import { Playback } from "../../../../src/audio/Playback";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { createAudioContext } from "../../../../src/audio/compat";
|
||||
import { flushPromises } from "../../../test-utils";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
|
||||
jest.mock("../../../../src/WorkerManager", () => ({
|
||||
WorkerManager: jest.fn(() => ({
|
||||
call: jest.fn().mockResolvedValue({ waveform: [0, 0, 1, 1] }),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/audio/compat", () => ({
|
||||
createAudioContext: jest.fn(),
|
||||
decodeOgg: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
describe("<RecordingPlayback />", () => {
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
currentTime: 0,
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
};
|
||||
|
||||
const mockAudioBuffer = {
|
||||
duration: 99,
|
||||
getChannelData: jest.fn(),
|
||||
};
|
||||
|
||||
const mockChannelData = new Float32Array();
|
||||
|
||||
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
|
||||
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
|
||||
render(
|
||||
<RoomContext.Provider value={room}>
|
||||
<RecordingPlayback {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData);
|
||||
mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer));
|
||||
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
|
||||
});
|
||||
|
||||
const getPlayButton = (component: RenderResult) => component.getByTestId("play-pause-button");
|
||||
|
||||
it("renders recording playback", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables play button while playback is decoding", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
expect(getPlayButton(component)).toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("enables play button when playback is finished decoding", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
await flushPromises();
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("displays error when playback decoding fails", async () => {
|
||||
// stub logger to keep console clean from expected error
|
||||
jest.spyOn(logger, "error").mockReturnValue(undefined);
|
||||
jest.spyOn(logger, "warn").mockReturnValue(undefined);
|
||||
mockAudioContext.decodeAudioData.mockImplementation((_b, _cb, error) => error(new Error("oh no")));
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback });
|
||||
await flushPromises();
|
||||
expect(component.container.querySelector(".text-warning")).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays pre-prepared playback with correct playback phase", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
await playback.prepare();
|
||||
const component = getComponent({ playback });
|
||||
// playback already decoded, button is not disabled
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
|
||||
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
|
||||
expect(component.container.querySelector(".text-warning")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("toggles playback on play pause button click", async () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
jest.spyOn(playback, "toggle").mockResolvedValue(undefined);
|
||||
await playback.prepare();
|
||||
const component = getComponent({ playback });
|
||||
|
||||
fireEvent.click(getPlayButton(component));
|
||||
|
||||
expect(playback.toggle).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Composer Layout", () => {
|
||||
it("should have a waveform, no seek bar, and clock", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback, layout: PlaybackLayout.Composer });
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Timeline Layout", () => {
|
||||
it("should have a waveform, a seek bar, and clock", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should be the default", () => {
|
||||
const playback = new Playback(new ArrayBuffer(8));
|
||||
const component = getComponent({ playback }); // no layout set for test
|
||||
|
||||
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
|
||||
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
125
test/unit-tests/components/views/audio_messages/SeekBar-test.tsx
Normal file
125
test/unit-tests/components/views/audio_messages/SeekBar-test.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef, RefObject } from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import { Playback } from "../../../../src/audio/Playback";
|
||||
import { createTestPlayback } from "../../../test-utils/audio";
|
||||
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
|
||||
|
||||
describe("SeekBar", () => {
|
||||
let playback: Playback;
|
||||
let renderResult: RenderResult;
|
||||
let frameRequestCallback: FrameRequestCallback;
|
||||
let seekBarRef: RefObject<SeekBar>;
|
||||
|
||||
beforeEach(() => {
|
||||
seekBarRef = createRef();
|
||||
jest.spyOn(window, "requestAnimationFrame").mockImplementation((callback: FrameRequestCallback) => {
|
||||
frameRequestCallback = callback;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(window.requestAnimationFrame).mockRestore();
|
||||
});
|
||||
|
||||
describe("when rendering a SeekBar for an empty playback", () => {
|
||||
beforeEach(() => {
|
||||
playback = createTestPlayback({
|
||||
durationSeconds: 0,
|
||||
timeSeconds: 0,
|
||||
});
|
||||
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render correctly", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a SeekBar", () => {
|
||||
beforeEach(() => {
|
||||
playback = createTestPlayback();
|
||||
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render the initial position", () => {
|
||||
// expected value 3141 / 31415 ~ 0.099984084
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and the playback proceeds", () => {
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore
|
||||
playback.timeSeconds = 6969;
|
||||
act(() => {
|
||||
playback.liveData.update([playback.timeSeconds, playback.durationSeconds]);
|
||||
frameRequestCallback(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
// expected value 6969 / 31415 ~ 0.221836702
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and seeking position with the slider", () => {
|
||||
beforeEach(() => {
|
||||
const rangeInput = renderResult.container.querySelector("[type='range']");
|
||||
act(() => {
|
||||
fireEvent.change(rangeInput!, { target: { value: 0.5 } });
|
||||
});
|
||||
});
|
||||
|
||||
it("should update the playback", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(0.5 * playback.durationSeconds);
|
||||
});
|
||||
|
||||
describe("and seeking left", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.skipTo).mockClear();
|
||||
act(() => {
|
||||
seekBarRef.current!.left();
|
||||
});
|
||||
});
|
||||
|
||||
it("should skip to minus 5 seconds", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds - 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and seeking right", () => {
|
||||
beforeEach(() => {
|
||||
mocked(playback.skipTo).mockClear();
|
||||
act(() => {
|
||||
seekBarRef.current!.right();
|
||||
});
|
||||
});
|
||||
|
||||
it("should skip to plus 5 seconds", () => {
|
||||
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds + 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rendering a disabled SeekBar", () => {
|
||||
beforeEach(async () => {
|
||||
renderResult = render(<SeekBar disabled={true} playback={playback} />);
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar and the playback proceeds should render as expected 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.22183670221231896;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.22183670221231896"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar for an empty playback should render correctly 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a SeekBar should render the initial position 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.0999840840362884;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.0999840840362884"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SeekBar when rendering a disabled SeekBar should render as expected 1`] = `
|
||||
<div>
|
||||
<input
|
||||
aria-label="Audio seek bar"
|
||||
class="mx_SeekBar"
|
||||
disabled=""
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0.0999840840362884;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0.0999840840362884"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import CountryDropdown from "../../../../src/components/views/auth/CountryDropdown";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
|
||||
describe("CountryDropdown", () => {
|
||||
describe("default_country_code", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["GB", 44],
|
||||
["IE", 353],
|
||||
["ES", 34],
|
||||
["FR", 33],
|
||||
["PL", 48],
|
||||
["DE", 49],
|
||||
])("should respect configured default country code for %s", (config, defaultCountryCode) => {
|
||||
SdkConfig.add({
|
||||
default_country_code: config,
|
||||
});
|
||||
|
||||
const fn = jest.fn();
|
||||
render(<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />);
|
||||
expect(fn).toHaveBeenCalledWith(expect.objectContaining({ prefix: defaultCountryCode.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultCountry", () => {
|
||||
it.each([
|
||||
["en-GB", 44],
|
||||
["en-ie", 353],
|
||||
["es-ES", 34],
|
||||
["fr", 33],
|
||||
["pl", 48],
|
||||
["de-DE", 49],
|
||||
])("should pick appropriate default country for %s", (language, defaultCountryCode) => {
|
||||
Object.defineProperty(navigator, "language", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return language;
|
||||
},
|
||||
});
|
||||
|
||||
const fn = jest.fn();
|
||||
render(<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />);
|
||||
expect(fn).toHaveBeenCalledWith(expect.objectContaining({ prefix: defaultCountryCode.toString() }));
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow filtering", async () => {
|
||||
const fn = jest.fn();
|
||||
const { getByRole, findByText } = render(
|
||||
<CountryDropdown onOptionChange={fn} isSmall={false} showPrefix={false} />,
|
||||
);
|
||||
|
||||
const dropdown = getByRole("button");
|
||||
fireEvent.click(dropdown);
|
||||
|
||||
await userEvent.keyboard("Al");
|
||||
|
||||
await expect(findByText("Albania (+355)")).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, waitFor, act, fireEvent } from "jest-matrix-react";
|
||||
import { AuthType } from "matrix-js-sdk/src/interactive-auth";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {
|
||||
EmailIdentityAuthEntry,
|
||||
MasUnlockCrossSigningAuthEntry,
|
||||
} from "../../../../src/components/views/auth/InteractiveAuthEntryComponents";
|
||||
import { createTestClient } from "../../../test-utils";
|
||||
|
||||
describe("<EmailIdentityAuthEntry/>", () => {
|
||||
const renderIdentityAuth = () => {
|
||||
const matrixClient = createTestClient();
|
||||
|
||||
return render(
|
||||
<EmailIdentityAuthEntry
|
||||
matrixClient={matrixClient}
|
||||
loginType={AuthType.Email}
|
||||
onPhaseChange={jest.fn()}
|
||||
submitAuthDict={jest.fn()}
|
||||
fail={jest.fn()}
|
||||
clientSecret="my secret"
|
||||
showContinue={true}
|
||||
inputs={{ emailAddress: "alice@example.xyz" }}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
test("should render", () => {
|
||||
const { container } = renderIdentityAuth();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should clear the requested state when the button tooltip is hidden", async () => {
|
||||
renderIdentityAuth();
|
||||
|
||||
// After a click on the resend button, the button should display the resent label
|
||||
screen.getByRole("button", { name: "Resend" }).click();
|
||||
await waitFor(() => expect(screen.queryByRole("button", { name: "Resent!" })).toBeInTheDocument());
|
||||
expect(screen.queryByRole("button", { name: "Resend" })).toBeNull();
|
||||
|
||||
const resentButton = screen.getByRole("button", { name: "Resent!" });
|
||||
// Hover briefly the button and wait for the tooltip to be displayed
|
||||
await userEvent.hover(resentButton);
|
||||
await waitFor(() => expect(screen.getByRole("tooltip", { name: "Resent!" })).toBeInTheDocument());
|
||||
|
||||
// On unhover, it should display again the resend button
|
||||
await act(() => userEvent.unhover(resentButton));
|
||||
await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
describe("<MasUnlockCrossSigningAuthEntry/>", () => {
|
||||
const renderAuth = (props = {}) => {
|
||||
const matrixClient = createTestClient();
|
||||
|
||||
return render(
|
||||
<MasUnlockCrossSigningAuthEntry
|
||||
matrixClient={matrixClient}
|
||||
loginType={AuthType.Email}
|
||||
onPhaseChange={jest.fn()}
|
||||
submitAuthDict={jest.fn()}
|
||||
fail={jest.fn()}
|
||||
clientSecret="my secret"
|
||||
showContinue={true}
|
||||
stageParams={{ url: "https://example.com" }}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
test("should render", () => {
|
||||
const { container } = renderAuth();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should open idp in new tab on click", async () => {
|
||||
const spy = jest.spyOn(global.window, "open");
|
||||
renderAuth();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Go to your account" }));
|
||||
expect(spy).toHaveBeenCalledWith("https://example.com", "_blank");
|
||||
});
|
||||
|
||||
test("should retry uia request on click", async () => {
|
||||
const submitAuthDict = jest.fn();
|
||||
renderAuth({ submitAuthDict });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||||
expect(submitAuthDict).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 Callum Brown
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import InteractiveAuthComponent from "../../../../src/components/structures/InteractiveAuth";
|
||||
import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
|
||||
|
||||
describe("InteractiveAuthComponent", function () {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
matrixClient: mockClient,
|
||||
makeRequest: jest.fn().mockResolvedValue(undefined),
|
||||
onAuthFinished: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => render(<InteractiveAuthComponent {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockClientPeg();
|
||||
});
|
||||
|
||||
const getSubmitButton = ({ container }: RenderResult) =>
|
||||
container.querySelector(".mx_AccessibleButton_kind_primary");
|
||||
const getRegistrationTokenInput = ({ container }: RenderResult) =>
|
||||
container.querySelector('input[name="registrationTokenField"]');
|
||||
|
||||
it("Should successfully complete a registration token flow", async () => {
|
||||
const onAuthFinished = jest.fn();
|
||||
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
|
||||
|
||||
const authData = {
|
||||
session: "sess",
|
||||
flows: [{ stages: ["m.login.registration_token"] }],
|
||||
};
|
||||
|
||||
const wrapper = getComponent({ makeRequest, onAuthFinished, authData });
|
||||
|
||||
const registrationTokenNode = getRegistrationTokenInput(wrapper);
|
||||
const submitNode = getSubmitButton(wrapper);
|
||||
const formNode = wrapper.container.querySelector("form");
|
||||
|
||||
expect(registrationTokenNode).toBeTruthy();
|
||||
expect(submitNode).toBeTruthy();
|
||||
expect(formNode).toBeTruthy();
|
||||
|
||||
// submit should be disabled
|
||||
expect(submitNode).toHaveAttribute("disabled");
|
||||
expect(submitNode).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
// put something in the registration token box
|
||||
fireEvent.change(registrationTokenNode!, { target: { value: "s3kr3t" } });
|
||||
|
||||
expect(getRegistrationTokenInput(wrapper)).toHaveValue("s3kr3t");
|
||||
expect(submitNode).not.toHaveAttribute("disabled");
|
||||
expect(submitNode).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
// hit enter; that should trigger a request
|
||||
fireEvent.submit(formNode!);
|
||||
|
||||
// wait for auth request to resolve
|
||||
await flushPromises();
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledTimes(1);
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
session: "sess",
|
||||
type: "m.login.registration_token",
|
||||
token: "s3kr3t",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onAuthFinished).toHaveBeenCalledTimes(1);
|
||||
expect(onAuthFinished).toHaveBeenCalledWith(
|
||||
true,
|
||||
{ a: 1 },
|
||||
{ clientSecret: "t35tcl1Ent5ECr3T", emailSid: undefined },
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EmailIdentityAuthEntry/> should render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_InteractiveAuthEntryComponents_emailWrapper"
|
||||
>
|
||||
<p>
|
||||
<span>
|
||||
To create your account, open the link in the email we just sent to
|
||||
<strong>
|
||||
alice@example.xyz
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="secondary"
|
||||
>
|
||||
<span>
|
||||
Did not receive it?
|
||||
<div
|
||||
aria-label="Resend"
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend it
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||
>
|
||||
Reset your identity through your account provider and then come back and click “Retry”.
|
||||
</p>
|
||||
<div
|
||||
class="mx_Flex"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton _has-icon_i91xf_66"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2Z"
|
||||
/>
|
||||
</svg>
|
||||
Go to your account
|
||||
</button>
|
||||
<button
|
||||
class="_button_i91xf_17 mx_Dialog_nonDialogButton"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { JoinRule, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import DecoratedRoomAvatar from "../../../../src/components/views/avatars/DecoratedRoomAvatar";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
|
||||
jest.mock("../../../../src/utils/presence", () => ({ isPresenceEnabled: jest.fn().mockReturnValue(true) }));
|
||||
|
||||
jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([0, 1]),
|
||||
}));
|
||||
|
||||
describe("DecoratedRoomAvatar", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function renderComponent() {
|
||||
return render(<DecoratedRoomAvatar room={room} size="32px" />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows an avatar with globe icon and tooltip for public room", async () => {
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
room.getJoinRule = jest.fn().mockReturnValue(JoinRule.Public);
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const globe = container.querySelector(".mx_DecoratedRoomAvatar_icon_globe")!;
|
||||
expect(globe).toBeVisible();
|
||||
await userEvent.hover(globe!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(globe.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent("This room is public");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows the presence indicator in a DM room that also has functional members", async () => {
|
||||
const DM_USER_ID = "@bob:foo.bar";
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: () => {
|
||||
return DM_USER_ID;
|
||||
},
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
jest.spyOn(DecoratedRoomAvatar.prototype as any, "getPresenceIcon").mockImplementation(() => "ONLINE");
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const presence = container.querySelector(".mx_DecoratedRoomAvatar_icon")!;
|
||||
expect(presence).toBeVisible();
|
||||
await userEvent.hover(presence!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent("Online");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { getByTestId, render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
|
||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { getRoomContext } from "../../../test-utils/room";
|
||||
import { stubClient } from "../../../test-utils/test-utils";
|
||||
|
||||
describe("MemberAvatar", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
let member: RoomMember;
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof MemberAvatar>>) {
|
||||
return (
|
||||
<RoomContext.Provider value={getRoomContext(room, {})}>
|
||||
<MemberAvatar member={null} size="35px" {...props} />
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
member = new RoomMember(ROOM_ID, "@bob:example.org");
|
||||
jest.spyOn(room, "getMember").mockReturnValue(member);
|
||||
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
|
||||
});
|
||||
|
||||
it("shows an avatar for useOnlyCurrentProfiles", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
return settingName === "useOnlyCurrentProfiles";
|
||||
});
|
||||
|
||||
const { container } = render(getComponent({}));
|
||||
|
||||
let avatar: HTMLElement;
|
||||
await waitFor(() => {
|
||||
avatar = getByTestId(container, "avatar-img");
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(avatar!.getAttribute("src")).not.toBe("");
|
||||
});
|
||||
});
|
67
test/unit-tests/components/views/avatars/RoomAvatar-test.tsx
Normal file
67
test/unit-tests/components/views/avatars/RoomAvatar-test.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import RoomAvatar from "../../../../src/components/views/avatars/RoomAvatar";
|
||||
import { filterConsole, stubClient } from "../../../test-utils";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||
import * as AvatarModule from "../../../../src/Avatar";
|
||||
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||
|
||||
describe("RoomAvatar", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
filterConsole(
|
||||
// unrelated for this test
|
||||
"Room !room:example.com does not have an m.room.create event",
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
|
||||
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
|
||||
});
|
||||
|
||||
it("should render as expected for a Room", () => {
|
||||
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||
room.name = "test room";
|
||||
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render as expected for a DM room", () => {
|
||||
const userId = "@dm_user@example.com";
|
||||
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||
room.name = "DM room";
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
|
||||
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render as expected for a LocalRoom", () => {
|
||||
const userId = "@local_room_user@example.com";
|
||||
const localRoom = new LocalRoom("!room:example.com", client, client.getSafeUserId());
|
||||
localRoom.name = "local test room";
|
||||
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
|
||||
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, Room, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import WithPresenceIndicator from "../../../../src/components/views/avatars/WithPresenceIndicator";
|
||||
import { isPresenceEnabled } from "../../../../src/utils/presence";
|
||||
|
||||
jest.mock("../../../../src/utils/presence");
|
||||
|
||||
jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]),
|
||||
}));
|
||||
|
||||
describe("WithPresenceIndicator", () => {
|
||||
const ROOM_ID = "roomId";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<WithPresenceIndicator room={room} size="32px">
|
||||
<span />
|
||||
</WithPresenceIndicator>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders only child if presence is disabled", async () => {
|
||||
mocked(isPresenceEnabled).mockReturnValue(false);
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.children).toHaveLength(1);
|
||||
expect(container.children[0].tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["online", "Online"],
|
||||
["offline", "Offline"],
|
||||
["unavailable", "Away"],
|
||||
])("renders presence indicator with tooltip for DM rooms", async (presenceStr, renderedStr) => {
|
||||
mocked(isPresenceEnabled).mockReturnValue(true);
|
||||
const DM_USER_ID = "@bob:foo.bar";
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: () => {
|
||||
return DM_USER_ID;
|
||||
},
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
room.getMember = jest.fn((userId) => {
|
||||
const member = new RoomMember(room.roomId, userId);
|
||||
member.user = new User(userId);
|
||||
member.user.presence = presenceStr;
|
||||
return member;
|
||||
});
|
||||
|
||||
const { container, asFragment } = renderComponent();
|
||||
|
||||
const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!;
|
||||
expect(presence).toBeVisible();
|
||||
await userEvent.hover(presence!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(presence.getAttribute("aria-labelledby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent(renderedStr);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DecoratedRoomAvatar shows an avatar with globe icon and tooltip for public room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar mx_DecoratedRoomAvatar_cutout"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<div
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_globe"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`DecoratedRoomAvatar shows the presence indicator in a DM room that also has functional members 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar mx_DecoratedRoomAvatar_cutout"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<div
|
||||
aria-labelledby="floating-ui-6"
|
||||
class="mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_online"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomAvatar should render as expected for a DM room 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
D
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
l
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomAvatar should render as expected for a Room 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_online"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-6"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_offline"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 3`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_WithPresenceIndicator"
|
||||
>
|
||||
<span />
|
||||
<div
|
||||
aria-labelledby="floating-ui-12"
|
||||
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_away"
|
||||
style="width: 32px; height: 32px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
183
test/unit-tests/components/views/beacon/BeaconListItem-test.tsx
Normal file
183
test/unit-tests/components/views/beacon/BeaconListItem-test.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
import { Beacon, RoomMember, MatrixEvent, LocationAssetType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import BeaconListItem from "../../../../src/components/views/beacon/BeaconListItem";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
} from "../../../test-utils";
|
||||
|
||||
describe("<BeaconListItem />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
// go back in time to create beacons and locations in the past
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now - 600000);
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alice:server";
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getRoom: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
});
|
||||
|
||||
const aliceBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
const alicePinBeaconEvent = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
{ isLive: true, assetType: LocationAssetType.Pin, description: "Alice's car" },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
const pinBeaconWithoutDescription = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
{ isLive: true, assetType: LocationAssetType.Pin },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
|
||||
const aliceLocation1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: aliceBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
timestamp: now - 1,
|
||||
});
|
||||
const aliceLocation2 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: aliceBeaconEvent.getId(),
|
||||
geoUri: "geo:52,42",
|
||||
timestamp: now - 500000,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
beacon: new Beacon(aliceBeaconEvent),
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<BeaconListItem {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => {
|
||||
const beacons = makeRoomWithBeacons(roomId, mockClient, beaconInfoEvents, locationEvents);
|
||||
|
||||
const member = new RoomMember(roomId, aliceId);
|
||||
member.name = `Alice`;
|
||||
const room = mockClient.getRoom(roomId)!;
|
||||
jest.spyOn(room, "getMember").mockReturnValue(member);
|
||||
|
||||
return beacons;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
it("renders null when beacon is not live", () => {
|
||||
const notLiveBeacon = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const [beacon] = setupRoomWithBeacons([notLiveBeacon]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders null when beacon has no location", () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("when a beacon is live and has locations", () => {
|
||||
it("renders beacon info", () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const { asFragment } = getComponent({ beacon });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("non-self beacons", () => {
|
||||
it("uses beacon description as beacon name", () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent("Alice's car");
|
||||
});
|
||||
|
||||
it("uses beacon owner mxid as beacon name for a beacon without description", () => {
|
||||
const [beacon] = setupRoomWithBeacons([pinBeaconWithoutDescription], [aliceLocation1]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent(aliceId);
|
||||
});
|
||||
|
||||
it("renders location icon", () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector(".mx_StyledLiveBeaconIcon")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("self locations", () => {
|
||||
it("renders beacon owner avatar", () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector(".mx_BaseAvatar")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses beacon owner name as beacon name", () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector(".mx_BeaconStatus_label")).toHaveTextContent("Alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("on location updates", () => {
|
||||
it("updates last updated time on location updated", () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation2]);
|
||||
const { container } = getComponent({ beacon });
|
||||
|
||||
expect(container.querySelector(".mx_BeaconListItem_lastUpdated")).toHaveTextContent(
|
||||
"Updated 9 minutes ago",
|
||||
);
|
||||
|
||||
// update to a newer location
|
||||
act(() => {
|
||||
beacon.addLocations([aliceLocation1]);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".mx_BeaconListItem_lastUpdated")).toHaveTextContent(
|
||||
"Updated a few seconds ago",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("interactions", () => {
|
||||
it("does not call onClick handler when clicking share button", () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const { getByTestId } = getComponent({ beacon, onClick });
|
||||
|
||||
fireEvent.click(getByTestId("open-location-in-osm"));
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onClick handler when clicking outside of share buttons", () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({ beacon, onClick });
|
||||
|
||||
// click the beacon name
|
||||
fireEvent.click(container.querySelector(".mx_BeaconStatus_description")!);
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
136
test/unit-tests/components/views/beacon/BeaconMarker-test.tsx
Normal file
136
test/unit-tests/components/views/beacon/BeaconMarker-test.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, screen, waitFor } from "jest-matrix-react";
|
||||
import * as maplibregl from "maplibre-gl";
|
||||
import { Beacon, Room, RoomMember, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import BeaconMarker from "../../../../src/components/views/beacon/BeaconMarker";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithStateEvents,
|
||||
} from "../../../test-utils";
|
||||
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
|
||||
|
||||
describe("<BeaconMarker />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
// stable date for snapshots
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alice:server";
|
||||
|
||||
const aliceMember = new RoomMember(roomId, aliceId);
|
||||
|
||||
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
|
||||
const mockMap = new maplibregl.Map(mapOptions);
|
||||
const mockMarker = new maplibregl.Marker();
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn().mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
}),
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getRoom: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
});
|
||||
|
||||
// make fresh rooms every time
|
||||
// as we update room state
|
||||
const setupRoom = (stateEvents: MatrixEvent[] = []): Room => {
|
||||
const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient });
|
||||
jest.spyOn(room1, "getMember").mockReturnValue(aliceMember);
|
||||
return room1;
|
||||
};
|
||||
|
||||
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
const notLiveEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-2");
|
||||
|
||||
const geoUri1 = "geo:51,41";
|
||||
const location1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: defaultEvent.getId(),
|
||||
geoUri: geoUri1,
|
||||
timestamp: now + 1,
|
||||
});
|
||||
const geoUri2 = "geo:52,42";
|
||||
const location2 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: defaultEvent.getId(),
|
||||
geoUri: geoUri2,
|
||||
timestamp: now + 10000,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
map: mockMap,
|
||||
beacon: new Beacon(defaultEvent),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const Wrapper = (wrapperProps = {}) => {
|
||||
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
|
||||
};
|
||||
|
||||
return render(<BeaconMarker {...defaultProps} {...props} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders nothing when beacon is not live", () => {
|
||||
const room = setupRoom([notLiveEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent));
|
||||
const { asFragment } = renderComponent({ beacon });
|
||||
expect(asFragment()).toMatchInlineSnapshot(`<DocumentFragment />`);
|
||||
expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing when beacon has no location", () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
|
||||
const { asFragment } = renderComponent({ beacon });
|
||||
expect(asFragment()).toMatchInlineSnapshot(`<DocumentFragment />`);
|
||||
expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders marker when beacon has location", async () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
|
||||
beacon?.addLocations([location1]);
|
||||
const { asFragment } = renderComponent({ beacon });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("avatar-img")).toBeInTheDocument();
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("updates with new locations", () => {
|
||||
const lonLat1 = { lon: 41, lat: 51 };
|
||||
const lonLat2 = { lon: 42, lat: 52 };
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
|
||||
beacon?.addLocations([location1]);
|
||||
|
||||
// render the component then add a new location, check mockMarker called as expected
|
||||
renderComponent({ beacon });
|
||||
expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat1);
|
||||
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
|
||||
|
||||
// add a location, check mockMarker called with new location details
|
||||
act(() => {
|
||||
beacon?.addLocations([location2]);
|
||||
});
|
||||
expect(mockMarker.setLngLat).toHaveBeenLastCalledWith(lonLat2);
|
||||
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { Beacon } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import BeaconStatus from "../../../../src/components/views/beacon/BeaconStatus";
|
||||
import { BeaconDisplayStatus } from "../../../../src/components/views/beacon/displayStatus";
|
||||
import { makeBeaconInfoEvent } from "../../../test-utils";
|
||||
|
||||
describe("<BeaconStatus />", () => {
|
||||
const defaultProps = {
|
||||
displayStatus: BeaconDisplayStatus.Loading,
|
||||
label: "test label",
|
||||
withIcon: true,
|
||||
};
|
||||
const renderComponent = (props = {}) => render(<BeaconStatus {...defaultProps} {...props} />);
|
||||
|
||||
it("renders loading state", () => {
|
||||
const { asFragment } = renderComponent({ displayStatus: BeaconDisplayStatus.Loading });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders stopped state", () => {
|
||||
const { asFragment } = renderComponent({ displayStatus: BeaconDisplayStatus.Stopped });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without icon", () => {
|
||||
const iconClassName = "mx_StyledLiveBeaconIcon";
|
||||
const { container } = renderComponent({ withIcon: false, displayStatus: BeaconDisplayStatus.Stopped });
|
||||
expect(container.getElementsByClassName(iconClassName)).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe("active state", () => {
|
||||
it("renders without children", () => {
|
||||
// mock for stable snapshot
|
||||
jest.spyOn(Date, "now").mockReturnValue(123456789);
|
||||
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
|
||||
const { asFragment } = renderComponent({ beacon, displayStatus: BeaconDisplayStatus.Active });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with children", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:sever", { isLive: false }));
|
||||
renderComponent({
|
||||
beacon,
|
||||
children: <span data-testid="test-child">test</span>,
|
||||
displayStatus: BeaconDisplayStatus.Active,
|
||||
});
|
||||
expect(screen.getByTestId("test-child")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders static remaining time when displayLiveTimeRemaining is falsy", () => {
|
||||
// mock for stable snapshot
|
||||
jest.spyOn(Date, "now").mockReturnValue(123456789);
|
||||
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
|
||||
renderComponent({ beacon, displayStatus: BeaconDisplayStatus.Active });
|
||||
expect(screen.getByText("Live until 11:17")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders live time remaining when displayLiveTimeRemaining is truthy", () => {
|
||||
// mock for stable snapshot
|
||||
jest.spyOn(Date, "now").mockReturnValue(123456789);
|
||||
const beacon = new Beacon(makeBeaconInfoEvent("@user:server", "!room:server", { isLive: false }, "$1"));
|
||||
renderComponent({
|
||||
beacon,
|
||||
displayStatus: BeaconDisplayStatus.Active,
|
||||
displayLiveTimeRemaining: true,
|
||||
});
|
||||
expect(screen.getByText("1h left")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,371 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
|
||||
import { MatrixClient, MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
import * as maplibregl from "maplibre-gl";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import BeaconViewDialog from "../../../../src/components/views/beacon/BeaconViewDialog";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
makeRoomWithStateEvents,
|
||||
} from "../../../test-utils";
|
||||
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
|
||||
import { OwnBeaconStore } from "../../../../src/stores/OwnBeaconStore";
|
||||
|
||||
describe("<BeaconViewDialog />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
// stable date for snapshots
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alice:server";
|
||||
const bobId = "@bob:server";
|
||||
|
||||
const aliceMember = new RoomMember(roomId, aliceId);
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn().mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
}),
|
||||
getUserId: jest.fn().mockReturnValue(bobId),
|
||||
getRoom: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
});
|
||||
|
||||
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
|
||||
const mockMap = new maplibregl.Map(mapOptions);
|
||||
const mockMarker = new maplibregl.Marker();
|
||||
|
||||
// make fresh rooms every time
|
||||
// as we update room state
|
||||
const setupRoom = (stateEvents: MatrixEvent[] = []): Room => {
|
||||
const room1 = makeRoomWithStateEvents(stateEvents, { roomId, mockClient });
|
||||
jest.spyOn(room1, "getMember").mockReturnValue(aliceMember);
|
||||
|
||||
return room1;
|
||||
};
|
||||
|
||||
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
|
||||
const location1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: defaultEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
timestamp: now + 1,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
onFinished: jest.fn(),
|
||||
roomId,
|
||||
matrixClient: mockClient as MatrixClient,
|
||||
};
|
||||
|
||||
const getComponent = (props = {}): RenderResult => render(<BeaconViewDialog {...defaultProps} {...props} />);
|
||||
|
||||
const openSidebar = (getByTestId: RenderResult["getByTestId"]) => {
|
||||
fireEvent.click(getByTestId("beacon-view-dialog-open-sidebar"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(OwnBeaconStore.instance, "getLiveBeaconIds").mockRestore();
|
||||
jest.spyOn(OwnBeaconStore.instance, "getBeaconById").mockRestore();
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders a map with markers", async () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
getComponent();
|
||||
// centered on default event
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({
|
||||
lon: 41,
|
||||
lat: 51,
|
||||
});
|
||||
// marker added
|
||||
await waitFor(() => {
|
||||
expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render any own beacon status when user is not live sharing", () => {
|
||||
// default event belongs to alice, we are bob
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { queryByText } = getComponent();
|
||||
expect(queryByText("Live location enabled")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders own beacon status when user is live sharing", () => {
|
||||
// default event belongs to alice
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
// mock own beacon store to show default event as alice's live beacon
|
||||
jest.spyOn(OwnBeaconStore.instance, "getLiveBeaconIds").mockReturnValue([beacon.identifier]);
|
||||
jest.spyOn(OwnBeaconStore.instance, "getBeaconById").mockReturnValue(beacon);
|
||||
const { container } = getComponent();
|
||||
expect(container.querySelector(".mx_DialogOwnBeaconStatus")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("updates markers on changes to beacons", async () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { container } = getComponent();
|
||||
|
||||
// one marker
|
||||
expect(mockMarker.addTo).toHaveBeenCalledTimes(1);
|
||||
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
|
||||
|
||||
const anotherBeaconEvent = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-1");
|
||||
act(() => {
|
||||
// emits RoomStateEvent.BeaconLiveness
|
||||
room.currentState.setStateEvents([anotherBeaconEvent]);
|
||||
const beacon2 = room.currentState.beacons.get(getBeaconInfoIdentifier(anotherBeaconEvent))!;
|
||||
beacon2.addLocations([location1]);
|
||||
});
|
||||
|
||||
// two markers now!
|
||||
expect(container.getElementsByClassName("mx_Marker").length).toEqual(2);
|
||||
});
|
||||
|
||||
it("does not update bounds or center on changing beacons", () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { container } = getComponent();
|
||||
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
|
||||
|
||||
const anotherBeaconEvent = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-1");
|
||||
act(() => {
|
||||
// emits RoomStateEvent.BeaconLiveness
|
||||
room.currentState.setStateEvents([anotherBeaconEvent]);
|
||||
const beacon2 = room.currentState.beacons.get(getBeaconInfoIdentifier(anotherBeaconEvent))!;
|
||||
beacon2.addLocations([location1]);
|
||||
});
|
||||
// called once on init
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders a fallback when there are no locations", () => {
|
||||
// this is a cornercase, should not be a reachable state in UI anymore
|
||||
const onFinished = jest.fn();
|
||||
const room = setupRoom([defaultEvent]);
|
||||
room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
|
||||
const { getByTestId } = getComponent({ onFinished });
|
||||
|
||||
// map placeholder
|
||||
expect(getByTestId("beacon-view-dialog-map-fallback")).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(getByTestId("beacon-view-dialog-fallback-close"));
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders map without markers when no live beacons remain", () => {
|
||||
const onFinished = jest.fn();
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { container } = getComponent({ onFinished });
|
||||
expect(container.getElementsByClassName("mx_Marker").length).toEqual(1);
|
||||
|
||||
// this will replace the defaultEvent
|
||||
// leading to no more live beacons
|
||||
const anotherBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-2");
|
||||
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
// reset call counts
|
||||
mocked(mockMap.setCenter).mockClear();
|
||||
mocked(mockMap.fitBounds).mockClear();
|
||||
|
||||
act(() => {
|
||||
// emits RoomStateEvent.BeaconLiveness
|
||||
room.currentState.setStateEvents([anotherBeaconEvent]);
|
||||
});
|
||||
|
||||
// no more avatars
|
||||
expect(container.getElementsByClassName("mx_Marker").length).toEqual(0);
|
||||
// map still rendered
|
||||
expect(container.querySelector("#mx_Map_mx_BeaconViewDialog")).toBeInTheDocument();
|
||||
// map location unchanged
|
||||
expect(mockMap.setCenter).not.toHaveBeenCalled();
|
||||
expect(mockMap.fitBounds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("sidebar", () => {
|
||||
it("opens sidebar on view list button click", () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { container, getByTestId } = getComponent();
|
||||
|
||||
openSidebar(getByTestId);
|
||||
|
||||
expect(container.querySelector(".mx_DialogSidebar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes sidebar on close button click", () => {
|
||||
const room = setupRoom([defaultEvent]);
|
||||
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!;
|
||||
beacon.addLocations([location1]);
|
||||
const { container, getByTestId } = getComponent();
|
||||
|
||||
// open the sidebar
|
||||
openSidebar(getByTestId);
|
||||
|
||||
expect(container.querySelector(".mx_DialogSidebar")).toBeInTheDocument();
|
||||
|
||||
// now close it
|
||||
fireEvent.click(getByTestId("dialog-sidebar-close"));
|
||||
|
||||
expect(container.querySelector(".mx_DialogSidebar")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("focused beacons", () => {
|
||||
const beacon2Event = makeBeaconInfoEvent(bobId, roomId, { isLive: true }, "$bob-room1-2");
|
||||
|
||||
const location2 = makeBeaconEvent(bobId, {
|
||||
beaconInfoId: beacon2Event.getId(),
|
||||
geoUri: "geo:33,22",
|
||||
timestamp: now + 1,
|
||||
});
|
||||
|
||||
const fitBoundsOptions = { maxZoom: 15, padding: 100 };
|
||||
|
||||
it("opens map with both beacons in view on first load without initialFocusedBeacon", () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId,
|
||||
mockClient,
|
||||
[defaultEvent, beacon2Event],
|
||||
[location1, location2],
|
||||
);
|
||||
|
||||
getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// start centered on mid point between both beacons
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 42, lon: 31.5 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fit both beacons, only called once
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(
|
||||
new maplibregl.LngLatBounds([22, 33], [41, 51]),
|
||||
fitBoundsOptions,
|
||||
);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens map with both beacons in view on first load with an initially focused beacon", () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId,
|
||||
mockClient,
|
||||
[defaultEvent, beacon2Event],
|
||||
[location1, location2],
|
||||
);
|
||||
|
||||
getComponent({ beacons: [beacon1, beacon2], initialFocusedBeacon: beacon1 });
|
||||
|
||||
// start centered on initialFocusedBeacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fit both beacons, only called once
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(
|
||||
new maplibregl.LngLatBounds([22, 33], [41, 51]),
|
||||
fitBoundsOptions,
|
||||
);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("focuses on beacon location on sidebar list item click", () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId,
|
||||
mockClient,
|
||||
[defaultEvent, beacon2Event],
|
||||
[location1, location2],
|
||||
);
|
||||
|
||||
const { container, getByTestId } = getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// reset call counts on map mocks after initial render
|
||||
jest.clearAllMocks();
|
||||
|
||||
openSidebar(getByTestId);
|
||||
|
||||
act(() => {
|
||||
const listItems = container.querySelectorAll(".mx_BeaconListItem");
|
||||
// click on the first beacon in the list
|
||||
fireEvent.click(listItems[0]!);
|
||||
});
|
||||
|
||||
// centered on clicked beacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fitted just to clicked beacon
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(
|
||||
new maplibregl.LngLatBounds([41, 51], [41, 51]),
|
||||
fitBoundsOptions,
|
||||
);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("refocuses on same beacon when clicking list item again", () => {
|
||||
// test the map responds to refocusing the same beacon
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId,
|
||||
mockClient,
|
||||
[defaultEvent, beacon2Event],
|
||||
[location1, location2],
|
||||
);
|
||||
|
||||
const { container, getByTestId } = getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// reset call counts on map mocks after initial render
|
||||
jest.clearAllMocks();
|
||||
|
||||
openSidebar(getByTestId);
|
||||
|
||||
act(() => {
|
||||
// click on the second beacon in the list
|
||||
const listItems = container.querySelectorAll(".mx_BeaconListItem");
|
||||
fireEvent.click(listItems[1]!);
|
||||
});
|
||||
|
||||
const expectedBounds = new maplibregl.LngLatBounds([22, 33], [22, 33]);
|
||||
|
||||
// date is mocked but this relies on timestamp, manually mock a tick
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now + 1);
|
||||
|
||||
act(() => {
|
||||
// click on the second beacon in the list
|
||||
const listItems = container.querySelectorAll(".mx_BeaconListItem");
|
||||
fireEvent.click(listItems[1]!);
|
||||
});
|
||||
|
||||
// centered on clicked beacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 33, lon: 22 });
|
||||
// bounds fitted just to clicked beacon
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(expectedBounds, fitBoundsOptions);
|
||||
// each called once per click
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(2);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import DialogSidebar from "../../../../src/components/views/beacon/DialogSidebar";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../test-utils";
|
||||
|
||||
describe("<DialogSidebar />", () => {
|
||||
const defaultProps: ComponentProps<typeof DialogSidebar> = {
|
||||
beacons: [],
|
||||
requestClose: jest.fn(),
|
||||
onBeaconClick: jest.fn(),
|
||||
};
|
||||
|
||||
const now = 1647270879403;
|
||||
|
||||
const roomId = "!room:server.org";
|
||||
const aliceId = "@alice:server.org";
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(aliceId),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
|
||||
const beaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true, timestamp: now }, "$alice-room1-1");
|
||||
const location1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: beaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
const getComponent = (props = {}) => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<DialogSidebar {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
// mock now so time based text in snapshots is stable
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("renders sidebar correctly without beacons", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders sidebar correctly with beacons", () => {
|
||||
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||
const { container } = render(getComponent({ beacons: [beacon] }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls on beacon click", () => {
|
||||
const onBeaconClick = jest.fn();
|
||||
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
|
||||
|
||||
act(() => {
|
||||
const [listItem] = container.getElementsByClassName("mx_BeaconListItem");
|
||||
fireEvent.click(listItem);
|
||||
});
|
||||
|
||||
expect(onBeaconClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes on close button click", () => {
|
||||
const requestClose = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ requestClose }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("dialog-sidebar-close"));
|
||||
});
|
||||
expect(requestClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
import { Beacon, BeaconIdentifier } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LeftPanelLiveShareWarning from "../../../../src/components/views/beacon/LeftPanelLiveShareWarning";
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../../src/stores/OwnBeaconStore";
|
||||
import { flushPromises, makeBeaconInfoEvent } from "../../../test-utils";
|
||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../../src/stores/OwnBeaconStore", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const EventEmitter = require("events");
|
||||
class MockOwnBeaconStore extends EventEmitter {
|
||||
public getLiveBeaconIdsWithLocationPublishError = jest.fn().mockReturnValue([]);
|
||||
public getBeaconById = jest.fn();
|
||||
public getLiveBeaconIds = jest.fn().mockReturnValue([]);
|
||||
public readonly beaconUpdateErrors = new Map<BeaconIdentifier, Error>();
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
}
|
||||
return {
|
||||
// @ts-ignore
|
||||
...jest.requireActual("../../../../src/stores/OwnBeaconStore"),
|
||||
OwnBeaconStore: {
|
||||
instance: new MockOwnBeaconStore() as unknown as OwnBeaconStore,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("<LeftPanelLiveShareWarning />", () => {
|
||||
const getComponent = (props = {}) => render(<LeftPanelLiveShareWarning {...props} />);
|
||||
|
||||
const roomId1 = "!room1:server";
|
||||
const roomId2 = "!room2:server";
|
||||
const aliceId = "@alive:server";
|
||||
|
||||
const now = 1647270879403;
|
||||
const HOUR_MS = 3600000;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
jest.spyOn(dispatcher, "dispatch")
|
||||
.mockClear()
|
||||
.mockImplementation(() => {});
|
||||
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.clear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
// 12h old, 12h left
|
||||
const beacon1 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId1, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
|
||||
);
|
||||
// 10h left
|
||||
const beacon2 = new Beacon(makeBeaconInfoEvent(aliceId, roomId2, { timeout: HOUR_MS * 10, timestamp: now }, "$2"));
|
||||
|
||||
it("renders nothing when user has no live beacons", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("when user has live location monitor", () => {
|
||||
beforeAll(() => {
|
||||
mocked(OwnBeaconStore.instance).getBeaconById.mockImplementation((beaconId) => {
|
||||
if (beaconId === beacon1.identifier) {
|
||||
return beacon1;
|
||||
}
|
||||
return beacon2;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore writing to readonly variable
|
||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([]);
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIds.mockReturnValue([beacon2.identifier, beacon1.identifier]);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(document, "addEventListener").mockRestore();
|
||||
});
|
||||
|
||||
it("renders correctly when not minimized", () => {
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("goes to room of latest beacon when clicked", () => {
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
fireEvent.click(container.querySelector("[role=button]")!);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
metricsTrigger: undefined,
|
||||
// latest beacon's room
|
||||
room_id: roomId2,
|
||||
event_id: beacon2.beaconInfoId,
|
||||
highlighted: true,
|
||||
scroll_into_view: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders correctly when minimized", () => {
|
||||
const { asFragment } = getComponent({ isMinimized: true });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders location publish error", () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
|
||||
beacon1.identifier,
|
||||
]);
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("goes to room of latest beacon with location publish error when clicked", () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
|
||||
beacon1.identifier,
|
||||
]);
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
fireEvent.click(container.querySelector("[role=button]")!);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
metricsTrigger: undefined,
|
||||
// error beacon's room
|
||||
room_id: roomId1,
|
||||
event_id: beacon1.beaconInfoId,
|
||||
highlighted: true,
|
||||
scroll_into_view: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("goes back to default style when wire errors are cleared", () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
|
||||
beacon1.identifier,
|
||||
]);
|
||||
const { container, rerender } = getComponent();
|
||||
// error mode
|
||||
expect(container.querySelector(".mx_LeftPanelLiveShareWarning")?.textContent).toEqual(
|
||||
"An error occurred whilst sharing your live location",
|
||||
);
|
||||
|
||||
act(() => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([]);
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LocationPublishError, "abc");
|
||||
});
|
||||
|
||||
rerender(<LeftPanelLiveShareWarning />);
|
||||
|
||||
// default mode
|
||||
expect(container.querySelector(".mx_LeftPanelLiveShareWarning")?.textContent).toEqual(
|
||||
"You are sharing your live location",
|
||||
);
|
||||
});
|
||||
|
||||
it("removes itself when user stops having live beacons", async () => {
|
||||
const { container, rerender } = getComponent({ isMinimized: true });
|
||||
// started out rendered
|
||||
expect(container.innerHTML).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
// @ts-ignore writing to readonly variable
|
||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
rerender(<LeftPanelLiveShareWarning />);
|
||||
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it("refreshes beacon liveness monitors when pagevisibilty changes to visible", () => {
|
||||
OwnBeaconStore.instance.beacons.set(beacon1.identifier, beacon1);
|
||||
OwnBeaconStore.instance.beacons.set(beacon2.identifier, beacon2);
|
||||
const beacon1MonitorSpy = jest.spyOn(beacon1, "monitorLiveness");
|
||||
const beacon2MonitorSpy = jest.spyOn(beacon1, "monitorLiveness");
|
||||
|
||||
jest.spyOn(document, "addEventListener").mockImplementation((_e, listener) =>
|
||||
(listener as EventListener)(new Event("")),
|
||||
);
|
||||
|
||||
expect(beacon1MonitorSpy).not.toHaveBeenCalled();
|
||||
|
||||
getComponent();
|
||||
|
||||
expect(beacon1MonitorSpy).toHaveBeenCalled();
|
||||
expect(beacon2MonitorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("stopping errors", () => {
|
||||
it("renders stopping error", () => {
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
|
||||
const { container } = getComponent();
|
||||
expect(container.textContent).toEqual("An error occurred while stopping your live location");
|
||||
});
|
||||
|
||||
it("starts rendering stopping error on beaconUpdateError emit", () => {
|
||||
const { container } = getComponent();
|
||||
// no error
|
||||
expect(container.textContent).toEqual("You are sharing your live location");
|
||||
|
||||
act(() => {
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.BeaconUpdateError, beacon2.identifier, true);
|
||||
});
|
||||
|
||||
expect(container.textContent).toEqual("An error occurred while stopping your live location");
|
||||
});
|
||||
|
||||
it("renders stopping error when beacons have stopping and location errors", () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
|
||||
beacon1.identifier,
|
||||
]);
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
|
||||
const { container } = getComponent();
|
||||
expect(container.textContent).toEqual("An error occurred while stopping your live location");
|
||||
});
|
||||
|
||||
it("goes to room of latest beacon with stopping error when clicked", () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue([
|
||||
beacon1.identifier,
|
||||
]);
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error("error"));
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
|
||||
fireEvent.click(container.querySelector("[role=button]")!);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
metricsTrigger: undefined,
|
||||
// stopping error beacon's room
|
||||
room_id: beacon2.roomId,
|
||||
event_id: beacon2.beaconInfoId,
|
||||
highlighted: true,
|
||||
scroll_into_view: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
155
test/unit-tests/components/views/beacon/OwnBeaconStatus-test.tsx
Normal file
155
test/unit-tests/components/views/beacon/OwnBeaconStatus-test.tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Beacon } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import OwnBeaconStatus from "../../../../src/components/views/beacon/OwnBeaconStatus";
|
||||
import { BeaconDisplayStatus } from "../../../../src/components/views/beacon/displayStatus";
|
||||
import { useOwnLiveBeacons } from "../../../../src/utils/beacon";
|
||||
import { makeBeaconInfoEvent } from "../../../test-utils";
|
||||
|
||||
jest.mock("../../../../src/utils/beacon/useOwnLiveBeacons", () => ({
|
||||
useOwnLiveBeacons: jest.fn(),
|
||||
}));
|
||||
|
||||
const defaultLiveBeaconsState = {
|
||||
onStopSharing: jest.fn(),
|
||||
onResetLocationPublishError: jest.fn(),
|
||||
stoppingInProgress: false,
|
||||
hasStopSharingError: false,
|
||||
hasLocationPublishError: false,
|
||||
};
|
||||
|
||||
describe("<OwnBeaconStatus />", () => {
|
||||
const defaultProps = {
|
||||
displayStatus: BeaconDisplayStatus.Loading,
|
||||
};
|
||||
const userId = "@user:server";
|
||||
const roomId = "!room:server";
|
||||
let defaultBeacon: Beacon;
|
||||
const renderComponent = (props: Partial<React.ComponentProps<typeof OwnBeaconStatus>> = {}) =>
|
||||
render(<OwnBeaconStatus {...defaultProps} {...props} />);
|
||||
const getRetryButton = () => screen.getByRole("button", { name: "Retry" });
|
||||
const getStopButton = () => screen.getByRole("button", { name: "Stop" });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(123456789);
|
||||
mocked(useOwnLiveBeacons).mockClear().mockReturnValue(defaultLiveBeaconsState);
|
||||
|
||||
defaultBeacon = new Beacon(makeBeaconInfoEvent(userId, roomId));
|
||||
});
|
||||
|
||||
it("renders without a beacon instance", () => {
|
||||
const { asFragment } = renderComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("Active state", () => {
|
||||
it("renders stop button", () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
onStopSharing: jest.fn(),
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
expect(screen.getByText("Live location enabled")).toBeInTheDocument();
|
||||
expect(getStopButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stops sharing on stop button click", async () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
const onStopSharing = jest.fn();
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
onStopSharing,
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
await userEvent.click(getStopButton());
|
||||
expect(onStopSharing).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("renders in error mode when displayStatus is error", () => {
|
||||
const displayStatus = BeaconDisplayStatus.Error;
|
||||
renderComponent({ displayStatus });
|
||||
expect(screen.getByText("Live location error")).toBeInTheDocument();
|
||||
|
||||
// no actions for plain error
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("with location publish error", () => {
|
||||
it("renders in error mode", () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
hasLocationPublishError: true,
|
||||
onResetLocationPublishError: jest.fn(),
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
expect(screen.getByText("Live location error")).toBeInTheDocument();
|
||||
// retry button
|
||||
expect(getRetryButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("retry button resets location publish error", async () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
const onResetLocationPublishError = jest.fn();
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
hasLocationPublishError: true,
|
||||
onResetLocationPublishError,
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
await userEvent.click(getRetryButton());
|
||||
|
||||
expect(onResetLocationPublishError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with stopping error", () => {
|
||||
it("renders in error mode", () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
hasLocationPublishError: false,
|
||||
hasStopSharingError: true,
|
||||
onStopSharing: jest.fn(),
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
expect(screen.getByText("Live location error")).toBeInTheDocument();
|
||||
// retry button
|
||||
expect(getRetryButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("retry button retries stop sharing", async () => {
|
||||
const displayStatus = BeaconDisplayStatus.Active;
|
||||
const onStopSharing = jest.fn();
|
||||
mocked(useOwnLiveBeacons).mockReturnValue({
|
||||
...defaultLiveBeaconsState,
|
||||
hasStopSharingError: true,
|
||||
onStopSharing,
|
||||
});
|
||||
renderComponent({ displayStatus, beacon: defaultBeacon });
|
||||
await userEvent.click(getRetryButton());
|
||||
|
||||
expect(onStopSharing).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("renders loading state correctly", () => {
|
||||
const component = renderComponent();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
118
test/unit-tests/components/views/beacon/RoomCallBanner-test.tsx
Normal file
118
test/unit-tests/components/views/beacon/RoomCallBanner-test.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Room, PendingEventOrdering, MatrixClient, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||
import { act, cleanup, render, screen } from "jest-matrix-react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
|
||||
import { mkRoomMember, MockedCall, setupAsyncStoreWithClient, stubClient, useMockedCalls } from "../../../test-utils";
|
||||
import RoomCallBanner from "../../../../src/components/views/beacon/RoomCallBanner";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { ConnectionState } from "../../../../src/models/Call";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
|
||||
describe("<RoomCallBanner />", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
useMockedCalls();
|
||||
|
||||
const defaultProps = {
|
||||
roomId: "!1:example.org",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId) => (userId === alice.userId ? alice : null));
|
||||
|
||||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
});
|
||||
|
||||
const renderBanner = async (props = {}): Promise<void> => {
|
||||
render(<RoomCallBanner {...defaultProps} {...props} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
it("renders nothing when there is no call", async () => {
|
||||
await renderBanner();
|
||||
const banner = await screen.queryByText("Video call");
|
||||
expect(banner).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("call started", () => {
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) {
|
||||
throw new Error("Failed to create call");
|
||||
}
|
||||
call = maybeCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
||||
call.destroy();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
it("renders if there is a call", async () => {
|
||||
await renderBanner();
|
||||
await screen.findByText("Video call");
|
||||
});
|
||||
|
||||
it("shows Join button if the user has not joined", async () => {
|
||||
await renderBanner();
|
||||
await screen.findByText("Join");
|
||||
});
|
||||
|
||||
it("doesn't show banner if the call is connected", async () => {
|
||||
call.setConnectionState(ConnectionState.Connected);
|
||||
await renderBanner();
|
||||
const banner = await screen.queryByText("Video call");
|
||||
expect(banner).toBeFalsy();
|
||||
});
|
||||
|
||||
it("doesn't show banner if the call is shown", async () => {
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall");
|
||||
mocked(SdkContextClass.instance.roomViewStore.isViewingCall).mockReturnValue(true);
|
||||
await renderBanner();
|
||||
const banner = await screen.queryByText("Video call");
|
||||
expect(banner).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: test clicking buttons
|
||||
// TODO: add live location share warning test (should not render if there is an active live location share)
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import ShareLatestLocation from "../../../../src/components/views/beacon/ShareLatestLocation";
|
||||
import { copyPlaintext } from "../../../../src/utils/strings";
|
||||
import { flushPromises } from "../../../test-utils";
|
||||
|
||||
jest.mock("../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe("<ShareLatestLocation />", () => {
|
||||
const defaultProps = {
|
||||
latestLocationState: {
|
||||
uri: "geo:51,42;u=35",
|
||||
timestamp: 123,
|
||||
},
|
||||
};
|
||||
const getComponent = (props = {}) => render(<ShareLatestLocation {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders null when no location", () => {
|
||||
const { container } = getComponent({ latestLocationState: undefined });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders share buttons when there is a location", async () => {
|
||||
const { container, asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_CopyableText_copyButton")!);
|
||||
await flushPromises();
|
||||
|
||||
expect(copyPlaintext).toHaveBeenCalledWith("51,42");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import StyledLiveBeaconIcon from "../../../../src/components/views/beacon/StyledLiveBeaconIcon";
|
||||
|
||||
describe("<StyledLiveBeaconIcon />", () => {
|
||||
const defaultProps = {};
|
||||
const getComponent = (props = {}) => render(<StyledLiveBeaconIcon {...defaultProps} {...props} />);
|
||||
|
||||
it("renders", () => {
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `
|
||||
<DocumentFragment>
|
||||
<li
|
||||
class="mx_BeaconListItem"
|
||||
>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon"
|
||||
/>
|
||||
<div
|
||||
class="mx_BeaconListItem_info"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
Alice's car
|
||||
</span>
|
||||
<span
|
||||
class="mx_BeaconStatus_expiryTime"
|
||||
>
|
||||
Live until 16:04
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BeaconListItem_interactions"
|
||||
>
|
||||
<a
|
||||
aria-labelledby="floating-ui-1"
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_BeaconListItem_lastUpdated"
|
||||
>
|
||||
Updated a few seconds ago
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,28 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
<div
|
||||
class="mx_Marker mx_Username_color6"
|
||||
id="!room:server_@alice:server"
|
||||
>
|
||||
<div
|
||||
class="mx_Marker_border"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
title="@alice:server"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,77 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconStatus /> active state renders with children 1`] = `
|
||||
<span
|
||||
data-testid="test-child"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`<BeaconStatus /> active state renders without children 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active"
|
||||
>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
test label
|
||||
</span>
|
||||
<span
|
||||
class="mx_BeaconStatus_expiryTime"
|
||||
>
|
||||
Live until 11:17
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<BeaconStatus /> renders loading state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Loading"
|
||||
>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle"
|
||||
/>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_description_status"
|
||||
>
|
||||
Loading live location…
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<BeaconStatus /> renders stopped state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Stopped"
|
||||
>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle"
|
||||
/>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_description_status"
|
||||
>
|
||||
Live location ended
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,73 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconViewDialog /> renders a fallback when there are no locations 1`] = `
|
||||
<div
|
||||
class="mx_MapFallback mx_BeaconViewDialog_map"
|
||||
data-testid="beacon-view-dialog-map-fallback"
|
||||
>
|
||||
<div
|
||||
class="mx_MapFallback_bg"
|
||||
/>
|
||||
<div
|
||||
class="mx_MapFallback_icon"
|
||||
/>
|
||||
<span
|
||||
class="mx_BeaconViewDialog_mapFallbackMessage"
|
||||
>
|
||||
No live locations
|
||||
</span>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
data-testid="beacon-view-dialog-fallback-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Close
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<BeaconViewDialog /> renders own beacon status when user is live sharing 1`] = `
|
||||
<div
|
||||
class="mx_DialogOwnBeaconStatus"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_DialogOwnBeaconStatus_avatar _avatar-imageless_mcap2_61"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
title="@alice:server"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active mx_DialogOwnBeaconStatus_status"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
Live location enabled
|
||||
</span>
|
||||
<span
|
||||
class="mx_LiveTimeRemaining"
|
||||
data-testid="room-live-share-expiry"
|
||||
>
|
||||
1h left
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
||||
data-testid="beacon-status-stop-beacon"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Stop
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,152 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DialogSidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_header"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
View list
|
||||
</h4>
|
||||
<div
|
||||
aria-label="Close sidebar"
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<ol
|
||||
class="mx_DialogSidebar_list"
|
||||
>
|
||||
<li
|
||||
class="mx_BeaconListItem"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_BeaconListItem_avatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
|
||||
</span>
|
||||
<div
|
||||
class="mx_BeaconListItem_info"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
@alice:server.org
|
||||
</span>
|
||||
<span
|
||||
class="mx_BeaconStatus_expiryTime"
|
||||
>
|
||||
Live until 16:14
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BeaconListItem_interactions"
|
||||
>
|
||||
<a
|
||||
aria-labelledby="floating-ui-8"
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_BeaconListItem_lastUpdated"
|
||||
>
|
||||
Updated a few seconds ago
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DialogSidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_header"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
View list
|
||||
</h4>
|
||||
<div
|
||||
aria-label="Close sidebar"
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DialogSidebar_noResults"
|
||||
>
|
||||
No live locations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,40 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="You are sharing your live location"
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
height="10"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
You are sharing your live location
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders location publish error 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
An error occurred whilst sharing your live location
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<OwnBeaconStatus /> renders without a beacon instance 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Loading"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_description_status"
|
||||
>
|
||||
Loading live location…
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ShareLatestLocation /> renders share buttons when there is a location 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
aria-labelledby="floating-ui-1"
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=42#map=16/51/42"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<StyledLiveBeaconIcon /> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
73
test/unit-tests/components/views/beta/BetaCard-test.tsx
Normal file
73
test/unit-tests/components/views/beta/BetaCard-test.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
|
||||
import { shouldShowFeedback } from "../../../../src/utils/Feedback";
|
||||
import BetaCard from "../../../../src/components/views/beta/BetaCard";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { TranslationKey } from "../../../../src/languageHandler";
|
||||
|
||||
jest.mock("../../../../src/utils/Feedback");
|
||||
jest.mock("../../../../src/settings/SettingsStore");
|
||||
|
||||
describe("<BetaCard />", () => {
|
||||
describe("Feedback prompt", () => {
|
||||
const featureId = "featureId";
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackLabel: "feedbackLabel",
|
||||
feedbackSubheading: "feedbackSubheading" as TranslationKey,
|
||||
});
|
||||
mocked(SettingsStore).getValue.mockReturnValue(true);
|
||||
mocked(shouldShowFeedback).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should show feedback prompt", () => {
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if beta is disabled", () => {
|
||||
mocked(SettingsStore).getValue.mockReturnValue(false);
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if label is unset", () => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackSubheading: "feedbackSubheading" as TranslationKey,
|
||||
});
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if subheading is unset", () => {
|
||||
mocked(SettingsStore).getBetaInfo.mockReturnValue({
|
||||
title: "title" as TranslationKey,
|
||||
caption: () => "caption",
|
||||
feedbackLabel: "feedbackLabel",
|
||||
});
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not show feedback prompt if feedback is disabled", () => {
|
||||
mocked(shouldShowFeedback).mockReturnValue(false);
|
||||
render(<BetaCard featureId={featureId} />);
|
||||
expect(screen.queryByText("Feedback")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import ContextMenu, { ChevronFace } from "../../../../src/components/structures/ContextMenu";
|
||||
import UIStore from "../../../../src/stores/UIStore";
|
||||
import Modal from "../../../../src/Modal";
|
||||
import BaseDialog from "../../../../src/components/views/dialogs/BaseDialog";
|
||||
|
||||
describe("<ContextMenu />", () => {
|
||||
// Hardcode window and menu dimensions
|
||||
const windowSize = 300;
|
||||
const menuSize = 200;
|
||||
jest.spyOn(UIStore, "instance", "get").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
windowWidth: windowSize,
|
||||
windowHeight: windowSize,
|
||||
}) as unknown as UIStore,
|
||||
);
|
||||
window.Element.prototype.getBoundingClientRect = jest.fn().mockReturnValue({
|
||||
width: menuSize,
|
||||
height: menuSize,
|
||||
});
|
||||
|
||||
const targetChevronOffset = 25;
|
||||
|
||||
it("near top edge of window", () => {
|
||||
const targetY = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={windowSize - targetY - menuSize}
|
||||
right={menuSize}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Left}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_left")!;
|
||||
|
||||
const bottomStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("bottom"),
|
||||
);
|
||||
const actualY = windowSize - bottomStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near right edge of window", () => {
|
||||
const targetX = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={0}
|
||||
onFinished={onFinished}
|
||||
left={targetX}
|
||||
chevronFace={ChevronFace.Top}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_top")!;
|
||||
|
||||
const actualX = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("left"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("near bottom edge of window", () => {
|
||||
const targetY = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={targetY}
|
||||
left={0}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Right}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_right")!;
|
||||
|
||||
const actualY = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("top"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near left edge of window", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_bottom")!;
|
||||
|
||||
const rightStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("right"),
|
||||
);
|
||||
const actualX = windowSize - rightStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("should automatically close when a modal is opened", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not automatically close when a modal is opened under the existing one", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
Modal.createDialog(BaseDialog);
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog, {}, "", false, true);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.appendDialog(BaseDialog);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { _t } from "../../../../src/languageHandler";
|
||||
import EmbeddedPage from "../../../../src/components/structures/EmbeddedPage";
|
||||
|
||||
jest.mock("../../../../src/languageHandler", () => ({
|
||||
_t: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<EmbeddedPage />", () => {
|
||||
it("should translate _t strings", async () => {
|
||||
mocked(_t).mockReturnValue("Przeglądaj pokoje");
|
||||
fetchMock.get("https://home.page", {
|
||||
body: '<h1>_t("Explore rooms")</h1>',
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://home.page" />);
|
||||
await screen.findByText("Przeglądaj pokoje");
|
||||
expect(_t).toHaveBeenCalledWith("Explore rooms");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show error if unable to load", async () => {
|
||||
mocked(_t).mockReturnValue("Couldn't load page");
|
||||
fetchMock.get("https://other.page", {
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://other.page" />);
|
||||
await screen.findByText("Couldn't load page");
|
||||
expect(_t).toHaveBeenCalledWith("cant_load_page");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render nothing if no url given", () => {
|
||||
const { asFragment } = render(<EmbeddedPage />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,553 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
PendingEventOrdering,
|
||||
BeaconIdentifier,
|
||||
Beacon,
|
||||
getBeaconInfoIdentifier,
|
||||
EventType,
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
EventTimeline,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import { canEditContent } from "../../../../src/utils/EventUtils";
|
||||
import { copyPlaintext, getSelectedText } from "../../../../src/utils/strings";
|
||||
import MessageContextMenu from "../../../../src/components/views/context_menus/MessageContextMenu";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent, makeLocationEvent, stubClient } from "../../../test-utils";
|
||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { ReadPinsEventId } from "../../../../src/components/views/right_panel/types";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||
import { createMessageEventContent } from "../../../test-utils/events";
|
||||
|
||||
jest.mock("../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn(),
|
||||
getSelectedText: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../src/utils/EventUtils", () => ({
|
||||
...(jest.requireActual("../../../../src/utils/EventUtils") as object),
|
||||
canEditContent: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../src/dispatcher/dispatcher");
|
||||
|
||||
const roomId = "roomid";
|
||||
|
||||
describe("MessageContextMenu", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("does show copy link button when supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const props = {
|
||||
link: "https://google.com/",
|
||||
};
|
||||
createMenuWithContent(eventContent, props);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toHaveAttribute("href", props.link);
|
||||
});
|
||||
|
||||
it("does not show copy link button when not supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("message pinning", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = makeDefaultRoom();
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
});
|
||||
|
||||
it("does not show pin option when user does not have rights to pin", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
// mock permission to disallow adding pinned messages to room
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
createMenu(event, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not show pin option for beacon_info event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice:server.org", roomId, { isLive: false });
|
||||
|
||||
createMenu(deadBeaconEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows pin option when pinning feature is enabled", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.getByRole("menuitem", { name: "Pin" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("pins event on pin option click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: ["!1", "!2"] }),
|
||||
});
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { onFinished, rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Pin" }));
|
||||
|
||||
// added to account data
|
||||
await waitFor(() =>
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
// from account data
|
||||
"!1",
|
||||
"!2",
|
||||
pinnableEvent.getId(),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// add to room's pins
|
||||
await waitFor(() =>
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{
|
||||
pinned: ["!1", "!2", pinnableEvent.getId()],
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unpins event on pin option click when event is pinned", async () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// make the event already pinned in the room
|
||||
const pinEvent = new MatrixEvent({
|
||||
type: EventType.RoomPinnedEvents,
|
||||
room_id: roomId,
|
||||
state_key: "",
|
||||
content: { pinned: [pinnableEvent.getId(), "!another-event"] },
|
||||
});
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.setStateEvents([pinEvent]);
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Unpin" }));
|
||||
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
|
||||
// add to room's pins
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
// pinnableEvent's id removed, other pins intact
|
||||
{ pinned: ["!another-event"] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message forwarding", () => {
|
||||
it("allows forwarding a room message", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a poll", () => {
|
||||
const eventContent = PollStartEvent.from("why?", ["42"], M_POLL_KIND_DISCLOSED);
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not allow forwarding a voice broadcast", () => {
|
||||
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ABC123",
|
||||
);
|
||||
createMenu(broadcastStartEvent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("forwarding beacons", () => {
|
||||
const aliceId = "@alice:server.org";
|
||||
|
||||
it("does not allow forwarding a beacon that is not live", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a beacon that is not live but has a latestLocation", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: deadBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a live beacon that does not have a latestLocation", () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
|
||||
const beacon = new Beacon(beaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(beaconEvent), beacon);
|
||||
createMenu(beaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows forwarding a live beacon that has a location", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens forward dialog with correct event", () => {
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
|
||||
fireEvent.click(document.querySelector('li[aria-label="Forward"]')!);
|
||||
|
||||
// called with forwardableEvent, not beaconInfo event
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: beaconLocation,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("open as map link", () => {
|
||||
it("does not allow opening a plain message in open street maps", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow opening a beacon that does not have a shareable location event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows opening a location event in open street map", () => {
|
||||
const locationEvent = makeLocationEvent("geo:50,50");
|
||||
createMenu(locationEvent);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=50&mlon=50#map=16/50/50",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows opening a beacon that has a shareable location event", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent("@alice", {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("right click", () => {
|
||||
it("copy button does work as expected", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue(text);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]')!;
|
||||
fireEvent.mouseDown(copyButton);
|
||||
expect(copyPlaintext).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it("copy button is not shown when there is nothing to copy", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue("");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]');
|
||||
expect(copyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows edit button when we can edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(true);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show edit button when we cannot edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(false);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows reply button when we can reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show reply button when we cannot reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
const unsentMessage = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
// queued messages are not actionable
|
||||
unsentMessage.setStatus(EventStatus.QUEUED);
|
||||
|
||||
createMenu(unsentMessage, {}, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows react button when we can react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show react button when we cannot react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: false,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows view in room button when the event is a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
mxEvent.getThread = () => ({ rootEvent: mxEvent }) as Thread;
|
||||
const props = {
|
||||
rightClick: true,
|
||||
};
|
||||
const context = {
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
};
|
||||
|
||||
createMenu(mxEvent, props, context);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show view in room button when the event is not a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("creates a new thread on reply in thread click", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
|
||||
createRightClickMenu(mxEvent, context);
|
||||
|
||||
const replyInThreadButton = document.querySelector('li[aria-label="Reply in thread"]')!;
|
||||
fireEvent.click(replyInThreadButton);
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: mxEvent,
|
||||
push: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenu(mxEvent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createMenuWithContent(
|
||||
eventContent: object,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context?: Partial<IRoomState>,
|
||||
): RenderResult {
|
||||
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||
// test is for the Message context menu, it's a fairly safe assumption.
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
return createMenu(mxEvent, props, context);
|
||||
}
|
||||
|
||||
function makeDefaultRoom(): Room {
|
||||
return new Room(roomId, MatrixClientPeg.safeGet(), "@user:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu(
|
||||
mxEvent: MatrixEvent,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context: Partial<IRoomState> = {},
|
||||
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
|
||||
room: Room = makeDefaultRoom(),
|
||||
): RenderResult {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// @ts-ignore illegally set private prop
|
||||
room.currentState.beacons = beacons;
|
||||
|
||||
mxEvent.setStatus(EventStatus.SENT);
|
||||
|
||||
client.getUserId = jest.fn().mockReturnValue("@user:example.com");
|
||||
client.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
return render(
|
||||
<RoomContext.Provider value={context as IRoomState}>
|
||||
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { fireEvent, getByLabelText, render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { ReceiptType, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { ChevronFace } from "../../../../src/components/structures/ContextMenu";
|
||||
import {
|
||||
RoomGeneralContextMenu,
|
||||
RoomGeneralContextMenuProps,
|
||||
} from "../../../../src/components/views/context_menus/RoomGeneralContextMenu";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import RoomListStore from "../../../../src/stores/room-list/RoomListStore";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { mkMessage, stubClient } from "../../../test-utils/test-utils";
|
||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { clearAllModals } from "../../../test-utils";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomGeneralContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
function getComponent(props?: Partial<RoomGeneralContextMenuProps>) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomGeneralContextMenu
|
||||
room={room}
|
||||
onFinished={onFinished}
|
||||
{...props}
|
||||
managed={true}
|
||||
mountAsChild={true}
|
||||
left={1}
|
||||
top={1}
|
||||
chevronFace={ChevronFace.Left}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Favourite,
|
||||
]);
|
||||
|
||||
onFinished = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("renders an empty context menu for archived rooms", async () => {
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([DefaultTagID.Archived]);
|
||||
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the default context menu", async () => {
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not render invite menu item when UIComponent customisations disable room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite menu item when UIComponent customisations enables room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks the room as read", async () => {
|
||||
const event = mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: 1000,
|
||||
});
|
||||
room.addLiveEvents([event], {});
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsReadBtn = getByLabelText(container, "Mark as read");
|
||||
fireEvent.click(markAsReadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(event, ReceiptType.Read, true);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks the room as unread", async () => {
|
||||
room.updateMyMembership("join");
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsUnreadBtn = getByLabelText(container, "Mark as unread");
|
||||
fireEvent.click(markAsUnreadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: true,
|
||||
});
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when developer mode is disabled, it should not render the developer tools option", () => {
|
||||
getComponent();
|
||||
expect(screen.queryByText("Developer tools")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when developer mode is enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode");
|
||||
getComponent();
|
||||
});
|
||||
|
||||
it("should render the developer tools option", async () => {
|
||||
const developerToolsItem = screen.getByRole("menuitem", { name: "Developer tools" });
|
||||
expect(developerToolsItem).toBeInTheDocument();
|
||||
|
||||
// click open developer tools dialog
|
||||
await userEvent.click(developerToolsItem);
|
||||
|
||||
// assert that the dialog is displayed by searching some if its contents
|
||||
expect(await screen.findByText("Toolbox")).toBeInTheDocument();
|
||||
expect(await screen.findByText(`Room ID: ${ROOM_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { prettyDOM, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import SpaceContextMenu from "../../../../src/components/views/context_menus/SpaceContextMenu";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
shouldShowSpaceSettings,
|
||||
showCreateNewRoom,
|
||||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../../../src/utils/space";
|
||||
import { leaveSpace } from "../../../../src/utils/leave-behaviour";
|
||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/utils/space", () => ({
|
||||
shouldShowSpaceSettings: jest.fn(),
|
||||
showCreateNewRoom: jest.fn(),
|
||||
showCreateNewSubspace: jest.fn(),
|
||||
showSpaceInvite: jest.fn(),
|
||||
showSpacePreferences: jest.fn(),
|
||||
showSpaceSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/utils/leave-behaviour", () => ({
|
||||
leaveSpace: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<SpaceContextMenu />", () => {
|
||||
const userId = "@test:server";
|
||||
|
||||
const mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as Mocked<MatrixClient>;
|
||||
|
||||
const makeMockSpace = (props = {}) =>
|
||||
({
|
||||
name: "test space",
|
||||
getJoinRule: jest.fn(),
|
||||
canInvite: jest.fn(),
|
||||
currentState: {
|
||||
maySendStateEvent: jest.fn(),
|
||||
},
|
||||
client: mockClient,
|
||||
getMyMembership: jest.fn(),
|
||||
...props,
|
||||
}) as unknown as Room;
|
||||
|
||||
const defaultProps = {
|
||||
space: makeMockSpace(),
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}): RenderResult =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SpaceContextMenu {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockClient.getUserId.mockReturnValue(userId);
|
||||
mockClient.getSafeUserId.mockReturnValue(userId);
|
||||
});
|
||||
|
||||
it("renders menu correctly", () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(prettyDOM(baseElement)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders invite option when space is public", () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite option when user is has invite rights for space", () => {
|
||||
const space = makeMockSpace({
|
||||
canInvite: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(space.canInvite).toHaveBeenCalledWith(userId);
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens invite dialog when invite option is clicked", async () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("invite-option"));
|
||||
|
||||
expect(showSpaceInvite).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders space settings option when user has rights", () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
renderComponent();
|
||||
expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(screen.getByTestId("settings-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens space settings when space settings option is clicked", async () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("settings-option"));
|
||||
|
||||
expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders leave option when user does not have rights to see space settings", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId("leave-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("leaves space when leave option is clicked", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
await userEvent.click(screen.getByTestId("leave-option"));
|
||||
expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("add children section", () => {
|
||||
const space = makeMockSpace();
|
||||
|
||||
beforeEach(() => {
|
||||
// set space to allow adding children to space
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("does not render section when user does not have permission to add children", () => {
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render section when UIComponent customisations disable room and space creation", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms);
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add room button when UIComponent customisation allows CreateRoom", () => {
|
||||
// only allow CreateRoom
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-room-option")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add space button when UIComponent customisation allows CreateSpace", () => {
|
||||
// only allow CreateSpaces
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-subspace-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens create room dialog on add room button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-room-option"));
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens create space dialog on add space button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-subspace-option"));
|
||||
expect(showCreateNewSubspace).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { getByTestId, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import ThreadListContextMenu, {
|
||||
ThreadListContextMenuProps,
|
||||
} from "../../../../src/components/views/context_menus/ThreadListContextMenu";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||
import { stubClient } from "../../../test-utils/test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("ThreadListContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
function getComponent(props: Partial<ThreadListContextMenuProps>) {
|
||||
return render(<ThreadListContextMenu mxEvent={event} {...props} />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const res = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
event = res.rootEvent;
|
||||
});
|
||||
|
||||
it("does not render the permalink", async () => {
|
||||
const { container } = getComponent({});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).toBeNull();
|
||||
});
|
||||
|
||||
it("does render the permalink", async () => {
|
||||
const { container } = getComponent({
|
||||
permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false),
|
||||
});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { screen, render } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import { WidgetContextMenu } from "../../../../src/components/views/context_menus/WidgetContextMenu";
|
||||
import { IApp } from "../../../../src/stores/WidgetStore";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||
import { ModuleRunner } from "../../../../src/modules/ModuleRunner";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<WidgetContextMenu />", () => {
|
||||
const widgetId = "w1";
|
||||
const eventId = "e1";
|
||||
const roomId = "r1";
|
||||
const userId = "@user-id:server";
|
||||
|
||||
const app: IApp = {
|
||||
id: widgetId,
|
||||
eventId,
|
||||
roomId,
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: userId,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
onFinished = jest.fn();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
||||
mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
it("renders revoke button", async () => {
|
||||
const { rerender } = render(getComponent());
|
||||
|
||||
const revokeButton = screen.getByLabelText("Revoke permissions");
|
||||
expect(revokeButton).toBeInTheDocument();
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
rerender(getComponent());
|
||||
expect(revokeButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("revokes permissions", async () => {
|
||||
render(getComponent());
|
||||
await userEvent.click(screen.getByLabelText("Revoke permissions"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EmbeddedPage /> should render nothing if no url given 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
Couldn't load page
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should translate _t strings 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
<h1>
|
||||
Przeglądaj pokoje
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,97 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomGeneralContextMenu renders an empty context menu for archived rooms 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomGeneralContextMenu renders the default context menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
|
||||
"[36m<body>[39m
|
||||
[36m<div />[39m
|
||||
[36m<div[39m
|
||||
[33mid[39m=[32m"mx_ContextualMenu_Container"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_wrapper"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_background"[39m
|
||||
[36m/>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu"[39m
|
||||
[33mrole[39m=[32m"menu"[39m
|
||||
[36m>[39m
|
||||
[36m<ul[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"[39m
|
||||
[33mrole[39m=[32m"none"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_SpacePanel_contextMenu_header"[39m
|
||||
[36m>[39m
|
||||
[0mtest space[0m
|
||||
[36m</div>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_optionList"[39m
|
||||
[36m>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Space home"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"0"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mSpace home[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Explore rooms"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mExplore rooms[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Preferences"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mPreferences[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Leave space"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"[39m
|
||||
[33mdata-testid[39m=[32m"leave-option"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mLeave space[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m</div>[39m
|
||||
[36m</ul>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</body>[39m"
|
||||
`;
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { SecretStorage, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { act, fireEvent, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { mockPlatformPeg, stubClient } from "../../../test-utils";
|
||||
import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog";
|
||||
|
||||
const securityKey = "EsTc WKmb ivvk jLS7 Y1NH 5CcQ mP1E JJwj B3Fd pFWm t4Dp dbyu";
|
||||
|
||||
describe("AccessSecretStorageDialog", () => {
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
const defaultProps: ComponentProps<typeof AccessSecretStorageDialog> = {
|
||||
keyInfo: {} as any,
|
||||
onFinished: jest.fn(),
|
||||
checkPrivateKey: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}): void => {
|
||||
render(<AccessSecretStorageDialog {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
const enterSecurityKey = (placeholder = "Security Key"): void => {
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByPlaceholderText(placeholder), {
|
||||
target: {
|
||||
value: securityKey,
|
||||
},
|
||||
});
|
||||
// wait for debounce
|
||||
jest.advanceTimersByTime(250);
|
||||
});
|
||||
};
|
||||
|
||||
const submitDialog = async (): Promise<void> => {
|
||||
await userEvent.click(screen.getByText("Continue"), { delay: null });
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
mockPlatformPeg();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = stubClient();
|
||||
});
|
||||
|
||||
it("Closes the dialog when the form is submitted with a valid key", async () => {
|
||||
jest.spyOn(mockClient.secretStorage, "checkKey").mockResolvedValue(true);
|
||||
|
||||
const onFinished = jest.fn();
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||
renderComponent({ onFinished, checkPrivateKey });
|
||||
|
||||
// check that the input field is focused
|
||||
expect(screen.getByPlaceholderText("Security Key")).toHaveFocus();
|
||||
|
||||
await enterSecurityKey();
|
||||
await submitDialog();
|
||||
|
||||
expect(screen.getByText("Looks good!")).toBeInTheDocument();
|
||||
expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: securityKey });
|
||||
expect(onFinished).toHaveBeenCalledWith({ recoveryKey: securityKey });
|
||||
});
|
||||
|
||||
it("Notifies the user if they input an invalid Security Key", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(true);
|
||||
renderComponent({ onFinished, checkPrivateKey });
|
||||
|
||||
jest.spyOn(mockClient.secretStorage, "checkKey").mockImplementation(() => {
|
||||
throw new Error("invalid key");
|
||||
});
|
||||
|
||||
await enterSecurityKey();
|
||||
await submitDialog();
|
||||
|
||||
expect(screen.getByText("Continue")).toBeDisabled();
|
||||
expect(screen.getByText("Invalid Security Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Notifies the user if they input an invalid passphrase", async function () {
|
||||
const keyInfo = {
|
||||
name: "test",
|
||||
algorithm: "test",
|
||||
iv: "test",
|
||||
mac: "1:2:3:4",
|
||||
passphrase: {
|
||||
// this type is weird in js-sdk
|
||||
// cast 'm.pbkdf2' to itself
|
||||
algorithm: "m.pbkdf2" as SecretStorage.PassphraseInfo["algorithm"],
|
||||
iterations: 2,
|
||||
salt: "nonempty",
|
||||
},
|
||||
};
|
||||
const checkPrivateKey = jest.fn().mockResolvedValue(false);
|
||||
renderComponent({ checkPrivateKey, keyInfo });
|
||||
|
||||
await enterSecurityKey("Security Phrase");
|
||||
expect(screen.getByPlaceholderText("Security Phrase")).toHaveValue(securityKey);
|
||||
await submitDialog();
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
|
||||
import { AppDownloadDialog } from "../../../../src/components/views/dialogs/AppDownloadDialog";
|
||||
import SdkConfig, { ConfigOptions } from "../../../../src/SdkConfig";
|
||||
|
||||
describe("AppDownloadDialog", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it("should render with desktop, ios, android, fdroid buttons by default", () => {
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling fdroid build", () => {
|
||||
SdkConfig.add({
|
||||
mobile_builds: {
|
||||
fdroid: null,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling desktop build", () => {
|
||||
SdkConfig.add({
|
||||
desktop_builds: {
|
||||
available: false,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow disabling mobile builds", () => {
|
||||
SdkConfig.add({
|
||||
mobile_builds: {
|
||||
ios: null,
|
||||
android: null,
|
||||
fdroid: null,
|
||||
},
|
||||
} as ConfigOptions);
|
||||
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
|
||||
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Download on the App Store" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { getByText, render, RenderResult } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import AskInviteAnywayDialog, {
|
||||
AskInviteAnywayDialogProps,
|
||||
} from "../../../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("AskInviteaAnywayDialog", () => {
|
||||
const onFinished: jest.Mock<any, any> = jest.fn();
|
||||
const onGiveUp: jest.Mock<any, any> = jest.fn();
|
||||
const onInviteAnyways: jest.Mock<any, any> = jest.fn();
|
||||
|
||||
function renderComponent(props: Partial<AskInviteAnywayDialogProps> = {}): RenderResult {
|
||||
return render(
|
||||
<AskInviteAnywayDialog
|
||||
onFinished={onFinished}
|
||||
onGiveUp={onGiveUp}
|
||||
onInviteAnyways={onInviteAnyways}
|
||||
unknownProfileUsers={[
|
||||
{
|
||||
userId: "@alice:localhost",
|
||||
errorText: "🤷♂️",
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("remembers to not warn again", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (): Promise<void> => {});
|
||||
|
||||
const neverWarnAgainBtn = getByText(container, /never warn/);
|
||||
await userEvent.click(neverWarnAgainBtn);
|
||||
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||
"promptBeforeInviteUnknownUsers",
|
||||
null,
|
||||
expect.any(String),
|
||||
false,
|
||||
);
|
||||
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("invites anyway", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
const inviteAnywayBtn = getByText(container, "Invite anyway");
|
||||
await userEvent.click(inviteAnywayBtn);
|
||||
|
||||
expect(onInviteAnyways).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("gives up", async () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
const closeBtn = getByText(container, /Close/);
|
||||
await userEvent.click(closeBtn);
|
||||
|
||||
expect(onGiveUp).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
|
||||
import ChangelogDialog from "../../../../src/components/views/dialogs/ChangelogDialog";
|
||||
|
||||
describe("<ChangelogDialog />", () => {
|
||||
it("should fetch github proxy url for each repo with old and new version strings", async () => {
|
||||
const webUrl = "https://riot.im/github/repos/element-hq/element-web/compare/oldsha1...newsha1";
|
||||
fetchMock.get(webUrl, {
|
||||
url: "https://api.github.com/repos/element-hq/element-web/compare/master...develop",
|
||||
html_url: "https://github.com/element-hq/element-web/compare/master...develop",
|
||||
permalink_url: "https://github.com/element-hq/element-web/compare/vector-im:72ca95e...vector-im:8891698",
|
||||
diff_url: "https://github.com/element-hq/element-web/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/element-hq/element-web/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 24,
|
||||
behind_by: 0,
|
||||
total_commits: 24,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha",
|
||||
html_url: "https://api.github.com/repos/element-hq/element-web/commit/commit-sha",
|
||||
commit: { message: "This is the first commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
const reactUrl = "https://riot.im/github/repos/element-hq/matrix-react-sdk/compare/oldsha2...newsha2";
|
||||
fetchMock.get(reactUrl, {
|
||||
url: "https://api.github.com/repos/element-hq/matrix-react-sdk/compare/master...develop",
|
||||
html_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop",
|
||||
permalink_url: "https://github.com/element-hq/matrix-react-sdk/compare/matrix-org:cdb00...matrix-org:4a926",
|
||||
diff_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/element-hq/matrix-react-sdk/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 83,
|
||||
behind_by: 0,
|
||||
total_commits: 83,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha0",
|
||||
html_url: "https://api.github.com/repos/element-hq/matrix-react-sdk/commit/commit-sha",
|
||||
commit: { message: "This is a commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
const jsUrl = "https://riot.im/github/repos/matrix-org/matrix-js-sdk/compare/oldsha3...newsha3";
|
||||
fetchMock.get(jsUrl, {
|
||||
url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/compare/master...develop",
|
||||
html_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop",
|
||||
permalink_url: "https://github.com/matrix-org/matrix-js-sdk/compare/matrix-org:6166a8f...matrix-org:fec350",
|
||||
diff_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.diff",
|
||||
patch_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.patch",
|
||||
base_commit: {},
|
||||
merge_base_commit: {},
|
||||
status: "ahead",
|
||||
ahead_by: 48,
|
||||
behind_by: 0,
|
||||
total_commits: 48,
|
||||
commits: [
|
||||
{
|
||||
sha: "commit-sha1",
|
||||
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1",
|
||||
commit: { message: "This is a commit message" },
|
||||
},
|
||||
{
|
||||
sha: "commit-sha2",
|
||||
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2",
|
||||
commit: { message: "This is another commit message" },
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
});
|
||||
|
||||
const newVersion = "newsha1-react-newsha2-js-newsha3";
|
||||
const oldVersion = "oldsha1-react-oldsha2-js-oldsha3";
|
||||
const { asFragment } = render(
|
||||
<ChangelogDialog newVersion={newVersion} version={oldVersion} onFinished={jest.fn()} />,
|
||||
);
|
||||
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
|
||||
expect(fetchMock).toHaveFetched(webUrl);
|
||||
expect(fetchMock).toHaveFetched(reactUrl);
|
||||
expect(fetchMock).toHaveFetched(jsUrl);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { flushPromises, mkEvent, stubClient } from "../../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||
import { createRedactEventDialog } from "../../../../src/components/views/dialogs/ConfirmRedactDialog";
|
||||
|
||||
describe("ConfirmRedactDialog", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let mxEvent: MatrixEvent;
|
||||
|
||||
const setUpVoiceBroadcastStartedEvent = () => {
|
||||
mxEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId()!,
|
||||
client.deviceId!,
|
||||
);
|
||||
};
|
||||
|
||||
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
|
||||
createRedactEventDialog({ mxEvent });
|
||||
// double-flush promises required for the dialog to show up
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(screen.getByTestId("dialog-primary-button"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
});
|
||||
|
||||
it("should raise an error for an event without ID", async () => {
|
||||
mxEvent = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
jest.spyOn(mxEvent, "getId").mockReturnValue(undefined);
|
||||
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow("cannot redact event without ID");
|
||||
});
|
||||
|
||||
it("should raise an error for an event without room-ID", async () => {
|
||||
mxEvent = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
room: roomId,
|
||||
content: {},
|
||||
user: client.getSafeUserId(),
|
||||
});
|
||||
jest.spyOn(mxEvent, "getRoomId").mockReturnValue(undefined);
|
||||
await expect(confirmDeleteVoiceBroadcastStartedEvent()).rejects.toThrow(
|
||||
`cannot redact event ${mxEvent.getId()} without room ID`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when redacting a voice broadcast started event", () => {
|
||||
beforeEach(() => {
|
||||
setUpVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
describe("and the server does not support relation based redactions", () => {
|
||||
beforeEach(() => {
|
||||
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
|
||||
});
|
||||
|
||||
describe("and displaying and confirm the dialog for a voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
await confirmDeleteVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
it("should call redact without `with_rel_types`", () => {
|
||||
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the server supports relation based redactions", () => {
|
||||
beforeEach(() => {
|
||||
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
|
||||
});
|
||||
|
||||
describe("and displaying and confirm the dialog for a voice broadcast", () => {
|
||||
beforeEach(async () => {
|
||||
await confirmDeleteVoiceBroadcastStartedEvent();
|
||||
});
|
||||
|
||||
it("should call redact with `with_rel_types`", () => {
|
||||
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
|
||||
with_rel_types: [RelationType.Reference],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import ConfirmUserActionDialog from "../../../../src/components/views/dialogs/ConfirmUserActionDialog";
|
||||
import { mkRoomMember } from "../../../test-utils";
|
||||
|
||||
describe("ConfirmUserActionDialog", () => {
|
||||
it("renders", () => {
|
||||
const { asFragment } = render(
|
||||
<ConfirmUserActionDialog
|
||||
onFinished={jest.fn()}
|
||||
member={mkRoomMember("123", "@user:test.com", KnownMembership.Join)}
|
||||
action="Ban"
|
||||
title="Ban this " // eg. 'Ban this user?'
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, within } from "jest-matrix-react";
|
||||
import { JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import CreateRoomDialog from "../../../../src/components/views/dialogs/CreateRoomDialog";
|
||||
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<CreateRoomDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getDomain: jest.fn().mockReturnValue("server.org"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
doesServerForceEncryptionForPreset: jest.fn(),
|
||||
// make every alias available
|
||||
getRoomIdForAlias: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
});
|
||||
|
||||
const getE2eeEnableToggleInputElement = () => screen.getByLabelText("Enable end-to-end encryption");
|
||||
// labelled toggle switch doesn't set the disabled attribute, only aria-disabled
|
||||
const getE2eeEnableToggleIsDisabled = () =>
|
||||
getE2eeEnableToggleInputElement().getAttribute("aria-disabled") === "true";
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(false);
|
||||
mockClient.getClientWellKnown.mockReturnValue({});
|
||||
});
|
||||
|
||||
const getComponent = (props = {}) => render(<CreateRoomDialog onFinished={jest.fn()} {...props} />);
|
||||
|
||||
it("should default to private room", async () => {
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Create a private room")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should use defaultName from props", async () => {
|
||||
const defaultName = "My test room";
|
||||
getComponent({ defaultName });
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveDisplayValue(defaultName);
|
||||
});
|
||||
|
||||
describe("for a private room", () => {
|
||||
// default behaviour is a private room
|
||||
|
||||
it("should use server .well-known default for encryption setting", async () => {
|
||||
// default to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should use server .well-known force_disable for encryption setting", async () => {
|
||||
// force to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: true,
|
||||
force_disable: true,
|
||||
},
|
||||
});
|
||||
getComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should use defaultEncrypted prop", async () => {
|
||||
// default to off in server wk
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
// but pass defaultEncrypted prop
|
||||
getComponent({ defaultEncrypted: true });
|
||||
await flushPromises();
|
||||
// encryption enabled
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should use defaultEncrypted prop when it is false", async () => {
|
||||
// default to off in server wk
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
// but pass defaultEncrypted prop
|
||||
getComponent({ defaultEncrypted: false });
|
||||
await flushPromises();
|
||||
// encryption disabled
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
// not forced to off
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should override defaultEncrypted when server .well-known forces disabled encryption", async () => {
|
||||
// force to off
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: true,
|
||||
},
|
||||
});
|
||||
getComponent({ defaultEncrypted: true });
|
||||
await flushPromises();
|
||||
|
||||
// server forces encryption to disabled, even though defaultEncrypted is false
|
||||
expect(getE2eeEnableToggleInputElement()).not.toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("should override defaultEncrypted when server forces enabled encryption", async () => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
|
||||
getComponent({ defaultEncrypted: false });
|
||||
await flushPromises();
|
||||
|
||||
// server forces encryption to enabled, even though defaultEncrypted is true
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should enable encryption toggle and disable field when server forces encryption", async () => {
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(true);
|
||||
getComponent();
|
||||
|
||||
await flushPromises();
|
||||
expect(getE2eeEnableToggleInputElement()).toBeChecked();
|
||||
expect(getE2eeEnableToggleIsDisabled()).toBeTruthy();
|
||||
|
||||
expect(screen.getByText("Your server requires encryption to be enabled in private rooms.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should warn when trying to create a room with an invalid form", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
// didn't submit room
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should create a private room", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
},
|
||||
encryption: true,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a knock room", () => {
|
||||
describe("when feature is disabled", () => {
|
||||
it("should not have the option to create a knock room", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
getComponent();
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when feature is enabled", () => {
|
||||
const onFinished = jest.fn();
|
||||
const roomName = "Test Room Name";
|
||||
|
||||
beforeEach(async () => {
|
||||
onFinished.mockReset();
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(setting) => setting === "feature_ask_to_join",
|
||||
);
|
||||
getComponent({ onFinished });
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
|
||||
});
|
||||
|
||||
it("should have a heading", () => {
|
||||
expect(screen.getByRole("heading")).toHaveTextContent("Create a room");
|
||||
});
|
||||
|
||||
it("should have a hint", () => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should create a knock room with private visibility", async () => {
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
visibility: Visibility.Private,
|
||||
},
|
||||
encryption: true,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a knock room with public visibility", async () => {
|
||||
fireEvent.click(
|
||||
screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
visibility: Visibility.Public,
|
||||
},
|
||||
encryption: true,
|
||||
joinRule: JoinRule.Knock,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a public room", () => {
|
||||
it("should set join rule to public defaultPublic is truthy", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ defaultPublic: true, onFinished });
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Create a public room")).toBeInTheDocument();
|
||||
|
||||
// e2e section is not rendered
|
||||
expect(screen.queryByText("Enable end-to-end encryption")).not.toBeInTheDocument();
|
||||
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
});
|
||||
|
||||
it("should not create a public room without an alias", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
await flushPromises();
|
||||
|
||||
// set to public
|
||||
fireEvent.click(screen.getByLabelText("Room visibility"));
|
||||
fireEvent.click(screen.getByText("Public room"));
|
||||
expect(within(screen.getByLabelText("Room visibility")).findByText("Public room")).toBeTruthy();
|
||||
expect(screen.getByText("Create a public room")).toBeInTheDocument();
|
||||
|
||||
// set name
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
// try to create the room
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
// alias field invalid
|
||||
expect(screen.getByLabelText("Room address").parentElement!).toHaveClass("mx_Field_invalid");
|
||||
|
||||
// didn't submit
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should create a public room", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished, defaultPublic: true });
|
||||
await flushPromises();
|
||||
|
||||
// set name
|
||||
const roomName = "Test Room Name";
|
||||
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
|
||||
|
||||
const roomAlias = "test";
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Room address"), { target: { value: roomAlias } });
|
||||
|
||||
// try to create the room
|
||||
fireEvent.click(screen.getByText("Create room"));
|
||||
await flushPromises();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true, {
|
||||
createOpts: {
|
||||
name: roomName,
|
||||
preset: Preset.PublicChat,
|
||||
room_alias_name: roomAlias,
|
||||
visibility: Visibility.Public,
|
||||
},
|
||||
guestAccess: false,
|
||||
parentSpace: undefined,
|
||||
roomType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { getByLabelText, getAllByLabelText, render } from "jest-matrix-react";
|
||||
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import DevtoolsDialog from "../../../../src/components/views/dialogs/DevtoolsDialog";
|
||||
|
||||
describe("DevtoolsDialog", () => {
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function getComponent(roomId: string, threadRootId: string | null = null, onFinished = () => true) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsDialog roomId={roomId} threadRootId={threadRootId} onFinished={onFinished} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
room = new Room("!id", cli, "@alice:matrix.org");
|
||||
|
||||
jest.spyOn(cli, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders the devtools dialog", () => {
|
||||
const { asFragment } = getComponent(room.roomId);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("copies the roomid", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(navigator.clipboard, "writeText");
|
||||
|
||||
const { container } = getComponent(room.roomId);
|
||||
|
||||
const copyBtn = getByLabelText(container, "Copy");
|
||||
await user.click(copyBtn);
|
||||
const copiedBtn = getByLabelText(container, "Copied!");
|
||||
|
||||
expect(copiedBtn).toBeInTheDocument();
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||
await expect(navigator.clipboard.readText()).resolves.toBe(room.roomId);
|
||||
});
|
||||
|
||||
it("copies the thread root id when provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(navigator.clipboard, "writeText");
|
||||
|
||||
const threadRootId = "$test_event_id_goes_here";
|
||||
const { container } = getComponent(room.roomId, threadRootId);
|
||||
|
||||
const copyBtn = getAllByLabelText(container, "Copy")[1];
|
||||
await user.click(copyBtn);
|
||||
const copiedBtn = getByLabelText(container, "Copied!");
|
||||
|
||||
expect(copiedBtn).toBeInTheDocument();
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||
await expect(navigator.clipboard.readText()).resolves.toBe(threadRootId);
|
||||
});
|
||||
});
|
326
test/unit-tests/components/views/dialogs/ExportDialog-test.tsx
Normal file
326
test/unit-tests/components/views/dialogs/ExportDialog-test.tsx
Normal file
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ExportDialog from "../../../../src/components/views/dialogs/ExportDialog";
|
||||
import { ExportType, ExportFormat } from "../../../../src/utils/exportUtils/exportUtils";
|
||||
import { createTestClient, mkStubRoom } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
|
||||
import ChatExport from "../../../../src/customisations/ChatExport";
|
||||
import PlainTextExporter from "../../../../src/utils/exportUtils/PlainTextExport";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const htmlExporterInstance = {
|
||||
export: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
const plainTextExporterInstance = {
|
||||
export: jest.fn().mockResolvedValue({}),
|
||||
};
|
||||
jest.mock("../../../../src/utils/exportUtils/HtmlExport", () => jest.fn());
|
||||
jest.mock("../../../../src/utils/exportUtils/PlainTextExport", () => jest.fn());
|
||||
|
||||
jest.mock("../../../../src/customisations/ChatExport", () => ({
|
||||
getForceChatExportParameters: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
const ChatExportMock = mocked(ChatExport);
|
||||
const HTMLExporterMock = mocked(HTMLExporter);
|
||||
const PlainTextExporterMock = mocked(PlainTextExporter);
|
||||
|
||||
describe("<ExportDialog />", () => {
|
||||
const mockClient = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
|
||||
const roomId = "test:test.org";
|
||||
const defaultProps = {
|
||||
room: mkStubRoom(roomId, "test", mockClient) as unknown as Room,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) => render(<ExportDialog {...defaultProps} {...props} />);
|
||||
|
||||
const getSizeInput = ({ container }: RenderResult) => container.querySelector('input[id="size-limit"]')!;
|
||||
const getExportTypeInput = ({ container }: RenderResult) => container.querySelector('select[id="export-type"]')!;
|
||||
const getAttachmentsCheckbox = ({ container }: RenderResult) =>
|
||||
container.querySelector('input[id="include-attachments"]')!;
|
||||
const getMessageCountInput = ({ container }: RenderResult) => container.querySelector('input[id="message-count"]')!;
|
||||
const getExportFormatInput = ({ container }: RenderResult, format: ExportFormat) =>
|
||||
container.querySelector(`input[id="exportFormat-${format}"]`)!;
|
||||
const getPrimaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-primary-button")!;
|
||||
const getSecondaryButton = ({ getByTestId }: RenderResult) => getByTestId("dialog-cancel-button")!;
|
||||
|
||||
const submitForm = async (component: RenderResult) => fireEvent.click(getPrimaryButton(component));
|
||||
const selectExportFormat = async (component: RenderResult, format: ExportFormat) =>
|
||||
fireEvent.click(getExportFormatInput(component, format));
|
||||
const selectExportType = async (component: RenderResult, type: ExportType) =>
|
||||
fireEvent.change(getExportTypeInput(component), { target: { value: type } });
|
||||
const setMessageCount = async (component: RenderResult, count: number) =>
|
||||
fireEvent.change(getMessageCountInput(component), { target: { value: count } });
|
||||
|
||||
const setSizeLimit = async (component: RenderResult, limit: number) =>
|
||||
fireEvent.change(getSizeInput(component), { target: { value: limit } });
|
||||
|
||||
beforeEach(() => {
|
||||
HTMLExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(htmlExporterInstance));
|
||||
PlainTextExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(plainTextExporterInstance));
|
||||
htmlExporterInstance.export.mockClear();
|
||||
plainTextExporterInstance.export.mockClear();
|
||||
|
||||
// default setting value
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockClear().mockReturnValue({});
|
||||
});
|
||||
|
||||
it("renders export dialog", () => {
|
||||
const component = getComponent();
|
||||
expect(component.container.querySelector(".mx_ExportDialog")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onFinished when cancel button is clicked", () => {
|
||||
const onFinished = jest.fn();
|
||||
const component = getComponent({ onFinished });
|
||||
fireEvent.click(getSecondaryButton(component));
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("exports room on submit", async () => {
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
// 4th arg is an component function
|
||||
const exportConstructorProps = HTMLExporterMock.mock.calls[0].slice(0, 3);
|
||||
expect(exportConstructorProps).toEqual([
|
||||
defaultProps.room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 8388608, // 8MB to bytes
|
||||
numberOfMessages: 100,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports room using values set from ForceRoomExportParameters", async () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
format: ExportFormat.PlainText,
|
||||
range: ExportType.Beginning,
|
||||
sizeMb: 7000,
|
||||
numberOfMessages: 30,
|
||||
includeAttachments: true,
|
||||
});
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
// 4th arg is an component function
|
||||
const exportConstructorProps = PlainTextExporterMock.mock.calls[0].slice(0, 3);
|
||||
expect(exportConstructorProps).toEqual([
|
||||
defaultProps.room,
|
||||
ExportType.Beginning,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 7000 * 1024 * 1024,
|
||||
numberOfMessages: 30,
|
||||
},
|
||||
]);
|
||||
expect(plainTextExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders success screen when export is finished", async () => {
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(component.container.querySelector(".mx_InfoDialog .mx_Dialog_content")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("export format", () => {
|
||||
it("renders export format with html selected by default", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
|
||||
});
|
||||
|
||||
it("sets export format on radio button click", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportFormat(component, ExportFormat.PlainText);
|
||||
expect(getExportFormatInput(component, ExportFormat.PlainText)).toBeChecked();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("hides export format input when format is valid in ForceRoomExportParameters", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not render export format when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
format: ExportFormat.PlainText,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getExportFormatInput(component, ExportFormat.Html)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("export type", () => {
|
||||
it("renders export type with timeline selected by default", () => {
|
||||
const component = getComponent();
|
||||
expect(getExportTypeInput(component)).toHaveValue(ExportType.Timeline);
|
||||
});
|
||||
|
||||
it("sets export type on change", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.Beginning);
|
||||
expect(getExportTypeInput(component)).toHaveValue(ExportType.Beginning);
|
||||
});
|
||||
|
||||
it("does not render export type when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
range: ExportType.Beginning,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getExportTypeInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not render message count input", async () => {
|
||||
const component = getComponent();
|
||||
expect(getMessageCountInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders message count input with default value 100 when export type is lastNMessages", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
expect(getMessageCountInput(component)).toHaveValue(100);
|
||||
});
|
||||
|
||||
it("sets message count on change", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 10);
|
||||
expect(getMessageCountInput(component)).toHaveValue(10);
|
||||
});
|
||||
|
||||
it("does not export when export type is lastNMessages and message count is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 0);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not export when export type is lastNMessages and message count is more than max", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 99999999999);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports when export type is NOT lastNMessages and message count is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await selectExportType(component, ExportType.LastNMessages);
|
||||
await setMessageCount(component, 0);
|
||||
await selectExportType(component, ExportType.Timeline);
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("size limit", () => {
|
||||
it("renders size limit input with default value", () => {
|
||||
const component = getComponent();
|
||||
expect(getSizeInput(component)).toHaveValue(8);
|
||||
});
|
||||
|
||||
it("updates size limit on change", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 20);
|
||||
expect(getSizeInput(component)).toHaveValue(20);
|
||||
});
|
||||
|
||||
it("does not export when size limit is falsy", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 0);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not export when size limit is larger than max", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 2001);
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports when size limit is max", async () => {
|
||||
const component = getComponent();
|
||||
await setSizeLimit(component, 2000);
|
||||
await submitForm(component);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render size limit input when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
sizeMb: 10000,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getSizeInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
/**
|
||||
* 2000mb size limit does not apply when higher limit is configured in config
|
||||
*/
|
||||
it("exports when size limit set in ForceRoomExportParameters is larger than 2000", async () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
sizeMb: 10000,
|
||||
});
|
||||
const component = getComponent();
|
||||
await submitForm(component);
|
||||
|
||||
expect(htmlExporterInstance.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("include attachments", () => {
|
||||
it("renders input with default value of false", () => {
|
||||
const component = getComponent();
|
||||
expect(getAttachmentsCheckbox(component)).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("updates include attachments on change", async () => {
|
||||
const component = getComponent();
|
||||
fireEvent.click(getAttachmentsCheckbox(component));
|
||||
expect(getAttachmentsCheckbox(component)).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not render input when set in ForceRoomExportParameters", () => {
|
||||
mocked(ChatExportMock.getForceChatExportParameters!).mockReturnValue({
|
||||
includeAttachments: false,
|
||||
});
|
||||
const component = getComponent();
|
||||
expect(getAttachmentsCheckbox(component)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import FeedbackDialog from "../../../../src/components/views/dialogs/FeedbackDialog";
|
||||
|
||||
describe("FeedbackDialog", () => {
|
||||
it("should respect feedback config", () => {
|
||||
SdkConfig.put({
|
||||
feedback: {
|
||||
existing_issues_url: "http://existing?foo=bar",
|
||||
new_issue_url: "https://new.issue.url?foo=bar",
|
||||
},
|
||||
});
|
||||
|
||||
const { asFragment } = render(<FeedbackDialog onFinished={jest.fn()} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
395
test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx
Normal file
395
test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx
Normal file
|
@ -0,0 +1,395 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||
|
||||
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 {
|
||||
MatrixEvent,
|
||||
EventType,
|
||||
LocationAssetType,
|
||||
M_ASSET,
|
||||
M_LOCATION,
|
||||
M_TIMESTAMP,
|
||||
M_TEXT,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { act, fireEvent, getByTestId, render, RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeLegacyLocationEvent,
|
||||
makeLocationEvent,
|
||||
mkEvent,
|
||||
mkMessage,
|
||||
mkStubRoom,
|
||||
mockPlatformPeg,
|
||||
} from "../../../test-utils";
|
||||
import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
// mock offsetParent
|
||||
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
|
||||
get() {
|
||||
return this.parentNode;
|
||||
},
|
||||
});
|
||||
|
||||
describe("ForwardDialog", () => {
|
||||
const sourceRoom = "!111111111111111111:example.org";
|
||||
const aliceId = "@alice:example.org";
|
||||
const defaultMessage = mkMessage({
|
||||
room: sourceRoom,
|
||||
user: aliceId,
|
||||
msg: "Hello world!",
|
||||
event: true,
|
||||
});
|
||||
const accountDataEvent = new MatrixEvent({
|
||||
type: EventType.Direct,
|
||||
sender: aliceId,
|
||||
content: {},
|
||||
});
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(aliceId),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
getRoom: jest.fn(),
|
||||
getAccountData: jest.fn().mockReturnValue(accountDataEvent),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue(""),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getProfileInfo: jest.fn().mockResolvedValue({
|
||||
displayname: "Alice",
|
||||
}),
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
sendEvent: jest.fn(),
|
||||
getClientWellKnown: jest.fn().mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
}),
|
||||
});
|
||||
const defaultRooms = ["a", "A", "b"].map((name) => mkStubRoom(name, name, mockClient));
|
||||
|
||||
const mountForwardDialog = (message = defaultMessage, rooms = defaultRooms) => {
|
||||
mockClient.getVisibleRooms.mockReturnValue(rooms);
|
||||
mockClient.getRoom.mockImplementation((roomId) => rooms.find((room) => room.roomId === roomId) || null);
|
||||
|
||||
const wrapper: RenderResult = render(
|
||||
<ForwardDialog
|
||||
matrixClient={mockClient}
|
||||
event={message}
|
||||
permalinkCreator={new RoomPermalinkCreator(undefined!, sourceRoom)}
|
||||
onFinished={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.clearAllMocks();
|
||||
mockClient.getUserId.mockReturnValue("@bob:example.org");
|
||||
mockClient.getSafeUserId.mockReturnValue("@bob:example.org");
|
||||
mockClient.sendEvent.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(MatrixClientPeg, "get").mockRestore();
|
||||
});
|
||||
|
||||
it("shows a preview with us as the sender", async () => {
|
||||
const { container } = mountForwardDialog();
|
||||
|
||||
expect(screen.queryByText("Hello world!")).toBeInTheDocument();
|
||||
|
||||
// We would just test SenderProfile for the user ID, but it's stubbed
|
||||
const previewAvatar = container.querySelector(".mx_EventTile_avatar .mx_BaseAvatar");
|
||||
expect(previewAvatar?.getAttribute("title")).toBe("@bob:example.org");
|
||||
});
|
||||
|
||||
it("filters the rooms", async () => {
|
||||
const { container } = mountForwardDialog();
|
||||
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(3);
|
||||
|
||||
const searchInput = getByTestId(container, "searchbox-input");
|
||||
await userEvent.type(searchInput, "a");
|
||||
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should be navigable using arrow keys", async () => {
|
||||
const { container } = mountForwardDialog();
|
||||
|
||||
const searchBox = getByTestId(container, "searchbox-input");
|
||||
searchBox.focus();
|
||||
await waitFor(() =>
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")[0]).toHaveClass("mx_ForwardList_entry_active"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("[ArrowDown]");
|
||||
await waitFor(() =>
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("[ArrowDown]");
|
||||
await waitFor(() =>
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")[2]).toHaveClass("mx_ForwardList_entry_active"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("[ArrowUp]");
|
||||
await waitFor(() =>
|
||||
expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("[Enter]");
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledWith("A", "m.room.message", {
|
||||
body: "Hello world!",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
|
||||
it("tracks message sending progress across multiple rooms", async () => {
|
||||
mockPlatformPeg();
|
||||
const { container } = mountForwardDialog();
|
||||
|
||||
// Make sendEvent require manual resolution so we can see the sending state
|
||||
let finishSend: (arg?: any) => void;
|
||||
let cancelSend: () => void;
|
||||
mockClient.sendEvent.mockImplementation(
|
||||
<T extends {}>() =>
|
||||
new Promise<T>((resolve, reject) => {
|
||||
finishSend = resolve;
|
||||
cancelSend = reject;
|
||||
}),
|
||||
);
|
||||
|
||||
let firstButton!: Element;
|
||||
let secondButton!: Element;
|
||||
const update = () => {
|
||||
[firstButton, secondButton] = container.querySelectorAll(".mx_ForwardList_sendButton");
|
||||
};
|
||||
update();
|
||||
|
||||
expect(firstButton.className).toContain("mx_ForwardList_canSend");
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(firstButton);
|
||||
});
|
||||
update();
|
||||
expect(firstButton.className).toContain("mx_ForwardList_sending");
|
||||
|
||||
await act(async () => {
|
||||
cancelSend();
|
||||
// Wait one tick for the button to realize the send failed
|
||||
await sleep(0);
|
||||
});
|
||||
update();
|
||||
expect(firstButton.className).toContain("mx_ForwardList_sendFailed");
|
||||
|
||||
expect(secondButton.className).toContain("mx_ForwardList_canSend");
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(secondButton);
|
||||
});
|
||||
update();
|
||||
expect(secondButton.className).toContain("mx_ForwardList_sending");
|
||||
|
||||
await act(async () => {
|
||||
finishSend();
|
||||
// Wait one tick for the button to realize the send succeeded
|
||||
await sleep(0);
|
||||
});
|
||||
update();
|
||||
expect(secondButton.className).toContain("mx_ForwardList_sent");
|
||||
});
|
||||
|
||||
it("can render replies", async () => {
|
||||
const replyMessage = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "!111111111111111111:example.org",
|
||||
user: "@alice:example.org",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$2222222222222222222222222222222222222222222",
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
mountForwardDialog(replyMessage);
|
||||
|
||||
expect(screen.queryByText("Hi Alice!", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables buttons for rooms without send permissions", async () => {
|
||||
const readOnlyRoom = mkStubRoom("a", "a", mockClient);
|
||||
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
|
||||
const rooms = [readOnlyRoom, mkStubRoom("b", "b", mockClient)];
|
||||
|
||||
const { container } = mountForwardDialog(undefined, rooms);
|
||||
|
||||
const [firstButton, secondButton] = container.querySelectorAll<HTMLButtonElement>(".mx_ForwardList_sendButton");
|
||||
|
||||
expect(firstButton.getAttribute("aria-disabled")).toBeTruthy();
|
||||
expect(secondButton.getAttribute("aria-disabled")).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("Location events", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
const roomId = "a";
|
||||
const geoUri = "geo:51.5076,-0.1276";
|
||||
const legacyLocationEvent = makeLegacyLocationEvent(geoUri);
|
||||
const modernLocationEvent = makeLocationEvent(geoUri);
|
||||
const pinDropLocationEvent = makeLocationEvent(geoUri, LocationAssetType.Pin);
|
||||
|
||||
beforeEach(() => {
|
||||
// legacy events will default timestamp to Date.now()
|
||||
// mock a stable now for easy assertion
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
const sendToFirstRoom = (container: HTMLElement): void =>
|
||||
act(() => {
|
||||
const sendToFirstRoomButton = container.querySelector(".mx_ForwardList_sendButton");
|
||||
fireEvent.click(sendToFirstRoomButton!);
|
||||
});
|
||||
|
||||
it("converts legacy location events to pin drop shares", async () => {
|
||||
const { container } = mountForwardDialog(legacyLocationEvent);
|
||||
|
||||
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
|
||||
sendToFirstRoom(container);
|
||||
|
||||
// text and description from original event are removed
|
||||
// text gets new default message from event values
|
||||
// timestamp is defaulted to now
|
||||
const text = `Location ${geoUri} at ${new Date(now).toISOString()}`;
|
||||
const expectedStrippedContent = {
|
||||
...modernLocationEvent.getContent(),
|
||||
body: text,
|
||||
[M_TEXT.name]: text,
|
||||
[M_TIMESTAMP.name]: now,
|
||||
[M_ASSET.name]: { type: LocationAssetType.Pin },
|
||||
[M_LOCATION.name]: {
|
||||
uri: geoUri,
|
||||
},
|
||||
};
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
legacyLocationEvent.getType(),
|
||||
expectedStrippedContent,
|
||||
);
|
||||
});
|
||||
|
||||
it("removes personal information from static self location shares", async () => {
|
||||
const { container } = mountForwardDialog(modernLocationEvent);
|
||||
|
||||
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
|
||||
sendToFirstRoom(container);
|
||||
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(modernLocationEvent.getContent())!;
|
||||
// text and description from original event are removed
|
||||
// text gets new default message from event values
|
||||
const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
|
||||
const expectedStrippedContent = {
|
||||
...modernLocationEvent.getContent(),
|
||||
body: text,
|
||||
[M_TEXT.name]: text,
|
||||
[M_ASSET.name]: { type: LocationAssetType.Pin },
|
||||
[M_LOCATION.name]: {
|
||||
uri: geoUri,
|
||||
},
|
||||
};
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
modernLocationEvent.getType(),
|
||||
expectedStrippedContent,
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards beacon location as a pin drop event", async () => {
|
||||
const timestamp = 123456;
|
||||
const beaconEvent = makeBeaconEvent("@alice:server.org", { geoUri, timestamp });
|
||||
const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
|
||||
const expectedContent = {
|
||||
msgtype: "m.location",
|
||||
body: text,
|
||||
[M_TEXT.name]: text,
|
||||
[M_ASSET.name]: { type: LocationAssetType.Pin },
|
||||
[M_LOCATION.name]: {
|
||||
uri: geoUri,
|
||||
},
|
||||
geo_uri: geoUri,
|
||||
[M_TIMESTAMP.name]: timestamp,
|
||||
};
|
||||
const { container } = mountForwardDialog(beaconEvent);
|
||||
|
||||
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
|
||||
|
||||
sendToFirstRoom(container);
|
||||
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, EventType.RoomMessage, expectedContent);
|
||||
});
|
||||
|
||||
it("forwards pin drop event", async () => {
|
||||
const { container } = mountForwardDialog(pinDropLocationEvent);
|
||||
|
||||
expect(container.querySelector(".mx_MLocationBody")).toBeTruthy();
|
||||
|
||||
sendToFirstRoom(container);
|
||||
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
pinDropLocationEvent.getType(),
|
||||
pinDropLocationEvent.getContent(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
mockClient.getVisibleRooms.mockClear();
|
||||
mountForwardDialog();
|
||||
expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(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 () => {
|
||||
mockClient.getVisibleRooms.mockClear();
|
||||
mountForwardDialog();
|
||||
expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { act, render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { Mocked } from "jest-mock";
|
||||
import {
|
||||
EmojiMapping,
|
||||
ShowSasCallbacks,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
VerifierEventHandlerMap,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import IncomingSasDialog from "../../../../src/components/views/dialogs/IncomingSasDialog";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
|
||||
describe("IncomingSasDialog", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("shows a spinner at first", () => {
|
||||
const mockVerifier = makeMockVerifier();
|
||||
const { container } = renderComponent(mockVerifier);
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show some emojis once keys are exchanged", () => {
|
||||
const mockVerifier = makeMockVerifier();
|
||||
const { container } = renderComponent(mockVerifier);
|
||||
|
||||
// fire the ShowSas event
|
||||
const sasEvent = makeMockSasCallbacks();
|
||||
act(() => {
|
||||
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
|
||||
});
|
||||
|
||||
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block");
|
||||
expect(emojis.length).toEqual(7);
|
||||
for (const emoji of emojis) {
|
||||
expect(emoji).toHaveTextContent("🦄Unicorn");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function renderComponent(verifier: Verifier, onFinished = () => true) {
|
||||
return render(<IncomingSasDialog verifier={verifier} onFinished={onFinished} />);
|
||||
}
|
||||
|
||||
function makeMockVerifier(): Mocked<Verifier> {
|
||||
const verifier = new TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap>();
|
||||
Object.assign(verifier, {
|
||||
cancel: jest.fn(),
|
||||
});
|
||||
return verifier as unknown as Mocked<Verifier>;
|
||||
}
|
||||
|
||||
function makeMockSasCallbacks(): ShowSasCallbacks {
|
||||
const unicorn: EmojiMapping = ["🦄", "unicorn"];
|
||||
return {
|
||||
sas: {
|
||||
emoji: new Array<EmojiMapping>(7).fill(unicorn),
|
||||
},
|
||||
cancel: jest.fn(),
|
||||
confirm: jest.fn(),
|
||||
mismatch: jest.fn(),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 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 { fireEvent, render, screen, act } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog";
|
||||
import { clearAllModals, flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
|
||||
|
||||
describe("InteractiveAuthDialog", function () {
|
||||
const homeserverUrl = "https://matrix.org";
|
||||
const authUrl = "https://auth.com";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
|
||||
getFallbackAuthUrl: jest.fn().mockReturnValue(authUrl),
|
||||
getHomeserverUrl: jest.fn().mockReturnValue(homeserverUrl),
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
matrixClient: mockClient,
|
||||
makeRequest: jest.fn().mockResolvedValue(undefined),
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}) => render(<InteractiveAuthDialog {...defaultProps} {...props} />);
|
||||
const getPasswordField = () => screen.getByLabelText("Password");
|
||||
const getSubmitButton = () => screen.getByRole("button", { name: "Continue" });
|
||||
|
||||
beforeEach(async function () {
|
||||
jest.clearAllMocks();
|
||||
mockClient.credentials = { userId: null };
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
unmockClientPeg();
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("Should successfully complete a password flow", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
|
||||
|
||||
mockClient.credentials = { userId: "@user:id" };
|
||||
const authData = {
|
||||
session: "sess",
|
||||
flows: [{ stages: ["m.login.password"] }],
|
||||
};
|
||||
|
||||
renderComponent({ makeRequest, onFinished, authData });
|
||||
|
||||
const passwordField = getPasswordField();
|
||||
const submitButton = getSubmitButton();
|
||||
|
||||
expect(passwordField).toBeTruthy();
|
||||
expect(submitButton).toBeTruthy();
|
||||
|
||||
// submit should be disabled
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// put something in the password box
|
||||
await userEvent.type(passwordField, "s3kr3t");
|
||||
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// hit enter; that should trigger a request
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// wait for auth request to resolve
|
||||
await flushPromises();
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledTimes(1);
|
||||
expect(makeRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
session: "sess",
|
||||
type: "m.login.password",
|
||||
password: "s3kr3t",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: "@user:id",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).toHaveBeenCalledWith(true, { a: 1 });
|
||||
});
|
||||
|
||||
describe("SSO flow", () => {
|
||||
it("should close on cancel", () => {
|
||||
const onFinished = jest.fn();
|
||||
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
|
||||
|
||||
mockClient.credentials = { userId: "@user:id" };
|
||||
const authData = {
|
||||
session: "sess",
|
||||
flows: [{ stages: ["m.login.sso"] }],
|
||||
};
|
||||
|
||||
renderComponent({ makeRequest, onFinished, authData });
|
||||
|
||||
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(false, null);
|
||||
});
|
||||
|
||||
it("should complete an sso flow", async () => {
|
||||
jest.spyOn(global.window, "addEventListener");
|
||||
// @ts-ignore
|
||||
jest.spyOn(global.window, "open").mockImplementation(() => {});
|
||||
const onFinished = jest.fn();
|
||||
const successfulResult = { test: 1 };
|
||||
const makeRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new MatrixError({ flows: [{ stages: ["m.login.sso"] }] }, 401))
|
||||
.mockResolvedValue(successfulResult);
|
||||
|
||||
mockClient.credentials = { userId: "@user:id" };
|
||||
const authData = {
|
||||
session: "sess",
|
||||
flows: [{ stages: ["m.login.sso"] }],
|
||||
};
|
||||
|
||||
renderComponent({ makeRequest, onFinished, authData });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("Single Sign On"));
|
||||
|
||||
// no we're on the sso auth screen
|
||||
expect(screen.getByText("Click the button below to confirm your identity.")).toBeInTheDocument();
|
||||
|
||||
// launch sso
|
||||
fireEvent.click(screen.getByText("Confirm"));
|
||||
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
|
||||
|
||||
const onWindowReceiveMessageCall = mocked(window.addEventListener).mock.calls.find(
|
||||
(args) => args[0] === "message",
|
||||
);
|
||||
expect(onWindowReceiveMessageCall).toBeTruthy();
|
||||
// get the handle from SSO auth component
|
||||
// so we can pretend sso auth was completed
|
||||
const onWindowReceiveMessage = onWindowReceiveMessageCall![1];
|
||||
|
||||
// complete sso successfully
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
onWindowReceiveMessage({ data: "authDone", origin: homeserverUrl });
|
||||
});
|
||||
|
||||
// expect(makeRequest).toHaveBeenCalledWith({ session: authData.session })
|
||||
|
||||
// spinner displayed
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
// cancel/confirm buttons hidden while request in progress
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
// nothing in progress
|
||||
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
||||
|
||||
// auth completed, now make the request again with auth
|
||||
fireEvent.click(screen.getByText("Confirm"));
|
||||
// loading while making request
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
|
||||
expect(makeRequest).toHaveBeenCalledTimes(2);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith(true, successfulResult);
|
||||
});
|
||||
});
|
||||
});
|
488
test/unit-tests/components/views/dialogs/InviteDialog-test.tsx
Normal file
488
test/unit-tests/components/views/dialogs/InviteDialog-test.tsx
Normal file
|
@ -0,0 +1,488 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { RoomType, MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
|
||||
import InviteDialog from "../../../../src/components/views/dialogs/InviteDialog";
|
||||
import { InviteKind } from "../../../../src/components/views/dialogs/InviteDialogTypes";
|
||||
import {
|
||||
clearAllModals,
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mkMembership,
|
||||
mkMessage,
|
||||
mkRoomCreateEvent,
|
||||
} from "../../../test-utils";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
||||
import { IConfigOptions } from "../../../../src/IConfigOptions";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
import { IProfileInfo } from "../../../../src/hooks/useProfileInfo";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
|
||||
jest.mock("../../../../src/IdentityAuthClient", () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: mockGetAccessToken,
|
||||
})),
|
||||
);
|
||||
|
||||
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||
__esModule: true,
|
||||
startDmOnFirstMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
const getSearchField = () => screen.getByTestId("invite-dialog-input");
|
||||
|
||||
const enterIntoSearchField = async (value: string) => {
|
||||
const searchField = getSearchField();
|
||||
await userEvent.clear(searchField);
|
||||
await userEvent.type(searchField, value + "{enter}");
|
||||
};
|
||||
|
||||
const pasteIntoSearchField = async (value: string) => {
|
||||
const searchField = getSearchField();
|
||||
await userEvent.clear(searchField);
|
||||
searchField.focus();
|
||||
await userEvent.paste(value);
|
||||
};
|
||||
|
||||
const expectPill = (value: string) => {
|
||||
expect(screen.getByText(value)).toBeInTheDocument();
|
||||
expect(getSearchField()).toHaveValue("");
|
||||
};
|
||||
|
||||
const expectNoPill = (value: string) => {
|
||||
expect(screen.queryByText(value)).not.toBeInTheDocument();
|
||||
expect(getSearchField()).toHaveValue(value);
|
||||
};
|
||||
|
||||
const roomId = "!111111111111111111:example.org";
|
||||
const aliceId = "@alice:example.org";
|
||||
const aliceEmail = "foobar@email.com";
|
||||
const bobId = "@bob:example.org";
|
||||
const bobEmail = "bobbob@example.com"; // bob@example.com is already used as an example in the invite dialog
|
||||
const carolId = "@carol:example.com";
|
||||
const bobbob = "bobbob";
|
||||
|
||||
const aliceProfileInfo: IProfileInfo = {
|
||||
user_id: aliceId,
|
||||
display_name: "Alice",
|
||||
};
|
||||
|
||||
const bobProfileInfo: IProfileInfo = {
|
||||
user_id: bobId,
|
||||
display_name: "Bob",
|
||||
};
|
||||
|
||||
describe("InviteDialog", () => {
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
|
||||
filterConsole(
|
||||
"Error retrieving profile for userId @carol:example.com",
|
||||
"Error retrieving profile for userId @localpart:server.tld",
|
||||
"Error retrieving profile for userId @localpart:server:tld",
|
||||
"[Invite:Recents] Excluding @alice:example.org from recents",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(bobId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(bobId),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
getRoom: jest.fn(),
|
||||
getRooms: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue(""),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getProfileInfo: jest.fn().mockImplementation(async (userId: string) => {
|
||||
if (userId === aliceId) return aliceProfileInfo;
|
||||
if (userId === bobId) return bobProfileInfo;
|
||||
|
||||
throw new MatrixError({
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Profile not found",
|
||||
});
|
||||
}),
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
searchUserDirectory: jest.fn().mockResolvedValue({}),
|
||||
lookupThreePid: jest.fn(),
|
||||
registerWithIdentityServer: jest.fn().mockResolvedValue({
|
||||
access_token: "access_token",
|
||||
token: "token",
|
||||
}),
|
||||
getOpenIdToken: jest.fn().mockResolvedValue({}),
|
||||
getIdentityAccount: jest.fn().mockResolvedValue({}),
|
||||
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
|
||||
supportsThreads: jest.fn().mockReturnValue(false),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(true),
|
||||
getClientWellKnown: jest.fn().mockResolvedValue({}),
|
||||
});
|
||||
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.clearAllMocks();
|
||||
|
||||
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
|
||||
room.addLiveEvents([
|
||||
mkMessage({
|
||||
msg: "Hello",
|
||||
relatesTo: undefined,
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: mockClient.getSafeUserId(),
|
||||
ts: Date.now(),
|
||||
}),
|
||||
]);
|
||||
room.currentState.setStateEvents([
|
||||
mkRoomCreateEvent(bobId, roomId),
|
||||
mkMembership({
|
||||
event: true,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: aliceId,
|
||||
skey: aliceId,
|
||||
}),
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUniqueRoomsWithIndividuals").mockReturnValue({
|
||||
[aliceId]: room,
|
||||
});
|
||||
mockClient.getRooms.mockReturnValue([room]);
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
|
||||
SdkContextClass.instance.client = mockClient;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
SdkContextClass.instance.onLoggedOut();
|
||||
SdkContextClass.instance.client = undefined;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should label with space name", () => {
|
||||
room.isSpaceRoom = jest.fn().mockReturnValue(true);
|
||||
room.getType = jest.fn().mockReturnValue(RoomType.Space);
|
||||
room.name = "Space";
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
expect(screen.queryByText("Invite to Space")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should label with room name", () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
expect(screen.getByText(`Invite to ${roomId}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not suggest valid unknown MXIDs", async () => {
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
initialText="@localpart:server.tld"
|
||||
/>,
|
||||
);
|
||||
await flushPromises();
|
||||
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not suggest invalid MXIDs", () => {
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
initialText="@localpart:server:tld"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("@localpart:server:tld")).toBeFalsy();
|
||||
});
|
||||
|
||||
it.each([[InviteKind.Dm], [InviteKind.Invite]] as [typeof InviteKind.Dm | typeof InviteKind.Invite][])(
|
||||
"should lookup inputs which look like email addresses (%s)",
|
||||
async (kind: typeof InviteKind.Dm | typeof InviteKind.Invite) => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({
|
||||
address: aliceEmail,
|
||||
medium: "email",
|
||||
mxid: aliceId,
|
||||
});
|
||||
mockClient.getProfileInfo.mockResolvedValue({
|
||||
displayname: "Mrs Alice",
|
||||
avatar_url: "mxc://foo/bar",
|
||||
});
|
||||
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={kind}
|
||||
roomId={kind === InviteKind.Invite ? roomId : ""}
|
||||
onFinished={jest.fn()}
|
||||
initialText={aliceEmail}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText("Mrs Alice");
|
||||
// expect the email and MXID to be visible
|
||||
await screen.findByText(aliceId);
|
||||
await screen.findByText(aliceEmail);
|
||||
expect(mockClient.lookupThreePid).toHaveBeenCalledWith("email", aliceEmail, expect.anything());
|
||||
expect(mockClient.getProfileInfo).toHaveBeenCalledWith(aliceId);
|
||||
},
|
||||
);
|
||||
|
||||
it("should suggest e-mail even if lookup fails", async () => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({});
|
||||
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
initialText="foobar@email.com"
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText("foobar@email.com");
|
||||
await screen.findByText("Invite by email");
|
||||
});
|
||||
|
||||
it("should add pasted values", async () => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({});
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
await userEvent.paste(`${bobId} ${aliceEmail}`);
|
||||
|
||||
await screen.findAllByText(bobId);
|
||||
await screen.findByText(aliceEmail);
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
it("should support pasting one username that is not a mx id or email", async () => {
|
||||
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
|
||||
mockClient.lookupThreePid.mockResolvedValue({});
|
||||
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
await userEvent.paste(`${bobbob}`);
|
||||
|
||||
await screen.findAllByText(bobId);
|
||||
expect(input).toHaveValue(`${bobbob}`);
|
||||
});
|
||||
|
||||
it("should allow to invite multiple emails to a room", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
expectPill(aliceEmail);
|
||||
|
||||
await enterIntoSearchField(bobEmail);
|
||||
expectPill(bobEmail);
|
||||
});
|
||||
|
||||
describe("when encryption by default is disabled", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to invite more than one email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
expectPill(aliceEmail);
|
||||
|
||||
await enterIntoSearchField(bobEmail);
|
||||
expectPill(bobEmail);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not allow to invite more than one email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
|
||||
// Start with an email → should convert to a pill
|
||||
await enterIntoSearchField(aliceEmail);
|
||||
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
|
||||
expectPill(aliceEmail);
|
||||
|
||||
// Everything else from now on should not convert to a pill
|
||||
|
||||
await enterIntoSearchField(bobEmail);
|
||||
expectNoPill(bobEmail);
|
||||
|
||||
await enterIntoSearchField(aliceId);
|
||||
expectNoPill(aliceId);
|
||||
|
||||
await pasteIntoSearchField(bobEmail);
|
||||
expectNoPill(bobEmail);
|
||||
});
|
||||
|
||||
it("should not allow to invite a MXID and an email to a DM", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
|
||||
// Start with a MXID → should convert to a pill
|
||||
await enterIntoSearchField(carolId);
|
||||
expect(screen.queryByText("Invites by email can only be sent one at a time")).not.toBeInTheDocument();
|
||||
expectPill(carolId);
|
||||
|
||||
// Add an email → should not convert to a pill
|
||||
await enterIntoSearchField(bobEmail);
|
||||
expect(screen.getByText("Invites by email can only be sent one at a time")).toBeInTheDocument();
|
||||
expectNoPill(bobEmail);
|
||||
});
|
||||
|
||||
it("should start a DM if the profile is available", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
await enterIntoSearchField(aliceId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||
new DirectoryMember({
|
||||
user_id: aliceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not allow pasting the same user multiple times", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
await userEvent.paste(`${bobId}`);
|
||||
await userEvent.paste(`${bobId}`);
|
||||
await userEvent.paste(`${bobId}`);
|
||||
|
||||
expect(input).toHaveValue("");
|
||||
await expect(screen.findAllByText(bobId, { selector: "a" })).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should add to selection on click of user tile", async () => {
|
||||
render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
|
||||
|
||||
const input = screen.getByTestId("invite-dialog-input");
|
||||
input.focus();
|
||||
await userEvent.keyboard(`${aliceId}`);
|
||||
|
||||
const btn = await screen.findByText(aliceId, {
|
||||
selector: ".mx_InviteDialog_tile_nameStack_userId .mx_InviteDialog_tile--room_highlight",
|
||||
});
|
||||
fireEvent.click(btn);
|
||||
|
||||
const tile = await screen.findByText(aliceId, { selector: ".mx_InviteDialog_userTile_name" });
|
||||
expect(tile).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when inviting a user with an unknown profile", () => {
|
||||
beforeEach(async () => {
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
await enterIntoSearchField(carolId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
// Wait for the »invite anyway« modal to show up
|
||||
await screen.findByText("The following users may not exist");
|
||||
});
|
||||
|
||||
it("should not start the DM", () => {
|
||||
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show the »invite anyway« dialog if the profile is not available", () => {
|
||||
expect(screen.getByText("The following users may not exist")).toBeInTheDocument();
|
||||
expect(screen.getByText(`${carolId}: Profile not found`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when clicking »Start DM anyway«", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: "Start DM anyway" }));
|
||||
});
|
||||
|
||||
it("should start the DM", () => {
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||
new DirectoryMember({
|
||||
user_id: carolId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking »Close«", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(startDmOnFirstMessage).mockClear();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Close" }));
|
||||
});
|
||||
|
||||
it("should not start the DM", () => {
|
||||
expect(startDmOnFirstMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when inviting a user with an unknown profile and »promptBeforeInviteUnknownUsers« setting = false", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(startDmOnFirstMessage).mockClear();
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName !== "promptBeforeInviteUnknownUsers",
|
||||
);
|
||||
render(<InviteDialog kind={InviteKind.Dm} onFinished={jest.fn()} />);
|
||||
await enterIntoSearchField(carolId);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
// modal rendering has some weird sleeps - fake timers will mess up the entire test
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
it("should not show the »invite anyway« dialog", () => {
|
||||
expect(screen.queryByText("The following users may not exist")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should start the DM directly", () => {
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||
new DirectoryMember({
|
||||
user_id: carolId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not suggest users from other server when room has m.federate=false", async () => {
|
||||
room.currentState.setStateEvents([mkRoomCreateEvent(bobId, roomId, { "m.federate": false })]);
|
||||
|
||||
render(
|
||||
<InviteDialog
|
||||
kind={InviteKind.Invite}
|
||||
roomId={roomId}
|
||||
onFinished={jest.fn()}
|
||||
initialText="@localpart:server.tld"
|
||||
/>,
|
||||
);
|
||||
await flushPromises();
|
||||
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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 { mocked, MockedObject } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoApi, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../test-utils";
|
||||
import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog";
|
||||
|
||||
describe("LogoutDialog", () => {
|
||||
let mockClient: MockedObject<MatrixClient>;
|
||||
let mockCrypto: MockedObject<CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsCrypto(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
});
|
||||
|
||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
||||
Object.assign(mockCrypto, {
|
||||
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
});
|
||||
|
||||
function renderComponent(props: Partial<React.ComponentProps<typeof LogoutDialog>> = {}): RenderResult {
|
||||
const onFinished = jest.fn();
|
||||
return render(<LogoutDialog onFinished={onFinished} {...props} />);
|
||||
}
|
||||
|
||||
it("shows a regular dialog when crypto is disabled", async () => {
|
||||
mocked(mockClient.getCrypto).mockReturnValue(undefined);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Are you sure you want to sign out?");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows a regular dialog if backups are working", async () => {
|
||||
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Are you sure you want to sign out?");
|
||||
});
|
||||
|
||||
it("Prompts user to connect backup if there is a backup on the server", async () => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Connect this session to Key Backup");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Prompts user to set up backup if there is no backup on the server", async () => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue(null);
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Start using Key Backup");
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when there is an error fetching backups", () => {
|
||||
filterConsole("Unable to fetch key backup status");
|
||||
it("prompts user to set up backup", async () => {
|
||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
||||
throw new Error("beep");
|
||||
});
|
||||
const rendered = renderComponent();
|
||||
await rendered.findByText("Start using Key Backup");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||
import ManageRestrictedJoinRuleDialog from "../../../../src/components/views/dialogs/ManageRestrictedJoinRuleDialog";
|
||||
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
|
||||
describe("<ManageRestrictedJoinRuleDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
const room = new Room("!roomId:server", mockClient, userId);
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
|
||||
const onFinished = jest.fn();
|
||||
const getComponent = (props = {}) =>
|
||||
render(<ManageRestrictedJoinRuleDialog room={room} onFinished={onFinished} {...props} />);
|
||||
|
||||
it("should render empty state", () => {
|
||||
expect(getComponent().asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should list spaces which are not parents of the room", () => {
|
||||
const space1 = new Room("!space:server", mockClient, userId);
|
||||
space1.name = "Other Space";
|
||||
jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([space1]);
|
||||
|
||||
expect(getComponent().asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { Device, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { ManualDeviceKeyVerificationDialog } from "../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
describe("ManualDeviceKeyVerificationDialog", () => {
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ManualDeviceKeyVerificationDialog userId={userId} device={device} onFinished={onLegacyFinished} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = stubClient();
|
||||
});
|
||||
|
||||
it("should display the device", () => {
|
||||
// When
|
||||
const deviceId = "XYZ";
|
||||
const device = new Device({
|
||||
userId: mockClient.getUserId()!,
|
||||
deviceId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
|
||||
});
|
||||
const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn());
|
||||
|
||||
// Then
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the device of another user", () => {
|
||||
// When
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "XYZ";
|
||||
const device = new Device({
|
||||
userId,
|
||||
deviceId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
|
||||
});
|
||||
const { container } = renderDialog(userId, device, jest.fn());
|
||||
|
||||
// Then
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call onFinished and matrixClient.setDeviceVerified", () => {
|
||||
// When
|
||||
const deviceId = "XYZ";
|
||||
const device = new Device({
|
||||
userId: mockClient.getUserId()!,
|
||||
deviceId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
renderDialog(mockClient.getUserId()!, device, onFinished);
|
||||
|
||||
screen.getByRole("button", { name: "Verify session" }).click();
|
||||
|
||||
// Then
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true);
|
||||
});
|
||||
|
||||
it("should call onFinished and not matrixClient.setDeviceVerified", () => {
|
||||
// When
|
||||
const deviceId = "XYZ";
|
||||
const device = new Device({
|
||||
userId: mockClient.getUserId()!,
|
||||
deviceId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
renderDialog(mockClient.getUserId()!, device, onFinished);
|
||||
|
||||
screen.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Then
|
||||
expect(onFinished).toHaveBeenCalledWith(false);
|
||||
expect(mockClient.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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, RenderResult, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { flushPromises, mkMessage, stubClient } from "../../../test-utils";
|
||||
import MessageEditHistoryDialog from "../../../../src/components/views/dialogs/MessageEditHistoryDialog";
|
||||
|
||||
describe("<MessageEditHistory />", () => {
|
||||
const roomId = "!aroom:example.com";
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||
event = mkMessage({
|
||||
event: true,
|
||||
user: "@user:example.com",
|
||||
room: "!room:example.com",
|
||||
msg: "My Great Message",
|
||||
});
|
||||
});
|
||||
|
||||
async function renderComponent(): Promise<RenderResult> {
|
||||
const result = render(<MessageEditHistoryDialog mxEvent={event} onFinished={jest.fn()} />);
|
||||
await waitForElementToBeRemoved(() => result.queryByRole("progressbar"));
|
||||
await flushPromises();
|
||||
return result;
|
||||
}
|
||||
|
||||
function mockEdits(...edits: { msg: string; ts?: number }[]) {
|
||||
client.relations.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
events: edits.map(
|
||||
(e) =>
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: roomId,
|
||||
origin_server_ts: e.ts ?? 0,
|
||||
content: {
|
||||
body: e.msg,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it("should match the snapshot", async () => {
|
||||
mockEdits({ msg: "My Great Massage", ts: 1234 });
|
||||
|
||||
const { container } = await renderComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should support events with", async () => {
|
||||
mockEdits(
|
||||
{ msg: "My Great Massage", ts: undefined },
|
||||
{ msg: "My Great Massage?", ts: undefined },
|
||||
{ msg: "My Great Missage", ts: undefined },
|
||||
);
|
||||
|
||||
const { container } = await renderComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
EventTimeline,
|
||||
EventType,
|
||||
JoinRule,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomStateEvent,
|
||||
Visibility,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||
import RoomSettingsDialog from "../../../../src/components/views/dialogs/RoomSettingsDialog";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
|
||||
describe("<RoomSettingsDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
isRoomEncrypted: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn(),
|
||||
getDomain: jest.fn().mockReturnValue("server.org"),
|
||||
getLocalAliases: jest.fn().mockResolvedValue({ aliases: [] }),
|
||||
getRoomDirectoryVisibility: jest.fn().mockResolvedValue({ visibility: Visibility.Private }),
|
||||
getOrCreateFilter: jest.fn(),
|
||||
});
|
||||
|
||||
const roomId = "!room:server.org";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
room.name = "Test Room";
|
||||
const room2 = new Room("!room2:server.org", mockClient, userId);
|
||||
room2.name = "Another Room";
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue");
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockClient.getRoom.mockImplementation((roomId) => {
|
||||
if (roomId === room.roomId) return room;
|
||||
if (roomId === room2.roomId) return room2;
|
||||
return null;
|
||||
});
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
});
|
||||
|
||||
const getComponent = (onFinished = jest.fn(), propRoomId = roomId) =>
|
||||
render(<RoomSettingsDialog roomId={propRoomId} onFinished={onFinished} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
it("catches errors when room is not found", () => {
|
||||
getComponent(undefined, "!room-that-does-not-exist");
|
||||
expect(screen.getByText("Something went wrong!")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates when roomId prop changes", () => {
|
||||
const { rerender, getByText } = getComponent(undefined, roomId);
|
||||
|
||||
expect(getByText(`Room Settings - ${room.name}`)).toBeInTheDocument();
|
||||
|
||||
rerender(<RoomSettingsDialog roomId={room2.roomId} onFinished={jest.fn()} />);
|
||||
|
||||
expect(getByText(`Room Settings - ${room2.name}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Settings tabs", () => {
|
||||
it("renders default tabs correctly", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("people settings tab", () => {
|
||||
it("does not render when disabled and room join rule is not knock", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
getComponent();
|
||||
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when disabled and room join rule is knock", () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
getComponent();
|
||||
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when enabled and room join rule is not knock", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(setting) => setting === "feature_ask_to_join",
|
||||
);
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
getComponent();
|
||||
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders when enabled and room join rule is knock", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(setting) => setting === "feature_ask_to_join",
|
||||
);
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
getComponent();
|
||||
expect(screen.getByTestId("settings-tab-ROOM_PEOPLE_TAB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-renders on room join rule changes", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(setting) => setting === "feature_ask_to_join",
|
||||
);
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||
getComponent();
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
mockClient.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({ content: {}, type: EventType.RoomJoinRules }),
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
null,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders voip settings tab when enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_group_calls",
|
||||
);
|
||||
getComponent();
|
||||
expect(screen.getByTestId("settings-tab-ROOM_VOIP_TAB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders bridges settings tab when enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_bridge_state",
|
||||
);
|
||||
getComponent();
|
||||
expect(screen.getByTestId("settings-tab-ROOM_BRIDGES_TAB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders advanced settings tab when enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === UIFeature.AdvancedSettings,
|
||||
);
|
||||
getComponent();
|
||||
expect(screen.getByTestId("settings-tab-ROOM_ADVANCED_TAB")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("poll history", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getOrCreateFilter.mockResolvedValue("filterId");
|
||||
});
|
||||
it("renders poll history tab", () => {
|
||||
getComponent();
|
||||
expect(screen.getByTestId("settings-tab-ROOM_POLL_HISTORY_TAB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays poll history when tab clicked", () => {
|
||||
const { container } = getComponent();
|
||||
|
||||
fireEvent.click(screen.getByText("Polls"));
|
||||
|
||||
expect(container.querySelector(".mx_SettingsTab")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import ServerPickerDialog from "../../../../src/components/views/dialogs/ServerPickerDialog";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import { flushPromises } from "../../../test-utils";
|
||||
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
|
||||
|
||||
/** 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("<ServerPickerDialog />", () => {
|
||||
const defaultServerConfig = {
|
||||
hsUrl: "https://matrix.org",
|
||||
hsName: "matrix.org",
|
||||
hsNameIsDifferent: true,
|
||||
isUrl: "https://is.org",
|
||||
isDefault: true,
|
||||
isNameResolvable: true,
|
||||
warning: "",
|
||||
};
|
||||
const wkHsUrl = "https://hsbaseurlfrom.wk";
|
||||
const wkIsUrl = "https://isbaseurlfrom.wk";
|
||||
const validWellKnown = {
|
||||
"m.homeserver": {
|
||||
base_url: wkHsUrl,
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: wkIsUrl,
|
||||
},
|
||||
};
|
||||
const defaultProps = {
|
||||
serverConfig: defaultServerConfig,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
const getComponent = (
|
||||
props: Partial<{
|
||||
onFinished: any;
|
||||
serverConfig: ValidatedServerConfig;
|
||||
}> = {},
|
||||
) => render(<ServerPickerDialog {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(() => {
|
||||
SdkConfig.add({
|
||||
validated_server_config: defaultServerConfig,
|
||||
});
|
||||
|
||||
fetchMock.resetHistory();
|
||||
fetchMock.catch({
|
||||
status: 404,
|
||||
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should render dialog", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// checkbox and text input have the same aria-label
|
||||
const getOtherHomeserverCheckBox = () =>
|
||||
screen.getAllByLabelText("Other homeserver").find((node) => node.getAttribute("type") === "radio")!;
|
||||
const getOtherHomeserverInput = () =>
|
||||
screen.getAllByLabelText("Other homeserver").find((node) => node.getAttribute("type") === "text")!;
|
||||
|
||||
describe("when default server config is selected", () => {
|
||||
it("should select other homeserver field on open", () => {
|
||||
getComponent();
|
||||
expect(getOtherHomeserverCheckBox()).toBeChecked();
|
||||
// empty field
|
||||
expect(getOtherHomeserverInput()).toHaveDisplayValue("");
|
||||
});
|
||||
|
||||
it("should display an error when trying to continue with an empty homeserver field", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const { container } = getComponent({ onFinished });
|
||||
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// error on field
|
||||
expect(container.querySelector(".mx_ServerPickerDialog_otherHomeserver.mx_Field_invalid")).toBeTruthy();
|
||||
|
||||
// didn't close dialog
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should close when selecting default homeserver and clicking continue", async () => {
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
|
||||
fireEvent.click(screen.getByTestId("defaultHomeserver"));
|
||||
expect(screen.getByTestId("defaultHomeserver")).toBeChecked();
|
||||
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
// closed dialog with default server
|
||||
expect(onFinished).toHaveBeenCalledWith(defaultServerConfig);
|
||||
});
|
||||
|
||||
it("should allow user to revert from a custom server to the default", async () => {
|
||||
fetchMock.get(`https://custom.org/_matrix/client/versions`, {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
|
||||
const onFinished = jest.fn();
|
||||
const serverConfig = {
|
||||
hsUrl: "https://custom.org",
|
||||
hsName: "custom.org",
|
||||
hsNameIsDifferent: true,
|
||||
isUrl: "https://is.org",
|
||||
isDefault: false,
|
||||
isNameResolvable: true,
|
||||
warning: "",
|
||||
};
|
||||
getComponent({ onFinished, serverConfig });
|
||||
|
||||
fireEvent.click(screen.getByTestId("defaultHomeserver"));
|
||||
expect(screen.getByTestId("defaultHomeserver")).toBeChecked();
|
||||
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
await flushPromises();
|
||||
|
||||
// closed dialog with default server and nothing else
|
||||
expect(onFinished).toHaveBeenCalledWith(defaultServerConfig);
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should submit successfully with a valid custom homeserver", async () => {
|
||||
const homeserver = "https://myhomeserver.site";
|
||||
fetchMock.get(`${homeserver}/_matrix/client/versions`, {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
|
||||
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
|
||||
expect(getOtherHomeserverInput()).toHaveDisplayValue(homeserver);
|
||||
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
// validation on submit is async
|
||||
await flushPromises();
|
||||
|
||||
// closed dialog with validated custom server
|
||||
expect(onFinished).toHaveBeenCalledWith({
|
||||
hsName: "myhomeserver.site",
|
||||
hsUrl: homeserver,
|
||||
hsNameIsDifferent: false,
|
||||
warning: null,
|
||||
isDefault: false,
|
||||
isNameResolvable: false,
|
||||
isUrl: defaultServerConfig.isUrl,
|
||||
});
|
||||
});
|
||||
|
||||
describe("validates custom homeserver", () => {
|
||||
it("should lookup .well-known for homeserver without protocol", async () => {
|
||||
const homeserver = "myhomeserver1.site";
|
||||
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
|
||||
fetchMock.get(wellKnownUrl, {});
|
||||
getComponent();
|
||||
|
||||
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
|
||||
expect(getOtherHomeserverInput()).toHaveDisplayValue(homeserver);
|
||||
// trigger validation
|
||||
fireEvent.blur(getOtherHomeserverInput());
|
||||
|
||||
// validation on submit is async
|
||||
await flushPromises();
|
||||
|
||||
expect(fetchMock).toHaveFetched(wellKnownUrl);
|
||||
});
|
||||
|
||||
it("should submit using validated config from a valid .well-known", async () => {
|
||||
const homeserver = "myhomeserver2.site";
|
||||
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
|
||||
|
||||
// urls from homeserver well-known
|
||||
const versionsUrl = `${wkHsUrl}/_matrix/client/versions`;
|
||||
const isWellKnownUrl = `${wkIsUrl}/_matrix/identity/v2`;
|
||||
|
||||
fetchMock.getOnce(wellKnownUrl, validWellKnown);
|
||||
fetchMock.getOnce(versionsUrl, {
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
fetchMock.getOnce(isWellKnownUrl, {});
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
|
||||
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
// validation on submit is async
|
||||
await flushPromises();
|
||||
|
||||
expect(fetchMock).toHaveFetched(wellKnownUrl);
|
||||
// fetched using urls from .well-known
|
||||
expect(fetchMock).toHaveFetched(versionsUrl);
|
||||
expect(fetchMock).toHaveFetched(isWellKnownUrl);
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith({
|
||||
hsName: homeserver,
|
||||
hsUrl: wkHsUrl,
|
||||
hsNameIsDifferent: true,
|
||||
warning: null,
|
||||
isDefault: false,
|
||||
isNameResolvable: true,
|
||||
isUrl: wkIsUrl,
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("should fall back to static config when well-known lookup fails", async () => {
|
||||
const homeserver = "myhomeserver3.site";
|
||||
// this server returns 404 for well-known
|
||||
const wellKnownUrl = `https://${homeserver}/.well-known/matrix/client`;
|
||||
fetchMock.get(wellKnownUrl, { status: 404 });
|
||||
// but is otherwise live (happy versions response)
|
||||
fetchMock.get(`https://${homeserver}/_matrix/client/versions`, {
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
|
||||
fireEvent.change(getOtherHomeserverInput(), { target: { value: homeserver } });
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
// validation on submit is async
|
||||
await flushPromises();
|
||||
|
||||
expect(fetchMock).toHaveFetched(wellKnownUrl);
|
||||
expect(fetchMock).toHaveFetched(`https://${homeserver}/_matrix/client/versions`);
|
||||
|
||||
expect(onFinished).toHaveBeenCalledWith({
|
||||
hsName: homeserver,
|
||||
hsUrl: "https://" + homeserver,
|
||||
hsNameIsDifferent: false,
|
||||
warning: null,
|
||||
isDefault: false,
|
||||
isNameResolvable: false,
|
||||
isUrl: defaultServerConfig.isUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
117
test/unit-tests/components/views/dialogs/ShareDialog-test.tsx
Normal file
117
test/unit-tests/components/views/dialogs/ShareDialog-test.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
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 { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { render, RenderOptions } from "jest-matrix-react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../src/languageHandler";
|
||||
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
|
||||
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
jest.mock("../../../../src/utils/ShieldUtils");
|
||||
|
||||
function getWrapper(): RenderOptions {
|
||||
return {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe("ShareDialog", () => {
|
||||
let room: Room;
|
||||
|
||||
const ROOM_ID = "!1:example.org";
|
||||
|
||||
beforeEach(async () => {
|
||||
stubClient();
|
||||
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders room share dialog", () => {
|
||||
const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||
expect(withoutEvents).toHaveTextContent(_t("share|title_room"));
|
||||
|
||||
jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline);
|
||||
const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||
expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent"));
|
||||
});
|
||||
|
||||
it("renders user share dialog", () => {
|
||||
mockRoomMembers(room, 1);
|
||||
const { container } = render(
|
||||
<ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />,
|
||||
getWrapper(),
|
||||
);
|
||||
expect(container).toHaveTextContent(_t("share|title_user"));
|
||||
});
|
||||
|
||||
it("renders link share dialog", () => {
|
||||
mockRoomMembers(room, 1);
|
||||
const { container } = render(
|
||||
<ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />,
|
||||
getWrapper(),
|
||||
);
|
||||
expect(container).toHaveTextContent(_t("share|title_link"));
|
||||
});
|
||||
|
||||
it("renders the QR code if configured", () => {
|
||||
const originalGetValue = SettingsStore.getValue;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
|
||||
if (feature === UIFeature.ShareQRCode) return true;
|
||||
return originalGetValue(feature);
|
||||
});
|
||||
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0;
|
||||
expect(qrCodesVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the social button if configured", () => {
|
||||
const originalGetValue = SettingsStore.getValue;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
|
||||
if (feature === UIFeature.ShareSocial) return true;
|
||||
return originalGetValue(feature);
|
||||
});
|
||||
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0;
|
||||
expect(qrCodesVisible).toBe(true);
|
||||
});
|
||||
it("renders custom title and subtitle", () => {
|
||||
const { container } = render(
|
||||
<ShareDialog
|
||||
target={room}
|
||||
customTitle="test_title_123"
|
||||
subtitle="custom_subtitle_1234"
|
||||
onFinished={jest.fn()}
|
||||
/>,
|
||||
getWrapper(),
|
||||
);
|
||||
expect(container).toHaveTextContent("test_title_123");
|
||||
expect(container).toHaveTextContent("custom_subtitle_1234");
|
||||
});
|
||||
});
|
||||
/**
|
||||
*
|
||||
* @param count the number of users to create
|
||||
*/
|
||||
function mockRoomMembers(room: Room, count: number) {
|
||||
const members = Array(count)
|
||||
.fill(0)
|
||||
.map((_, index) => new RoomMember(room.roomId, "@alice:example.org"));
|
||||
|
||||
room.currentState.setJoinedMemberCount(members.length);
|
||||
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||
}
|
|
@ -0,0 +1,643 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import {
|
||||
ConnectionError,
|
||||
IProtocol,
|
||||
IPublicRoomsChunkRoom,
|
||||
JoinRule,
|
||||
MatrixClient,
|
||||
Room,
|
||||
RoomMember,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
|
||||
import SpotlightDialog from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
|
||||
import { Filter } from "../../../../src/components/views/dialogs/spotlight/Filter";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { flushPromisesWithFakeTimers, mkRoom, stubClient } from "../../../test-utils";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.mock("../../../../src/utils/Feedback");
|
||||
|
||||
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||
startDmOnFirstMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/dispatcher/dispatcher", () => ({
|
||||
register: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
interface IUserChunkMember {
|
||||
user_id: string;
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
interface MockClientOptions {
|
||||
userId?: string;
|
||||
homeserver?: string;
|
||||
thirdPartyProtocols?: Record<string, IProtocol>;
|
||||
rooms?: IPublicRoomsChunkRoom[];
|
||||
members?: RoomMember[];
|
||||
users?: IUserChunkMember[];
|
||||
}
|
||||
|
||||
function mockClient({
|
||||
userId = "testuser",
|
||||
homeserver = "example.tld",
|
||||
thirdPartyProtocols = {},
|
||||
rooms = [],
|
||||
members = [],
|
||||
users = [],
|
||||
}: MockClientOptions = {}): MatrixClient {
|
||||
stubClient();
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
cli.getUserId = jest.fn(() => userId);
|
||||
cli.getDomain = jest.fn(() => homeserver);
|
||||
cli.getHomeserverUrl = jest.fn(() => homeserver);
|
||||
cli.getThirdpartyProtocols = jest.fn(() => Promise.resolve(thirdPartyProtocols));
|
||||
cli.publicRooms = jest.fn((options) => {
|
||||
const searchTerm = options?.filter?.generic_search_term?.toLowerCase();
|
||||
const chunk = rooms.filter(
|
||||
(it) =>
|
||||
!searchTerm ||
|
||||
it.room_id.toLowerCase().includes(searchTerm) ||
|
||||
it.name?.toLowerCase().includes(searchTerm) ||
|
||||
sanitizeHtml(it?.topic || "", { allowedTags: [] })
|
||||
.toLowerCase()
|
||||
.includes(searchTerm) ||
|
||||
it.canonical_alias?.toLowerCase().includes(searchTerm) ||
|
||||
it.aliases?.find((alias) => alias.toLowerCase().includes(searchTerm)),
|
||||
);
|
||||
return Promise.resolve({
|
||||
chunk,
|
||||
total_room_count_estimate: chunk.length,
|
||||
});
|
||||
});
|
||||
cli.searchUserDirectory = jest.fn(({ term, limit }) => {
|
||||
const searchTerm = term?.toLowerCase();
|
||||
const results = users.filter(
|
||||
(it) =>
|
||||
!searchTerm ||
|
||||
it.user_id.toLowerCase().includes(searchTerm) ||
|
||||
it.display_name?.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
return Promise.resolve({
|
||||
results: results.slice(0, limit ?? +Infinity),
|
||||
limited: !!limit && limit < results.length,
|
||||
});
|
||||
});
|
||||
cli.getProfileInfo = jest.fn(async (userId) => {
|
||||
const member = members.find((it) => it.userId === userId);
|
||||
if (member) {
|
||||
return Promise.resolve({
|
||||
displayname: member.rawDisplayName,
|
||||
avatar_url: member.getMxcAvatarUrl(),
|
||||
});
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
return cli;
|
||||
}
|
||||
|
||||
describe("Spotlight Dialog", () => {
|
||||
const testPerson: IUserChunkMember = {
|
||||
user_id: "@janedoe:matrix.org",
|
||||
display_name: "Jane Doe",
|
||||
avatar_url: undefined,
|
||||
};
|
||||
|
||||
const testPublicRoom: IPublicRoomsChunkRoom = {
|
||||
room_id: "!room247:matrix.org",
|
||||
name: "Room #247",
|
||||
topic: "We hope you'll have a <b>shining</b> experience!",
|
||||
world_readable: false,
|
||||
num_joined_members: 1,
|
||||
guest_can_join: false,
|
||||
};
|
||||
|
||||
const testDMRoomId = "!testDM:example.com";
|
||||
const testDMUserId = "@alice:matrix.org";
|
||||
|
||||
let testRoom: Room;
|
||||
let testDM: Room;
|
||||
let testLocalRoom: LocalRoom;
|
||||
|
||||
let mockedClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedClient = mockClient({ rooms: [testPublicRoom], users: [testPerson] });
|
||||
testRoom = mkRoom(mockedClient, "!test23:example.com");
|
||||
mocked(testRoom.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
testLocalRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test23", mockedClient, mockedClient.getUserId()!);
|
||||
testLocalRoom.updateMyMembership(KnownMembership.Join);
|
||||
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom]);
|
||||
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
|
||||
testDM = mkRoom(mockedClient, testDMRoomId);
|
||||
testDM.name = "Chat with Alice";
|
||||
mocked(testDM.getMyMembership).mockReturnValue(KnownMembership.Join);
|
||||
|
||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId: string) => {
|
||||
if (roomId === testDMRoomId) {
|
||||
return testDMUserId;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom, testDM]);
|
||||
});
|
||||
|
||||
describe("should apply filters supplied via props", () => {
|
||||
it("without filter", async () => {
|
||||
render(<SpotlightDialog onFinished={() => null} />);
|
||||
|
||||
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
|
||||
expect(filterChip).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("with public room filter", async () => {
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip.innerHTML).toContain("Public rooms");
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBe(1);
|
||||
expect(options[0].innerHTML).toContain(testPublicRoom.name);
|
||||
});
|
||||
|
||||
it("with people filter", async () => {
|
||||
render(
|
||||
<SpotlightDialog
|
||||
initialFilter={Filter.People}
|
||||
initialText={testPerson.display_name}
|
||||
onFinished={() => null}
|
||||
/>,
|
||||
);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip.innerHTML).toContain("People");
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(1);
|
||||
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when MSC3946 dynamic room predecessors is enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, excludeDefault) => {
|
||||
if (settingName === "feature_dynamic_room_predecessors") {
|
||||
return true;
|
||||
} else {
|
||||
return []; // SpotlightSearch.recentSearches
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => {
|
||||
render(<SpotlightDialog onFinished={() => null} />);
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should apply manually selected filter", () => {
|
||||
it("with public rooms", async () => {
|
||||
render(<SpotlightDialog onFinished={() => null} />);
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
fireEvent.click(screen.getByText("Public rooms"));
|
||||
// wrapper.find("#mx_SpotlightDialog_button_explorePublicRooms").first().simulate("click");
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip.innerHTML).toContain("Public rooms");
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBe(1);
|
||||
expect(options[0]!.innerHTML).toContain(testPublicRoom.name);
|
||||
|
||||
// assert that getVisibleRooms is called without MSC3946 dynamic room predecessors
|
||||
expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(false);
|
||||
});
|
||||
it("with people", async () => {
|
||||
render(<SpotlightDialog initialText={testPerson.display_name} onFinished={() => null} />);
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
fireEvent.click(screen.getByText("People"));
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip.innerHTML).toContain("People");
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(1);
|
||||
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should allow clearing filter manually", () => {
|
||||
it("with public room filter", async () => {
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
let filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip.innerHTML).toContain("Public rooms");
|
||||
|
||||
fireEvent.click(filterChip.querySelector("div.mx_SpotlightDialog_filter--close")!);
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
filterChip = document.querySelector("div.mx_SpotlightDialog_filter")!;
|
||||
expect(filterChip).not.toBeInTheDocument();
|
||||
});
|
||||
it("with people filter", async () => {
|
||||
render(
|
||||
<SpotlightDialog
|
||||
initialFilter={Filter.People}
|
||||
initialText={testPerson.display_name}
|
||||
onFinished={() => null}
|
||||
/>,
|
||||
);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
let filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
|
||||
expect(filterChip).toBeInTheDocument();
|
||||
expect(filterChip!.innerHTML).toContain("People");
|
||||
|
||||
fireEvent.click(filterChip!.querySelector("div.mx_SpotlightDialog_filter--close")!);
|
||||
jest.advanceTimersByTime(1);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
filterChip = document.querySelector("div.mx_SpotlightDialog_filter");
|
||||
expect(filterChip).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("searching for rooms", () => {
|
||||
let options: NodeListOf<Element>;
|
||||
|
||||
beforeAll(async () => {
|
||||
render(<SpotlightDialog initialText="test23" onFinished={() => null} />);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
});
|
||||
|
||||
it("should find Rooms", () => {
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]!.innerHTML).toContain(testRoom.name);
|
||||
});
|
||||
|
||||
it("should not find LocalRooms", () => {
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]!.innerHTML).not.toContain(testLocalRoom.name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not filter out users sent by the server", async () => {
|
||||
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
|
||||
results: [
|
||||
{ user_id: "@user1:server", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
|
||||
{ user_id: "@user2:server", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
|
||||
],
|
||||
limited: false,
|
||||
});
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.People} initialText="Alpha" onFinished={() => null} />);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(2);
|
||||
expect(options[0]).toHaveTextContent("User Alpha");
|
||||
expect(options[1]).toHaveTextContent("User Beta");
|
||||
});
|
||||
|
||||
it("should not filter out users sent by the server even if a local suggestion gets filtered out", async () => {
|
||||
const member = new RoomMember(testRoom.roomId, testPerson.user_id);
|
||||
member.name = member.rawDisplayName = testPerson.display_name!;
|
||||
member.getMxcAvatarUrl = jest.fn().mockReturnValue("mxc://0/avatar");
|
||||
mocked(testRoom.getJoinedMembers).mockReturnValue([member]);
|
||||
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
|
||||
results: [
|
||||
{ user_id: "@janedoe:matrix.org", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
|
||||
{ user_id: "@johndoe:matrix.org", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
|
||||
],
|
||||
limited: false,
|
||||
});
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.People} initialText="Beta" onFinished={() => null} />);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(2);
|
||||
expect(options[0]).toHaveTextContent(testPerson.display_name!);
|
||||
expect(options[1]).toHaveTextContent("User Beta");
|
||||
});
|
||||
|
||||
it("show non-matching query members with DMs if they are present in the server search results", async () => {
|
||||
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
|
||||
results: [
|
||||
{ user_id: testDMUserId, display_name: "Alice Wonder", avatar_url: "mxc://1/avatar" },
|
||||
{ user_id: "@bob:matrix.org", display_name: "Bob Wonder", avatar_url: "mxc://2/avatar" },
|
||||
],
|
||||
limited: false,
|
||||
});
|
||||
render(
|
||||
<SpotlightDialog initialFilter={Filter.People} initialText="Something Wonder" onFinished={() => null} />,
|
||||
);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(2);
|
||||
expect(options[0]).toHaveTextContent(testDMUserId);
|
||||
expect(options[1]).toHaveTextContent("Bob Wonder");
|
||||
});
|
||||
|
||||
it("don't sort the order of users sent by the server", async () => {
|
||||
const serverList = [
|
||||
{ user_id: "@user2:server", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
|
||||
{ user_id: "@user1:server", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
|
||||
];
|
||||
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
|
||||
results: serverList,
|
||||
limited: false,
|
||||
});
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.People} initialText="User" onFinished={() => null} />);
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(2);
|
||||
expect(options[0]).toHaveTextContent("User Beta");
|
||||
expect(options[1]).toHaveTextContent("User Alpha");
|
||||
});
|
||||
|
||||
it("should start a DM when clicking a person", async () => {
|
||||
render(
|
||||
<SpotlightDialog
|
||||
initialFilter={Filter.People}
|
||||
initialText={testPerson.display_name}
|
||||
onFinished={() => null}
|
||||
/>,
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const options = document.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBeGreaterThanOrEqual(1);
|
||||
expect(options[0]!.innerHTML).toContain(testPerson.display_name);
|
||||
|
||||
fireEvent.click(options[0]!);
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockedClient, [new DirectoryMember(testPerson)]);
|
||||
});
|
||||
|
||||
it("should pass via of the server being explored when joining room from directory", async () => {
|
||||
SdkConfig.put({
|
||||
room_directory: {
|
||||
servers: ["example.tld"],
|
||||
},
|
||||
});
|
||||
localStorage.setItem("mx_last_room_directory_server", "example.tld");
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
const content = document.querySelector("#mx_SpotlightDialog_content")!;
|
||||
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
|
||||
expect(options.length).toBe(1);
|
||||
expect(options[0].innerHTML).toContain(testPublicRoom.name);
|
||||
|
||||
fireEvent.click(options[0].querySelector("[role='button']")!);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "view_room",
|
||||
room_id: testPublicRoom.room_id,
|
||||
via_servers: ["example.tld"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("nsfw public rooms filter", () => {
|
||||
const nsfwNameRoom: IPublicRoomsChunkRoom = {
|
||||
room_id: "@room1:matrix.org",
|
||||
name: "Room 1 [NSFW]",
|
||||
topic: undefined,
|
||||
world_readable: false,
|
||||
num_joined_members: 1,
|
||||
guest_can_join: false,
|
||||
};
|
||||
|
||||
const nsfwTopicRoom: IPublicRoomsChunkRoom = {
|
||||
room_id: "@room2:matrix.org",
|
||||
name: "Room 2",
|
||||
topic: "A room with a topic that includes nsfw",
|
||||
world_readable: false,
|
||||
num_joined_members: 1,
|
||||
guest_can_join: false,
|
||||
};
|
||||
|
||||
const potatoRoom: IPublicRoomsChunkRoom = {
|
||||
room_id: "@room3:matrix.org",
|
||||
name: "Potato Room 3",
|
||||
topic: "Room where we discuss potatoes",
|
||||
world_readable: false,
|
||||
num_joined_members: 1,
|
||||
guest_can_join: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedClient = mockClient({ rooms: [nsfwNameRoom, nsfwTopicRoom, potatoRoom], users: [testPerson] });
|
||||
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, false);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, false);
|
||||
});
|
||||
|
||||
it("does not display rooms with nsfw keywords in results when showNsfwPublicRooms is falsy", async () => {
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(screen.getByText(potatoRoom.name!)).toBeInTheDocument();
|
||||
expect(screen.queryByText(nsfwTopicRoom.name!)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(nsfwTopicRoom.name!)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays rooms with nsfw keywords in results when showNsfwPublicRooms is truthy", async () => {
|
||||
SettingsStore.setValue("SpotlightSearch.showNsfwPublicRooms", null, SettingLevel.DEVICE, true);
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(screen.getByText(nsfwTopicRoom.name!)).toBeInTheDocument();
|
||||
expect(screen.getByText(nsfwNameRoom.name!)).toBeInTheDocument();
|
||||
expect(screen.getByText(potatoRoom.name!)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error if /publicRooms API failed", async () => {
|
||||
mocked(mockedClient.publicRooms).mockRejectedValue(new ConnectionError("Failed to fetch"));
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => null} />);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
expect(screen.getByText("Failed to query public rooms")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("knock rooms", () => {
|
||||
const knockRoom: IPublicRoomsChunkRoom = {
|
||||
guest_can_join: false,
|
||||
join_rule: JoinRule.Knock,
|
||||
num_joined_members: 0,
|
||||
room_id: "some-room-id",
|
||||
world_readable: false,
|
||||
};
|
||||
|
||||
const viewRoomParams = {
|
||||
action: Action.ViewRoom,
|
||||
metricsTrigger: "WebUnifiedSearch",
|
||||
metricsViaKeyboard: false,
|
||||
room_alias: undefined,
|
||||
room_id: knockRoom.room_id,
|
||||
should_peek: false,
|
||||
via_servers: ["example.tld"],
|
||||
};
|
||||
|
||||
beforeEach(() => (mockedClient = mockClient({ rooms: [knockRoom] })));
|
||||
|
||||
describe("when disabling feature", () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) =>
|
||||
setting === "feature_ask_to_join" ? false : [],
|
||||
);
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => {}} />);
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "View" }));
|
||||
});
|
||||
|
||||
it("should not skip to auto join", async () => {
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ ...viewRoomParams, auto_join: true });
|
||||
});
|
||||
|
||||
it("should not prompt ask to join", async () => {
|
||||
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.PromptAskToJoin });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when enabling feature", () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) =>
|
||||
setting === "feature_ask_to_join" ? true : [],
|
||||
);
|
||||
jest.spyOn(mockedClient, "getRoom").mockReturnValue(null);
|
||||
|
||||
render(<SpotlightDialog initialFilter={Filter.PublicRooms} onFinished={() => {}} />);
|
||||
|
||||
// search is debounced
|
||||
jest.advanceTimersByTime(200);
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Ask to join" }));
|
||||
});
|
||||
|
||||
it("should skip to auto join", async () => {
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ ...viewRoomParams, auto_join: false });
|
||||
});
|
||||
|
||||
it("should prompt ask to join", async () => {
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.PromptAskToJoin });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { EventType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { UnpinAllDialog } from "../../../../src/components/views/dialogs/UnpinAllDialog";
|
||||
import { createTestClient } from "../../../test-utils";
|
||||
|
||||
describe("<UnpinAllDialog />", () => {
|
||||
const client = createTestClient();
|
||||
const roomId = "!room:example.org";
|
||||
|
||||
function renderDialog(onFinished = jest.fn()) {
|
||||
return render(<UnpinAllDialog matrixClient={client} roomId={roomId} onFinished={onFinished} />);
|
||||
}
|
||||
|
||||
it("should render", () => {
|
||||
const { asFragment } = renderDialog();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should remove all pinned events when clicked on Continue", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderDialog(onFinished);
|
||||
|
||||
await userEvent.click(screen.getByText("Continue"));
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPinnedEvents, { pinned: [] }, "");
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
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, { ReactElement } from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore";
|
||||
import SdkConfig from "../../../../src/SdkConfig";
|
||||
import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
|
||||
import UserSettingsDialog from "../../../../src/components/views/dialogs/UserSettingsDialog";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsUser,
|
||||
mockClientMethodsServer,
|
||||
mockPlatformPeg,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsRooms,
|
||||
useMockMediaDevices,
|
||||
} from "../../../test-utils";
|
||||
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
|
||||
mockPlatformPeg({
|
||||
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
|
||||
getAppVersion: jest.fn().mockResolvedValue("1"),
|
||||
});
|
||||
|
||||
jest.mock("../../../../src/settings/SettingsStore", () => ({
|
||||
getValue: jest.fn(),
|
||||
getValueAt: jest.fn(),
|
||||
canSetValue: jest.fn(),
|
||||
monitorSetting: jest.fn(),
|
||||
watchSetting: jest.fn(),
|
||||
unwatchSetting: jest.fn(),
|
||||
getFeatureSettingNames: jest.fn(),
|
||||
getBetaInfo: jest.fn(),
|
||||
getDisplayName: jest.fn(),
|
||||
getDescription: jest.fn(),
|
||||
shouldHaveWarning: jest.fn(),
|
||||
disabledMessage: jest.fn(),
|
||||
settingIsOveriddenAtConfigLevel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<UserSettingsDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockSettingsStore = mocked(SettingsStore);
|
||||
let mockClient!: MockedObject<MatrixClient>;
|
||||
|
||||
let sdkContext: SdkContextClass;
|
||||
const defaultProps = { onFinished: jest.fn() };
|
||||
const getComponent = (props: Partial<typeof defaultProps & { initialTabId?: UserTab }> = {}): ReactElement => (
|
||||
<UserSettingsDialog sdkContext={sdkContext} {...defaultProps} {...props} />
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsCrypto(),
|
||||
...mockClientMethodsRooms(),
|
||||
getIgnoredUsers: jest.fn().mockResolvedValue([]),
|
||||
getPushers: jest.fn().mockResolvedValue([]),
|
||||
getProfileInfo: jest.fn().mockResolvedValue({}),
|
||||
});
|
||||
sdkContext = new SdkContextClass();
|
||||
sdkContext.client = mockClient;
|
||||
mockSettingsStore.getValue.mockReturnValue(false);
|
||||
mockSettingsStore.getValueAt.mockReturnValue(false);
|
||||
mockSettingsStore.getFeatureSettingNames.mockReturnValue([]);
|
||||
SdkConfig.reset();
|
||||
SdkConfig.put({ brand: "Test" });
|
||||
});
|
||||
|
||||
const getActiveTabLabel = (container: Element) =>
|
||||
container.querySelector(".mx_TabbedView_tabLabel_active")?.textContent;
|
||||
|
||||
it("should render general settings tab when no initialTabId", () => {
|
||||
const { container } = render(getComponent());
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Account");
|
||||
});
|
||||
|
||||
it("should render initial tab when initialTabId is set", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Help }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Help & About");
|
||||
});
|
||||
|
||||
it("should render general tab if initialTabId tab cannot be rendered", () => {
|
||||
// mjolnir tab is only rendered in some configs
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Mjolnir }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Account");
|
||||
});
|
||||
|
||||
it("renders tabs correctly", () => {
|
||||
SdkConfig.add({
|
||||
show_labs_settings: true,
|
||||
});
|
||||
const { container } = render(getComponent());
|
||||
expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders ignored users tab when feature_mjolnir is enabled", () => {
|
||||
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === "feature_mjolnir");
|
||||
const { getByTestId } = render(getComponent());
|
||||
expect(getByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders voip tab when voip is enabled", () => {
|
||||
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
|
||||
const { getByTestId } = render(getComponent());
|
||||
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders with session manager tab selected", () => {
|
||||
const { getByTestId } = render(getComponent({ initialTabId: UserTab.SessionManager }));
|
||||
expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Sessions");
|
||||
});
|
||||
|
||||
it("renders with appearance tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Appearance }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Appearance");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Appearance");
|
||||
});
|
||||
|
||||
it("renders with notifications tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Notifications }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Notifications");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Notifications");
|
||||
});
|
||||
|
||||
it("renders with preferences tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Preferences }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Preferences");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Preferences");
|
||||
});
|
||||
|
||||
it("renders with keyboard tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Keyboard }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Keyboard");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Keyboard");
|
||||
});
|
||||
|
||||
it("renders with sidebar tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Sidebar }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Sidebar");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Sidebar");
|
||||
});
|
||||
|
||||
it("renders with voip tab selected", () => {
|
||||
useMockMediaDevices();
|
||||
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Voice }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Voice & Video");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Voice & Video");
|
||||
});
|
||||
|
||||
it("renders with security tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Security }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Security & Privacy");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Security & Privacy");
|
||||
});
|
||||
|
||||
it("renders with labs tab selected", () => {
|
||||
SdkConfig.add({
|
||||
show_labs_settings: true,
|
||||
});
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Labs }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Labs");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Labs");
|
||||
});
|
||||
|
||||
it("renders with mjolnir tab selected", () => {
|
||||
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === "feature_mjolnir");
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Mjolnir }));
|
||||
expect(getActiveTabLabel(container)).toEqual("Ignored users");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Ignored Users");
|
||||
});
|
||||
|
||||
it("renders with help tab selected", () => {
|
||||
const { container } = render(getComponent({ initialTabId: UserTab.Help }));
|
||||
|
||||
expect(getActiveTabLabel(container)).toEqual("Help & About");
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings: Help & About");
|
||||
});
|
||||
|
||||
it("renders labs tab when show_labs_settings is enabled in config", () => {
|
||||
SdkConfig.add({
|
||||
show_labs_settings: true,
|
||||
});
|
||||
const { getByTestId } = render(getComponent());
|
||||
expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders labs tab when some feature is in beta", () => {
|
||||
mockSettingsStore.getFeatureSettingNames.mockReturnValue(["feature_beta_setting", "feature_just_normal_labs"]);
|
||||
mockSettingsStore.getBetaInfo.mockImplementation((settingName) =>
|
||||
settingName === "feature_beta_setting" ? ({} as any) : undefined,
|
||||
);
|
||||
const { getByTestId } = render(getComponent());
|
||||
expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("watches settings", async () => {
|
||||
const watchSettingCallbacks: Record<string, CallbackFn> = {};
|
||||
|
||||
mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => {
|
||||
watchSettingCallbacks[settingName] = callback;
|
||||
return `mock-watcher-id-${settingName}`;
|
||||
});
|
||||
mockSettingsStore.getValue.mockReturnValue(false);
|
||||
|
||||
const { queryByTestId, findByTestId, unmount } = render(getComponent());
|
||||
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy();
|
||||
|
||||
expect(mockSettingsStore.watchSetting).toHaveBeenCalledWith("feature_mjolnir", null, expect.anything());
|
||||
|
||||
// call the watch setting callback
|
||||
mockSettingsStore.getValue.mockReturnValue(true);
|
||||
watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true);
|
||||
|
||||
// tab is rendered now
|
||||
await expect(findByTestId(`settings-tab-${UserTab.Mjolnir}`)).resolves.toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
// unwatches settings on unmount
|
||||
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,540 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AppDownloadDialog should allow disabling desktop build 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_AppDownloadDialog 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"
|
||||
>
|
||||
Download Element
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_mobile"
|
||||
>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
iOS
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Download on the App Store"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Android
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://play.google.com/store/apps/details?id=im.vector.app"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
<a
|
||||
aria-label="Get it on F-Droid"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_legal"
|
||||
>
|
||||
<p>
|
||||
App Store® and the Apple logo® are trademarks of Apple Inc.
|
||||
</p>
|
||||
<p>
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppDownloadDialog should allow disabling fdroid build 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_AppDownloadDialog 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"
|
||||
>
|
||||
Download Element
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_desktop"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Download Element Desktop
|
||||
</h3>
|
||||
<a
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
href="https://element.io/download"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
Download Element Desktop
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_mobile"
|
||||
>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
iOS
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Download on the App Store"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Android
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://play.google.com/store/apps/details?id=im.vector.app"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_legal"
|
||||
>
|
||||
<p>
|
||||
App Store® and the Apple logo® are trademarks of Apple Inc.
|
||||
</p>
|
||||
<p>
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppDownloadDialog should allow disabling mobile builds 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_AppDownloadDialog 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"
|
||||
>
|
||||
Download Element
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_desktop"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Download Element Desktop
|
||||
</h3>
|
||||
<a
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
href="https://element.io/download"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
Download Element Desktop
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_mobile"
|
||||
/>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_legal"
|
||||
>
|
||||
<p>
|
||||
App Store® and the Apple logo® are trademarks of Apple Inc.
|
||||
</p>
|
||||
<p>
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppDownloadDialog should render with desktop, ios, android, fdroid buttons by default 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_AppDownloadDialog 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"
|
||||
>
|
||||
Download Element
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_desktop"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Download Element Desktop
|
||||
</h3>
|
||||
<a
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
href="https://element.io/download"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
Download Element Desktop
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_mobile"
|
||||
>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
iOS
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Download on the App Store"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_app"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Android
|
||||
</h3>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<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
|
||||
class="mx_AppDownloadDialog_info"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_links"
|
||||
>
|
||||
<a
|
||||
aria-label="Get it on Google Play"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://play.google.com/store/apps/details?id=im.vector.app"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
<a
|
||||
aria-label="Get it on F-Droid"
|
||||
class="mx_AccessibleButton"
|
||||
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<div />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppDownloadDialog_legal"
|
||||
>
|
||||
<p>
|
||||
App Store® and the Apple logo® are trademarks of Apple Inc.
|
||||
</p>
|
||||
<p>
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,141 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ChangelogDialog /> should fetch github proxy url for each repo with old and new version strings 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Changelog
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_ChangelogDialog_content"
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
element-hq/element-web
|
||||
</h2>
|
||||
<ul>
|
||||
<li
|
||||
class="mx_ChangelogDialog_li"
|
||||
>
|
||||
<a
|
||||
href="https://api.github.com/repos/element-hq/element-web/commit/commit-sha"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
This is the first commit message
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
element-hq/matrix-react-sdk
|
||||
</h2>
|
||||
<ul>
|
||||
<li
|
||||
class="mx_ChangelogDialog_li"
|
||||
>
|
||||
<a
|
||||
href="https://api.github.com/repos/element-hq/matrix-react-sdk/commit/commit-sha"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
This is a commit message
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
matrix-org/matrix-js-sdk
|
||||
</h2>
|
||||
<ul>
|
||||
<li
|
||||
class="mx_ChangelogDialog_li"
|
||||
>
|
||||
<a
|
||||
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
This is a commit message
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="mx_ChangelogDialog_li"
|
||||
>
|
||||
<a
|
||||
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
This is another commit message
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,95 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmUserActionDialog renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_ConfirmUserActionDialog 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"
|
||||
>
|
||||
Ban this
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_ConfirmUserActionDialog_user"
|
||||
>
|
||||
<div
|
||||
class="mx_ConfirmUserActionDialog_avatar"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 48px;"
|
||||
title="@user:test.com"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ConfirmUserActionDialog_name"
|
||||
>
|
||||
@user:test.com
|
||||
</div>
|
||||
<div
|
||||
class="mx_ConfirmUserActionDialog_userId"
|
||||
>
|
||||
@user:test.com
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
Ban
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,243 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
Developer Tools
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevTools_label_left"
|
||||
>
|
||||
Toolbox
|
||||
</div>
|
||||
<div
|
||||
class="mx_CopyableText mx_DevTools_label_right"
|
||||
>
|
||||
Room ID: !id
|
||||
<div
|
||||
aria-describedby="floating-ui-2"
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevTools_label_bottom"
|
||||
/>
|
||||
<div
|
||||
class="mx_DevTools_content"
|
||||
>
|
||||
<div>
|
||||
<h3>
|
||||
Room
|
||||
</h3>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Send custom timeline event
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Explore room state
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Explore room account data
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
View servers in room
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Notifications debug
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Verification explorer
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Active Widgets
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
Other
|
||||
</h3>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Explore account data
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Settings explorer
|
||||
</button>
|
||||
<button
|
||||
class="mx_DevTools_button"
|
||||
>
|
||||
Server info
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
Options
|
||||
</h3>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Developer mode
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Developer mode"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_QgU2PomxwKpa"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Show hidden events in timeline
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Show hidden events in timeline"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_QgU2PomxwKpa"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_6hpi3YEetmBG"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Enable widget screenshots on supported widgets
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Enable widget screenshots on supported widgets"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_6hpi3YEetmBG"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
for="mx_SettingsFlag_4yVCeEefiPqp"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Force 15s voice broadcast chunk length
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Force 15s voice broadcast chunk length"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
id="mx_SettingsFlag_4yVCeEefiPqp"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<button>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,206 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ExportDialog /> renders export dialog 1`] = `
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_ExportDialog false 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"
|
||||
>
|
||||
Export Chat
|
||||
</h1>
|
||||
</div>
|
||||
<p>
|
||||
Select from the options below to export chats from your timeline
|
||||
</p>
|
||||
<div
|
||||
class="mx_ExportDialog_options"
|
||||
>
|
||||
<span
|
||||
class="mx_ExportDialog_subheading"
|
||||
>
|
||||
Format
|
||||
</span>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
id="exportFormat-Html"
|
||||
name="exportFormat"
|
||||
type="radio"
|
||||
value="Html"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
HTML
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
id="exportFormat-PlainText"
|
||||
name="exportFormat"
|
||||
type="radio"
|
||||
value="PlainText"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
Plain Text
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
id="exportFormat-Json"
|
||||
name="exportFormat"
|
||||
type="radio"
|
||||
value="Json"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
JSON
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
class="mx_ExportDialog_subheading"
|
||||
>
|
||||
Messages
|
||||
</span>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
id="export-type"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
value="Timeline"
|
||||
>
|
||||
Current Timeline
|
||||
</option>
|
||||
<option
|
||||
value="Beginning"
|
||||
>
|
||||
From the beginning
|
||||
</option>
|
||||
<option
|
||||
value="LastNMessages"
|
||||
>
|
||||
Specify a number of messages
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="export-type"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="mx_ExportDialog_subheading"
|
||||
>
|
||||
Size Limit
|
||||
</span>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
id="size-limit"
|
||||
type="number"
|
||||
value="8"
|
||||
/>
|
||||
<label
|
||||
for="size-limit"
|
||||
/>
|
||||
<span
|
||||
class="mx_Field_postfix"
|
||||
>
|
||||
<span>
|
||||
MB
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="include-attachments"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="include-attachments"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Include Attachments
|
||||
</div>
|
||||
</label>
|
||||
</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"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ExportDialog /> renders success screen when export is finished 1`] = `null`;
|
|
@ -0,0 +1,90 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FeedbackDialog should respect feedback config 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_FeedbackDialog 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"
|
||||
>
|
||||
Feedback
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug"
|
||||
>
|
||||
<h3>
|
||||
Report a bug
|
||||
</h3>
|
||||
<p>
|
||||
<span>
|
||||
Please view
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="http://existing?foo=bar"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
existing bugs on Github
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
first. No match?
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://new.issue.url?foo=bar"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Start a new one
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,243 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LogoutDialog Prompts user to connect backup if there is a backup on the server 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="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"
|
||||
>
|
||||
You'll lose access to your encrypted messages
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
|
||||
</p>
|
||||
<p>
|
||||
When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
|
||||
</p>
|
||||
<p>
|
||||
Back up your keys before signing out to avoid losing them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button>
|
||||
I don't want my encrypted messages
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Connect this session to Key Backup
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<details>
|
||||
<summary
|
||||
class="mx_LogoutDialog_ExportKeyAdvanced"
|
||||
>
|
||||
Advanced
|
||||
</summary>
|
||||
<p>
|
||||
<button>
|
||||
Manually export keys
|
||||
</button>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="floating-ui-22"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LogoutDialog Prompts user to set up backup if there is no backup on the server 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="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"
|
||||
>
|
||||
You'll lose access to your encrypted messages
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.
|
||||
</p>
|
||||
<p>
|
||||
When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.
|
||||
</p>
|
||||
<p>
|
||||
Back up your keys before signing out to avoid losing them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button>
|
||||
I don't want my encrypted messages
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Start using Key Backup
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<details>
|
||||
<summary
|
||||
class="mx_LogoutDialog_ExportKeyAdvanced"
|
||||
>
|
||||
Advanced
|
||||
</summary>
|
||||
<p>
|
||||
<button>
|
||||
Manually export keys
|
||||
</button>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="floating-ui-28"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LogoutDialog shows a regular dialog when crypto is disabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Sign out
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
Are you sure you want to sign out?
|
||||
</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"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,252 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ManageRestrictedJoinRuleDialog /> should list spaces which are not parents of the room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_ManageRestrictedJoinRuleDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Select spaces
|
||||
</h1>
|
||||
</div>
|
||||
<p>
|
||||
<span>
|
||||
Decide which spaces can access this room. If a space is selected, its members can find and join
|
||||
<strong>
|
||||
!roomId:server
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="mx_SearchBox mx_textinput"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="mx_textinput_icon mx_textinput_search mx_textinput_icon mx_textinput_search"
|
||||
data-testid="searchbox-input"
|
||||
placeholder="Search spaces"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SearchBox_closeButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_section"
|
||||
>
|
||||
<h3>
|
||||
Other spaces you know
|
||||
</h3>
|
||||
<label
|
||||
class="mx_ManageRestrictedJoinRuleDialog_entry"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
O
|
||||
</span>
|
||||
<span
|
||||
class="mx_ManageRestrictedJoinRuleDialog_entry_name"
|
||||
>
|
||||
Other Space
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_entry_description"
|
||||
>
|
||||
0 members
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
id="checkbox_vY7Q4uEh9K"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_vY7Q4uEh9K"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_footer"
|
||||
>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_section_info"
|
||||
>
|
||||
You're removing all spaces. Access will default to invite only
|
||||
</div>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_footer_buttons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Confirm
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ManageRestrictedJoinRuleDialog /> should render empty state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_ManageRestrictedJoinRuleDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Select spaces
|
||||
</h1>
|
||||
</div>
|
||||
<p>
|
||||
<span>
|
||||
Decide which spaces can access this room. If a space is selected, its members can find and join
|
||||
<strong>
|
||||
!roomId:server
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="mx_SearchBox mx_textinput"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="mx_textinput_icon mx_textinput_search mx_textinput_icon mx_textinput_search"
|
||||
data-testid="searchbox-input"
|
||||
placeholder="Search spaces"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_SearchBox_closeButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_ManageRestrictedJoinRuleDialog_noResults"
|
||||
>
|
||||
No results
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_footer"
|
||||
>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_section_info"
|
||||
>
|
||||
You're removing all spaces. Access will default to invite only
|
||||
</div>
|
||||
<div
|
||||
class="mx_ManageRestrictedJoinRuleDialog_footer_buttons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Confirm
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,231 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ManualDeviceKeyVerificationDialog should display the device 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Verify session
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Confirm by comparing the following with the User Settings in your other session:
|
||||
</p>
|
||||
<div
|
||||
class="mx_DeviceVerifyDialog_cryptoSection"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<label>
|
||||
Session name
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
my device
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
Session ID
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
<code>
|
||||
XYZ
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
Session key
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
<code>
|
||||
<strong>
|
||||
ABCD EFGH
|
||||
</strong>
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
If they don't match, the security of your communication may be compromised.
|
||||
</p>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
Verify session
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ManualDeviceKeyVerificationDialog should display the device of another user 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Verify session
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Confirm this user's session by comparing the following with their User Settings:
|
||||
</p>
|
||||
<div
|
||||
class="mx_DeviceVerifyDialog_cryptoSection"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<label>
|
||||
Session name
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
my device
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
Session ID
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
<code>
|
||||
XYZ
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
Session key
|
||||
:
|
||||
</label>
|
||||
|
||||
<span>
|
||||
<code>
|
||||
<strong>
|
||||
ABCD EFGH
|
||||
</strong>
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
If they don't match, the security of your communication may be compromised.
|
||||
</p>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
Verify session
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,332 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MessageEditHistory /> should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_MessageEditHistoryDialog 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"
|
||||
>
|
||||
Message edits
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
>
|
||||
<ul
|
||||
class="mx_MessageEditHistoryDialog_edits"
|
||||
>
|
||||
<li>
|
||||
<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>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
My Great Massage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="floating-ui-2"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_MessageEditHistoryDialog 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"
|
||||
>
|
||||
Message edits
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
>
|
||||
<ul
|
||||
class="mx_MessageEditHistoryDialog_edits"
|
||||
>
|
||||
<li>
|
||||
<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>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
My Great Massage
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
My Great M
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
ssage
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
My Great Missage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="floating-ui-8"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,159 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomSettingsDialog /> Settings tabs renders default tabs correctly 1`] = `
|
||||
NodeList [
|
||||
<li
|
||||
aria-controls="mx_tabpanel_ROOM_GENERAL_TAB"
|
||||
aria-selected="true"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
|
||||
data-testid="settings-tab-ROOM_GENERAL_TAB"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_ROOM_GENERAL_TAB_label"
|
||||
>
|
||||
General
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_ROOM_SECURITY_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-ROOM_SECURITY_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_ROOM_SECURITY_TAB_label"
|
||||
>
|
||||
Security & Privacy
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_ROOM_ROLES_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-ROOM_ROLES_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_ROOM_ROLES_TAB_label"
|
||||
>
|
||||
Roles & Permissions
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_ROOM_NOTIFICATIONS_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-ROOM_NOTIFICATIONS_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_notificationsIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_ROOM_NOTIFICATIONS_TAB_label"
|
||||
>
|
||||
Notifications
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_ROOM_POLL_HISTORY_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-ROOM_POLL_HISTORY_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_pollsIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_ROOM_POLL_HISTORY_TAB_label"
|
||||
>
|
||||
Polls
|
||||
</span>
|
||||
</li>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<RoomSettingsDialog /> poll history displays poll history when tab clicked 1`] = `
|
||||
<div
|
||||
class="mx_SettingsTab"
|
||||
>
|
||||
<div
|
||||
class="mx_PollHistory_content"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2 mx_PollHistory_header"
|
||||
>
|
||||
Polls
|
||||
</h2>
|
||||
<div
|
||||
class="mx_PollHistoryList"
|
||||
>
|
||||
<fieldset
|
||||
class="mx_FilterTabGroup"
|
||||
>
|
||||
<label
|
||||
data-testid="filter-tab-PollHistory_filter-ACTIVE"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
name="PollHistory_filter"
|
||||
type="radio"
|
||||
value="ACTIVE"
|
||||
/>
|
||||
<span>
|
||||
Active polls
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
data-testid="filter-tab-PollHistory_filter-ENDED"
|
||||
>
|
||||
<input
|
||||
name="PollHistory_filter"
|
||||
type="radio"
|
||||
value="ENDED"
|
||||
/>
|
||||
<span>
|
||||
Past polls
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div
|
||||
class="mx_PollHistoryList_loading mx_PollHistoryList_noResultsYet"
|
||||
>
|
||||
<div
|
||||
class="mx_InlineSpinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||
style="width: 16px; height: 16px;"
|
||||
/>
|
||||
</div>
|
||||
Loading polls
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,142 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ServerPickerDialog /> should render dialog 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_ServerPickerDialog"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_ServerPickerDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Sign into your homeserver
|
||||
</h1>
|
||||
</div>
|
||||
<form
|
||||
class="mx_Dialog_content"
|
||||
id="mx_ServerPickerDialog"
|
||||
>
|
||||
<p>
|
||||
We call the places where you can host your account 'homeservers'.
|
||||
|
||||
Matrix.org is the biggest public homeserver in the world, so it's a good place for many.
|
||||
</p>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
data-testid="defaultHomeserver"
|
||||
name="defaultChosen"
|
||||
type="radio"
|
||||
value="true"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_Login_underlinedServerName"
|
||||
tabindex="0"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
class="mx_StyledRadioButton mx_ServerPickerDialog_otherHomeserverRadio mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
|
||||
>
|
||||
<label
|
||||
class="mx_StyledRadioButton_innerLabel"
|
||||
>
|
||||
<input
|
||||
aria-label="Other homeserver"
|
||||
name="defaultChosen"
|
||||
type="radio"
|
||||
value="false"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
</label>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_ServerPickerDialog_otherHomeserver"
|
||||
>
|
||||
<input
|
||||
id="mx_homeserverInput"
|
||||
label="Other homeserver"
|
||||
placeholder="Other homeserver"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_homeserverInput"
|
||||
>
|
||||
Other homeserver
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Use your preferred Matrix homeserver if you have one, or host your own.
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_ServerPickerDialog_continue mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</div>
|
||||
<h2>
|
||||
Learn more
|
||||
</h2>
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://matrix.org/docs/matrix-concepts/elements-of-matrix/#homeserver"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
About homeservers
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
</form>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UnpinAllDialog /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_UnpinAllDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title mx_UnpinAllDialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Unpin all messages?
|
||||
</h1>
|
||||
</div>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||
>
|
||||
Make sure that you really want to remove all pinned messages. This action can’t be undone.
|
||||
</span>
|
||||
<div
|
||||
class="mx_UnpinAllDialog_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_i91xf_17 _destructive_i91xf_116"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<button
|
||||
class="_button_i91xf_17"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,292 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserSettingsDialog /> renders tabs correctly 1`] = `
|
||||
NodeList [
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_ACCOUNT_TAB"
|
||||
aria-selected="true"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
|
||||
data-testid="settings-tab-USER_ACCOUNT_TAB"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.175 13.825C9.958 14.608 10.9 15 12 15s2.042-.392 2.825-1.175C15.608 13.042 16 12.1 16 11s-.392-2.042-1.175-2.825C14.042 7.392 13.1 7 12 7s-2.042.392-2.825 1.175C8.392 8.958 8 9.9 8 11s.392 2.042 1.175 2.825Zm4.237-1.412A1.926 1.926 0 0 1 12 13c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 10 11c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 12 9c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412Z"
|
||||
/>
|
||||
<path
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
|
||||
/>
|
||||
<path
|
||||
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_ACCOUNT_TAB_label"
|
||||
>
|
||||
Account
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_SESSION_MANAGER_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_SESSION_MANAGER_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20c-.417 0-.77-.146-1.063-.438A1.447 1.447 0 0 1 2 18.5c0-.417.146-.77.438-1.063A1.446 1.446 0 0 1 3.5 17H4V6c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 4h14a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 20 6H6v11h4.5c.417 0 .77.146 1.063.438.291.291.437.645.437 1.062 0 .417-.146.77-.438 1.063A1.446 1.446 0 0 1 10.5 20h-7ZM15 20a.968.968 0 0 1-.713-.288A.968.968 0 0 1 14 19V9c0-.283.096-.52.287-.713A.968.968 0 0 1 15 8h6a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v10c0 .283-.096.52-.288.712A.968.968 0 0 1 21 20h-6Zm1-3h4v-7h-4v7Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_SESSION_MANAGER_TAB_label"
|
||||
>
|
||||
Sessions
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_APPEARANCE_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_APPEARANCE_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 16c1.25 0 2.313-.438 3.188-1.313.874-.874 1.312-1.937 1.312-3.187 0-1.25-.438-2.313-1.313-3.188C14.313 7.439 13.25 7 12 7c-1.25 0-2.312.438-3.187 1.313C7.938 9.187 7.5 10.25 7.5 11.5c0 1.25.438 2.313 1.313 3.188C9.688 15.562 10.75 16 12 16Zm0-1.8c-.75 0-1.387-.262-1.912-.787A2.604 2.604 0 0 1 9.3 11.5c0-.75.263-1.387.787-1.912A2.604 2.604 0 0 1 12 8.8c.75 0 1.387.262 1.912.787.525.526.788 1.163.788 1.913s-.262 1.387-.787 1.912A2.604 2.604 0 0 1 12 14.2Zm0 4.8c-2.317 0-4.433-.613-6.35-1.837-1.917-1.226-3.367-2.88-4.35-4.963a.812.812 0 0 1-.1-.313 2.93 2.93 0 0 1 0-.774.812.812 0 0 1 .1-.313c.983-2.083 2.433-3.738 4.35-4.963C7.567 4.614 9.683 4 12 4c2.317 0 4.433.612 6.35 1.838 1.917 1.224 3.367 2.879 4.35 4.962a.81.81 0 0 1 .1.313 2.925 2.925 0 0 1 0 .774.81.81 0 0 1-.1.313c-.983 2.083-2.433 3.738-4.35 4.963C16.433 18.387 14.317 19 12 19Zm0-2a9.544 9.544 0 0 0 5.188-1.488A9.773 9.773 0 0 0 20.8 11.5a9.773 9.773 0 0 0-3.613-4.013A9.544 9.544 0 0 0 12 6a9.545 9.545 0 0 0-5.187 1.487A9.773 9.773 0 0 0 3.2 11.5a9.773 9.773 0 0 0 3.613 4.012A9.544 9.544 0 0 0 12 17Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_APPEARANCE_TAB_label"
|
||||
>
|
||||
Appearance
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_NOTIFICATIONS_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_NOTIFICATIONS_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 3c7 0 7 7 7 7v6l1.293 1.293c.63.63.184 1.707-.707 1.707H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-7 7-7Zm5 7.01v-.022l-.009-.146a6.591 6.591 0 0 0-.073-.607 6.608 6.608 0 0 0-.582-1.84c-.319-.638-.766-1.215-1.398-1.637C14.318 5.344 13.4 5 12 5c-1.4 0-2.317.344-2.937.758-.633.422-1.08.999-1.4 1.636a6.606 6.606 0 0 0-.58 1.841A6.596 6.596 0 0 0 7 9.988v6.84L6.828 17h10.344L17 16.828V10.01ZM12 22a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_NOTIFICATIONS_TAB_label"
|
||||
>
|
||||
Notifications
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_PREFERENCES_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_PREFERENCES_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M6.5 2h11a4.5 4.5 0 1 1 0 9h-11a4.5 4.5 0 0 1 0-9Zm0 2h7.258A4.479 4.479 0 0 0 13 6.5c0 .925.28 1.785.758 2.5H6.5a2.5 2.5 0 0 1 0-5ZM15 6.5a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm-13 11A4.5 4.5 0 0 1 6.5 13h11a4.5 4.5 0 1 1 0 9h-11A4.5 4.5 0 0 1 2 17.5Zm8.242-2.5H17.5a2.5 2.5 0 0 1 0 5h-7.258A4.478 4.478 0 0 0 11 17.5c0-.925-.28-1.785-.758-2.5ZM6.5 15a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_PREFERENCES_TAB_label"
|
||||
>
|
||||
Preferences
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_KEYBOARD_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_KEYBOARD_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.188 8v2h2V8h-2Zm3.875 0v2h2V8h-2Zm3.875 0v2h2V8h-2Zm3.875 0v2h2V8h-2ZM5.188 11.531v2h2v-2h-2Zm3.875 0v2h2v-2h-2Zm3.875 0v2h2v-2h-2Zm3.875 0v2h2v-2h-2ZM9 15a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H9Z"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Zm2 0h16v12H4V6Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_KEYBOARD_TAB_label"
|
||||
>
|
||||
Keyboard
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_SIDEBAR_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_SIDEBAR_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M18 3a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12Zm-8 2h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-8V5ZM8 19H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2v14Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_SIDEBAR_TAB_label"
|
||||
>
|
||||
Sidebar
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_SECURITY_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_SECURITY_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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 20V10c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 6 8h1V6c0-1.383.487-2.563 1.463-3.538C9.438 1.487 10.617 1 12 1s2.563.488 3.537 1.462C16.512 3.438 17 4.617 17 6v2h1c.55 0 1.02.196 1.413.588.391.391.587.862.587 1.412v10c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 22H6Zm0-2h12V10H6v10ZM9 8h6V6c0-.833-.292-1.542-.875-2.125A2.893 2.893 0 0 0 12 3c-.833 0-1.542.292-2.125.875A2.893 2.893 0 0 0 9 6v2Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_SECURITY_TAB_label"
|
||||
>
|
||||
Security & Privacy
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_LABS_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 5a1 1 0 0 1-1-1V3a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1Zm-7.071-.071a1 1 0 0 1 1.414 0l.707.707A1 1 0 0 1 5.636 7.05l-.707-.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M15.734 15.325C15.316 15.795 15 16.371 15 17v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2c0-.63-.316-1.205-.734-1.675a5 5 0 1 1 7.468 0Zm-1.493-1.33a3 3 0 1 0-4.482 0c.433.486.894 1.166 1.112 2.005h2.258c.218-.84.679-1.52 1.112-2.005ZM13 18h-2v1h2v-1Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M2 12a1 1 0 0 1 1-1h1a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm18-1a1 1 0 1 0 0 2h1a1 1 0 1 0 0-2h-1Zm-3.05-5.364a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0-1.414-1.414l-.707.707Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_LABS_TAB_label"
|
||||
>
|
||||
Labs
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_HELP_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_HELP_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 8a1.5 1.5 0 0 0-1.5 1.5 1 1 0 1 1-2 0 3.5 3.5 0 1 1 6.01 2.439c-.122.126-.24.243-.352.355-.287.288-.54.54-.76.824-.293.375-.398.651-.398.882a1 1 0 1 1-2 0c0-.874.407-1.58.819-2.11.305-.392.688-.775 1-1.085l.257-.26A1.5 1.5 0 0 0 12 8Zm1 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
/>
|
||||
<path
|
||||
d="M8.1 21.212A9.738 9.738 0 0 0 12 22a9.738 9.738 0 0 0 3.9-.788 10.098 10.098 0 0 0 3.175-2.137c.9-.9 1.613-1.958 2.137-3.175A9.738 9.738 0 0 0 22 12a9.738 9.738 0 0 0-.788-3.9 10.099 10.099 0 0 0-2.137-3.175c-.9-.9-1.958-1.612-3.175-2.137A9.738 9.738 0 0 0 12 2a9.738 9.738 0 0 0-3.9.788 10.099 10.099 0 0 0-3.175 2.137c-.9.9-1.612 1.958-2.137 3.175A9.738 9.738 0 0 0 2 12a9.74 9.74 0 0 0 .788 3.9 10.098 10.098 0 0 0 2.137 3.175c.9.9 1.958 1.613 3.175 2.137Zm9.575-3.537C16.125 19.225 14.233 20 12 20c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_HELP_TAB_label"
|
||||
>
|
||||
Help & About
|
||||
</span>
|
||||
</li>,
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
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 { Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { DevtoolsContext } from "../../../../../src/components/views/dialogs/devtools/BaseTool";
|
||||
import { TimelineEventEditor } from "../../../../../src/components/views/dialogs/devtools/Event";
|
||||
|
||||
describe("<EventEditor />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsContext.Provider
|
||||
value={{
|
||||
room: new Room("!roomId", cli, "@alice:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TimelineEventEditor onBack={() => {}} />
|
||||
</DevtoolsContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("thread context", () => {
|
||||
it("should pre-populate a thread relationship", () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsContext.Provider
|
||||
value={{
|
||||
room: new Room("!roomId", cli, "@alice:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
}),
|
||||
threadRootId: "$this_is_a_thread_id",
|
||||
}}
|
||||
>
|
||||
<TimelineEventEditor onBack={() => {}} />
|
||||
</DevtoolsContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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 { Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import RoomNotifications from "../../../../../src/components/views/dialogs/devtools/RoomNotifications";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { DevtoolsContext } from "../../../../../src/components/views/dialogs/devtools/BaseTool";
|
||||
|
||||
describe("<RoomNotifications />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<DevtoolsContext.Provider
|
||||
value={{
|
||||
room: new Room("!roomId", cli, "@alice:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RoomNotifications onBack={() => {}} setTool={() => {}} />
|
||||
</DevtoolsContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EventEditor /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DevTools_content"
|
||||
>
|
||||
<div
|
||||
class="mx_DevTools_eventTypeStateKeyGroup"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="on"
|
||||
id="eventType"
|
||||
label="Event Type"
|
||||
placeholder="Event Type"
|
||||
size="42"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="eventType"
|
||||
>
|
||||
Event Type
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Field mx_Field_textarea mx_DevTools_textarea"
|
||||
>
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
id="evContent"
|
||||
label="Event Content"
|
||||
placeholder="Event Content"
|
||||
type="text"
|
||||
>
|
||||
{
|
||||
|
||||
}
|
||||
</textarea>
|
||||
<label
|
||||
for="evContent"
|
||||
>
|
||||
Event Content
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<button>
|
||||
Back
|
||||
</button>
|
||||
<button>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EventEditor /> thread context should pre-populate a thread relationship 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DevTools_content"
|
||||
>
|
||||
<div
|
||||
class="mx_DevTools_eventTypeStateKeyGroup"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="on"
|
||||
id="eventType"
|
||||
label="Event Type"
|
||||
placeholder="Event Type"
|
||||
size="42"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="eventType"
|
||||
>
|
||||
Event Type
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Field mx_Field_textarea mx_DevTools_textarea"
|
||||
>
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
id="evContent"
|
||||
label="Event Content"
|
||||
placeholder="Event Content"
|
||||
type="text"
|
||||
>
|
||||
{
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.thread",
|
||||
"event_id": "$this_is_a_thread_id"
|
||||
}
|
||||
}
|
||||
</textarea>
|
||||
<label
|
||||
for="evContent"
|
||||
>
|
||||
Event Content
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<button>
|
||||
Back
|
||||
</button>
|
||||
<button>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,72 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomNotifications /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_DevTools_content"
|
||||
>
|
||||
<section>
|
||||
<h2>
|
||||
Room status
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span>
|
||||
Room unread status:
|
||||
<strong>
|
||||
None
|
||||
</strong>
|
||||
, count:
|
||||
<strong>
|
||||
0
|
||||
</strong>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
Notification state is
|
||||
<strong />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
Room is
|
||||
<strong>
|
||||
not encrypted 🚨
|
||||
</strong>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>
|
||||
Main timeline
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Total: 0
|
||||
</li>
|
||||
<li>
|
||||
Highlight: 0
|
||||
</li>
|
||||
<li>
|
||||
Dot: false
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>
|
||||
Threads timeline
|
||||
</h2>
|
||||
<ul />
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<button>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, screen, waitFor } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import CreateKeyBackupDialog from "../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("../../../../../src/SecurityManager", () => ({
|
||||
accessSecretStorage: jest.fn().mockResolvedValue(undefined),
|
||||
withSecretStorageKeyCache: jest.fn().mockImplementation((fn) => fn()),
|
||||
}));
|
||||
|
||||
describe("CreateKeyBackupDialog", () => {
|
||||
beforeEach(() => {
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => createTestClient();
|
||||
});
|
||||
|
||||
it("should display the spinner when creating backup", () => {
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the spinner is displayed
|
||||
expect(screen.getByTestId("spinner")).toBeDefined();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error message when backup creation failed", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
mocked(matrixClient.hasSecretStorageKey).mockResolvedValue(true);
|
||||
mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => {
|
||||
throw new Error("failed");
|
||||
});
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the error message is displayed
|
||||
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error message when there is no Crypto available", async () => {
|
||||
const matrixClient = createTestClient();
|
||||
mocked(matrixClient.hasSecretStorageKey).mockResolvedValue(true);
|
||||
mocked(matrixClient.getCrypto).mockReturnValue(undefined);
|
||||
MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient;
|
||||
|
||||
render(<CreateKeyBackupDialog onFinished={jest.fn()} />);
|
||||
|
||||
// Check if the error message is displayed
|
||||
await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined());
|
||||
});
|
||||
|
||||
it("should display the success dialog when the key backup is finished", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const { asFragment } = render(<CreateKeyBackupDialog onFinished={onFinished} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("Your keys are being backed up (the first backup could take a few minutes)."),
|
||||
).toBeDefined(),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// Click on the OK button
|
||||
screen.getByRole("button", { name: "OK" }).click();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
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, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { Crypto, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
|
||||
import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import {
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsServer,
|
||||
} from "../../../../test-utils";
|
||||
import CreateSecretStorageDialog from "../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import RestoreKeyBackupDialog from "../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
|
||||
describe("CreateSecretStorageDialog", () => {
|
||||
let mockClient: MockedObject<MatrixClient>;
|
||||
let mockCrypto: MockedObject<Crypto.CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsCrypto(),
|
||||
uploadDeviceSigningKeys: jest.fn().mockImplementation(async () => {
|
||||
await sleep(0); // CreateSecretStorageDialog doesn't expect this to resolve immediately
|
||||
throw new MatrixError({ flows: [] });
|
||||
}),
|
||||
});
|
||||
|
||||
mockCrypto = mocked(mockClient.getCrypto()!);
|
||||
Object.assign(mockCrypto, {
|
||||
isKeyBackupTrusted: jest.fn(),
|
||||
isDehydrationSupported: jest.fn(() => false),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent(
|
||||
props: Partial<React.ComponentProps<typeof CreateSecretStorageDialog>> = {},
|
||||
): RenderResult {
|
||||
const onFinished = jest.fn();
|
||||
return render(<CreateSecretStorageDialog onFinished={onFinished} {...props} />);
|
||||
}
|
||||
|
||||
it("shows a loading spinner initially", async () => {
|
||||
const { container } = renderComponent();
|
||||
expect(screen.getByTestId("spinner")).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
describe("when there is an error fetching the backup version", () => {
|
||||
filterConsole("Error fetching backup data from server");
|
||||
|
||||
it("shows an error", async () => {
|
||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
||||
throw new Error("bleh bleh");
|
||||
});
|
||||
|
||||
const result = renderComponent();
|
||||
// XXX the error message is... misleading.
|
||||
await result.findByText("Unable to query secret storage status");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'Generate a Security Key' text if no key backup is present", async () => {
|
||||
const result = renderComponent();
|
||||
await flushPromises();
|
||||
expect(result.container).toMatchSnapshot();
|
||||
result.getByText("Generate a Security Key");
|
||||
});
|
||||
|
||||
describe("when canUploadKeysWithPasswordOnly", () => {
|
||||
// spy on Modal.createDialog
|
||||
let modalSpy: jest.SpyInstance;
|
||||
|
||||
// deferred which should be resolved to indicate that the created dialog has completed
|
||||
let restoreDialogFinishedDefer: IDeferred<[done?: boolean]>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
mockClient.uploadDeviceSigningKeys.mockImplementation(async () => {
|
||||
await sleep(0);
|
||||
throw new MatrixError({
|
||||
flows: [{ stages: ["m.login.password"] }],
|
||||
});
|
||||
});
|
||||
|
||||
restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
||||
modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: restoreDialogFinishedDefer.promise,
|
||||
close: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it("prompts for a password and then shows RestoreKeyBackupDialog", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalledWith(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
keyCallback: expect.any(Function),
|
||||
showSummary: false,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
});
|
||||
|
||||
it("calls bootstrapSecretStorage once keys are restored if the backup is now trusted", async () => {
|
||||
mockClient.isCryptoEnabled.mockReturnValue(true);
|
||||
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
|
||||
// While we restore the key backup, its signature becomes accepted
|
||||
mockCrypto.isKeyBackupTrusted.mockResolvedValue({ trusted: true } as BackupTrustInfo);
|
||||
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
await flushPromises();
|
||||
|
||||
// XXX no idea why this is a sensible thing to do. I just work here.
|
||||
expect(mockCrypto.bootstrapCrossSigning).toHaveBeenCalled();
|
||||
expect(mockCrypto.bootstrapSecretStorage).toHaveBeenCalled();
|
||||
|
||||
await result.findByText("Your keys are now being backed up from this device.");
|
||||
});
|
||||
|
||||
describe("when there is an error fetching the backup version after RestoreKeyBackupDialog", () => {
|
||||
filterConsole("Error fetching backup data from server");
|
||||
|
||||
it("handles the error sensibly", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText(/Enter your account password to confirm the upgrade/);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
await userEvent.type(result.getByPlaceholderText("Password"), "my pass");
|
||||
result.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
|
||||
mockClient.getKeyBackupVersion.mockImplementation(async () => {
|
||||
throw new Error("bleh bleh");
|
||||
});
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
await result.findByText("Unable to query secret storage status");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when backup is present but not trusted", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo);
|
||||
});
|
||||
|
||||
it("shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked", async () => {
|
||||
const result = renderComponent();
|
||||
await result.findByText("Restore your key backup to upgrade your encryption");
|
||||
expect(result.container).toMatchSnapshot();
|
||||
|
||||
// before we click "Restore", set up a spy on createDialog
|
||||
const restoreDialogFinishedDefer = defer<[done?: boolean]>();
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: restoreDialogFinishedDefer.promise,
|
||||
close: jest.fn(),
|
||||
});
|
||||
|
||||
result.getByRole("button", { name: "Restore" }).click();
|
||||
|
||||
expect(modalSpy).toHaveBeenCalledWith(
|
||||
RestoreKeyBackupDialog,
|
||||
{
|
||||
keyCallback: expect.any(Function),
|
||||
showSummary: false,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
// simulate RestoreKeyBackupDialog completing, to run that code path
|
||||
restoreDialogFinishedDefer.resolve([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { screen, fireEvent, render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import * as MegolmExportEncryption from "../../../../../src/utils/MegolmExportEncryption";
|
||||
import ExportE2eKeysDialog from "../../../../../src/async-components/views/dialogs/security/ExportE2eKeysDialog";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
|
||||
describe("ExportE2eKeysDialog", () => {
|
||||
it("renders", () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const { asFragment } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should have disabled submit button initially", () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
fireEvent.click(container.querySelector("[type=submit]")!);
|
||||
expect(screen.getByText("Enter passphrase")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should complain about weak passphrases", async () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
|
||||
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
const input = screen.getByLabelText("Enter passphrase");
|
||||
await userEvent.type(input, "password");
|
||||
fireEvent.click(container.querySelector("[type=submit]")!);
|
||||
await expect(screen.findByText("This is a top-10 common password")).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should complain if passphrases don't match", async () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
|
||||
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
await userEvent.type(screen.getByLabelText("Enter passphrase"), "ThisIsAMoreSecurePW123$$");
|
||||
await userEvent.type(screen.getByLabelText("Confirm passphrase"), "ThisIsAMoreSecurePW124$$");
|
||||
fireEvent.click(container.querySelector("[type=submit]")!);
|
||||
await expect(screen.findByText("Passphrases must match")).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should export if everything is fine", async () => {
|
||||
// Given a client able to export keys
|
||||
const cli = createTestClient();
|
||||
const keys: IMegolmSessionData[] = [];
|
||||
const passphrase = "ThisIsAMoreSecurePW123$$";
|
||||
const exportRoomKeysAsJson = jest.fn().mockResolvedValue(JSON.stringify(keys));
|
||||
cli.getCrypto = () => {
|
||||
return {
|
||||
exportRoomKeysAsJson,
|
||||
} as unknown as Crypto.CryptoApi;
|
||||
};
|
||||
|
||||
// Mock the result of encrypting the sessions. If we don't do this, the
|
||||
// encryption process fails, possibly because we didn't initialise
|
||||
// something.
|
||||
jest.spyOn(MegolmExportEncryption, "encryptMegolmKeyFile").mockResolvedValue(new ArrayBuffer(3));
|
||||
|
||||
// When we tell the dialog to export
|
||||
const { container } = render(<ExportE2eKeysDialog matrixClient={cli} onFinished={jest.fn()} />);
|
||||
await userEvent.type(screen.getByLabelText("Enter passphrase"), passphrase);
|
||||
await userEvent.type(screen.getByLabelText("Confirm passphrase"), passphrase);
|
||||
fireEvent.click(container.querySelector("[type=submit]")!);
|
||||
|
||||
// Then it exports keys and encrypts them
|
||||
await waitFor(() => expect(exportRoomKeysAsJson).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(MegolmExportEncryption.encryptMegolmKeyFile).toHaveBeenCalledWith(JSON.stringify(keys), passphrase),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Crypto } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ImportE2eKeysDialog from "../../../../../src/async-components/views/dialogs/security/ImportE2eKeysDialog";
|
||||
import * as MegolmExportEncryption from "../../../../../src/utils/MegolmExportEncryption";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
|
||||
describe("ImportE2eKeysDialog", () => {
|
||||
it("renders", () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const { asFragment } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should have disabled submit button initially", () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
expect(container.querySelector("[type=submit]")!).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable submit once file is uploaded and passphrase typed in", () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const file = new File(["test"], "file.txt", { type: "text/plain" });
|
||||
|
||||
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
fireEvent.change(container.querySelector("[type=file]")!, {
|
||||
target: { files: [file] },
|
||||
});
|
||||
fireEvent.change(container.querySelector("[type=password]")!, {
|
||||
target: { value: "passphrase" },
|
||||
});
|
||||
expect(container.querySelector("[type=submit]")!).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should enable submit once file is uploaded and passphrase pasted in", async () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const file = new File(["test"], "file.txt", { type: "text/plain" });
|
||||
|
||||
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
fireEvent.change(container.querySelector("[type=file]")!, {
|
||||
target: { files: [file] },
|
||||
});
|
||||
await userEvent.click(container.querySelector("[type=password]")!);
|
||||
await userEvent.paste("passphrase");
|
||||
expect(container.querySelector("[type=submit]")!).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should import exported keys on submit", async () => {
|
||||
const cli = createTestClient();
|
||||
const onFinished = jest.fn();
|
||||
const file = new File(["test"], "file.txt", { type: "text/plain" });
|
||||
const importRoomKeysAsJson = jest.fn();
|
||||
cli.getCrypto = () => {
|
||||
return {
|
||||
importRoomKeysAsJson,
|
||||
} as unknown as Crypto.CryptoApi;
|
||||
};
|
||||
|
||||
// Mock the result of decrypting the sessions, to avoid needing to
|
||||
// create encrypted input data.
|
||||
jest.spyOn(MegolmExportEncryption, "decryptMegolmKeyFile").mockResolvedValue("[]");
|
||||
|
||||
const { container } = render(<ImportE2eKeysDialog matrixClient={cli} onFinished={onFinished} />);
|
||||
fireEvent.change(container.querySelector("[type=file]")!, {
|
||||
target: { files: [file] },
|
||||
});
|
||||
await userEvent.click(container.querySelector("[type=password]")!);
|
||||
await userEvent.paste("passphrase");
|
||||
fireEvent.click(container.querySelector("[type=submit]")!);
|
||||
|
||||
await waitFor(() => expect(importRoomKeysAsJson).toHaveBeenCalled());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { screen, render, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
// Needed to be able to mock decodeRecoveryKey
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import * as recoveryKeyModule from "matrix-js-sdk/src/crypto-api/recovery-key";
|
||||
|
||||
import RestoreKeyBackupDialog from "../../../../../src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
|
||||
describe("<RestoreKeyBackupDialog />", () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32));
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display an error when recovery key is invalid", async () => {
|
||||
jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockImplementation(() => {
|
||||
throw new Error("Invalid recovery key");
|
||||
});
|
||||
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||
|
||||
await userEvent.type(screen.getByRole("textbox"), "invalid key");
|
||||
await waitFor(() => expect(screen.getByText("👎 Not a valid Security Key")).toBeInTheDocument());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not raise an error when recovery is valid", async () => {
|
||||
const { asFragment } = render(<RestoreKeyBackupDialog onFinished={jest.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText("Enter Security Key")).toBeInTheDocument());
|
||||
|
||||
await userEvent.type(screen.getByRole("textbox"), "valid key");
|
||||
await waitFor(() => expect(screen.getByText("👍 This looks like a valid Security Key!")).toBeInTheDocument());
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateKeyBackupDialog should display an error message when backup creation failed 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog 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"
|
||||
>
|
||||
Starting backup…
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Unable to create key backup
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CreateKeyBackupDialog should display the spinner when creating backup 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog 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"
|
||||
>
|
||||
Starting backup…
|
||||
</h1>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CreateKeyBackupDialog should display the success dialog when the key backup is finished 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateKeyBackupDialog 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"
|
||||
>
|
||||
Success!
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Your keys are being backed up (the first backup could take a few minutes).
|
||||
</p>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,551 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreateSecretStorageDialog shows 'Generate a Security Key' text if no key backup is present 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title mx_CreateSecretStorageDialog_centeredTitle"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Set up Secure Backup
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p
|
||||
class="mx_CreateSecretStorageDialog_centeredBody"
|
||||
>
|
||||
Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.
|
||||
</p>
|
||||
<div
|
||||
class="mx_CreateSecretStorageDialog_primaryContainer"
|
||||
role="radiogroup"
|
||||
>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked mx_StyledRadioButton_outlined"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
name="keyPassphrase"
|
||||
type="radio"
|
||||
value="key"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<div
|
||||
class="mx_CreateSecretStorageDialog_optionTitle"
|
||||
>
|
||||
<span
|
||||
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"
|
||||
/>
|
||||
Generate a Security Key
|
||||
</div>
|
||||
<div>
|
||||
We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_outlined"
|
||||
>
|
||||
<input
|
||||
name="keyPassphrase"
|
||||
type="radio"
|
||||
value="passphrase"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<div
|
||||
class="mx_CreateSecretStorageDialog_optionTitle"
|
||||
>
|
||||
<span
|
||||
class="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"
|
||||
/>
|
||||
Enter a Security Phrase
|
||||
</div>
|
||||
<div>
|
||||
Use a secret phrase only you know, and optionally save a Security Key to use for backup.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
</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"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Restore your key backup to upgrade your encryption
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Upgrade your encryption
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
Enter your account password to confirm the upgrade:
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_CreateSecretStorageDialog_password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_CreateSecretStorageDialog_password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="danger"
|
||||
type="button"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Unable to query secret storage status
|
||||
</p>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,112 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ExportE2eKeysDialog renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_exportE2eKeysDialog 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"
|
||||
>
|
||||
Export room keys
|
||||
</h1>
|
||||
</div>
|
||||
<form>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
>
|
||||
<p>
|
||||
This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.
|
||||
</p>
|
||||
<p>
|
||||
The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.
|
||||
</p>
|
||||
<div
|
||||
class="error"
|
||||
/>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputTable"
|
||||
>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputRow"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_PassphraseField"
|
||||
>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
id="mx_Field_1"
|
||||
label="Enter passphrase"
|
||||
placeholder="Enter passphrase"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Enter passphrase
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputRow"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
id="mx_Field_2"
|
||||
label="Confirm passphrase"
|
||||
placeholder="Confirm passphrase"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_2"
|
||||
>
|
||||
Confirm passphrase
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<input
|
||||
class="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value="Export"
|
||||
/>
|
||||
<button>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,113 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportE2eKeysDialog renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_importE2eKeysDialog 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"
|
||||
>
|
||||
Import room keys
|
||||
</h1>
|
||||
</div>
|
||||
<form>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
>
|
||||
<p>
|
||||
This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.
|
||||
</p>
|
||||
<p>
|
||||
The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.
|
||||
</p>
|
||||
<div
|
||||
class="error"
|
||||
/>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputTable"
|
||||
>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputRow"
|
||||
>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputLabel"
|
||||
>
|
||||
<label
|
||||
for="importFile"
|
||||
>
|
||||
File to import
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputCell"
|
||||
>
|
||||
<input
|
||||
id="importFile"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_E2eKeysDialog_inputRow"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Enter passphrase"
|
||||
placeholder="Enter passphrase"
|
||||
size="64"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Enter passphrase
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<input
|
||||
class="mx_Dialog_primary"
|
||||
disabled=""
|
||||
type="submit"
|
||||
value="Import"
|
||||
/>
|
||||
<button>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,298 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RestoreKeyBackupDialog /> should display an error when recovery key is invalid 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_RestoreKeyBackupDialog 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"
|
||||
>
|
||||
Enter Security Key
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
<strong>
|
||||
Warning
|
||||
</strong>
|
||||
: you should only set up key backup from a trusted computer.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||
</p>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||
>
|
||||
<input
|
||||
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||
value="invalid key"
|
||||
/>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||
>
|
||||
👎 Not a valid Security Key
|
||||
</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"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
If you've forgotten your Security Key you can
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
set up new recovery options
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RestoreKeyBackupDialog /> should not raise an error when recovery is valid 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_RestoreKeyBackupDialog 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"
|
||||
>
|
||||
Enter Security Key
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
<strong>
|
||||
Warning
|
||||
</strong>
|
||||
: you should only set up key backup from a trusted computer.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||
</p>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||
>
|
||||
<input
|
||||
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||
value="valid key"
|
||||
/>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||
>
|
||||
👍 This looks like a valid Security Key!
|
||||
</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"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
If you've forgotten your Security Key you can
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
set up new recovery options
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RestoreKeyBackupDialog /> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_RestoreKeyBackupDialog 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"
|
||||
>
|
||||
Enter Security Key
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
<strong>
|
||||
Warning
|
||||
</strong>
|
||||
: you should only set up key backup from a trusted computer.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Access your secure message history and set up secure messaging by entering your Security Key.
|
||||
</p>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_primaryContainer"
|
||||
>
|
||||
<input
|
||||
class="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="mx_RestoreKeyBackupDialog_keyStatus"
|
||||
/>
|
||||
<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"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
If you've forgotten your Security Key you can
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
set up new recovery options
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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 { IPublicRoomsChunkRoom } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { PublicRoomResultDetails } from "../../../../../src/components/views/dialogs/spotlight/PublicRoomResultDetails";
|
||||
|
||||
describe("PublicRoomResultDetails", () => {
|
||||
it("renders", () => {
|
||||
const { asFragment } = render(
|
||||
<PublicRoomResultDetails
|
||||
room={{
|
||||
room_id: "room-id",
|
||||
name: "hello?",
|
||||
canonical_alias: "canonical-alias",
|
||||
world_readable: true,
|
||||
guest_can_join: false,
|
||||
num_joined_members: 666,
|
||||
}}
|
||||
labelId="label-id"
|
||||
descriptionId="description-id"
|
||||
detailsId="details-id"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ canonical_alias: "canonical-alias" },
|
||||
{ aliases: ["alias-from-aliases"] },
|
||||
{ name: "name over alias", canonical_alias: "canonical-alias" },
|
||||
{
|
||||
name: "with an overly long name that will be truncated for sure, you can't say anything about it",
|
||||
topic: "with a topic!",
|
||||
},
|
||||
{ topic: "Very long topic " + new Array(1337).join("a") },
|
||||
])("Public room results", (partialPublicRoomChunk: Partial<IPublicRoomsChunkRoom>) => {
|
||||
const roomChunk: IPublicRoomsChunkRoom = {
|
||||
room_id: "room-id",
|
||||
world_readable: true,
|
||||
guest_can_join: false,
|
||||
num_joined_members: 666,
|
||||
...partialPublicRoomChunk,
|
||||
};
|
||||
|
||||
const { asFragment } = render(
|
||||
<PublicRoomResultDetails
|
||||
room={roomChunk}
|
||||
labelId="label-id"
|
||||
descriptionId="description-id"
|
||||
detailsId="details-id"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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, screen, RenderResult } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room, MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RoomResultContextMenus } from "../../../../../src/components/views/dialogs/spotlight/RoomResultContextMenus";
|
||||
import { filterConsole, stubClient } from "../../../../test-utils";
|
||||
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomResultContextMenus", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderRoomResultContextMenus = (): RenderResult => {
|
||||
return render(<RoomResultContextMenus room={room} />);
|
||||
};
|
||||
|
||||
filterConsole(
|
||||
// irrelevant for this test
|
||||
"Room !1:example.org does not have an m.room.create event",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render the room options context menu when UIComponent customisations disable room options", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
renderRoomResultContextMenus();
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
|
||||
expect(screen.queryByRole("button", { name: "Room options" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the room options context menu when UIComponent customisations enable room options", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
renderRoomResultContextMenus();
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
|
||||
expect(screen.queryByRole("button", { name: "Room options" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue