Prepare for repo merge

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

View file

@ -0,0 +1,192 @@
/*
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 { cleanup, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { ActionState, ActionTypes, AllActionStates, FormattingFunctions } from "@vector-im/matrix-wysiwyg";
import { FormattingButtons } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons";
import * as LinkModal from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/LinkModal";
import { setLanguage } from "../../../../../../src/languageHandler";
const mockWysiwyg = {
bold: jest.fn(),
italic: jest.fn(),
underline: jest.fn(),
strikeThrough: jest.fn(),
inlineCode: jest.fn(),
codeBlock: jest.fn(),
link: jest.fn(),
orderedList: jest.fn(),
unorderedList: jest.fn(),
quote: jest.fn(),
indent: jest.fn(),
unIndent: jest.fn(),
} as unknown as FormattingFunctions;
const openLinkModalSpy = jest.spyOn(LinkModal, "openLinkModal");
const testCases: Record<
Exclude<ActionTypes, "undo" | "redo" | "clear" | "indent" | "unindent">,
{ label: string; mockFormatFn: jest.Func | jest.SpyInstance }
> = {
bold: { label: "Bold", mockFormatFn: mockWysiwyg.bold },
italic: { label: "Italic", mockFormatFn: mockWysiwyg.italic },
underline: { label: "Underline", mockFormatFn: mockWysiwyg.underline },
strikeThrough: { label: "Strikethrough", mockFormatFn: mockWysiwyg.strikeThrough },
inlineCode: { label: "Code", mockFormatFn: mockWysiwyg.inlineCode },
codeBlock: { label: "Code block", mockFormatFn: mockWysiwyg.inlineCode },
link: { label: "Link", mockFormatFn: openLinkModalSpy },
orderedList: { label: "Numbered list", mockFormatFn: mockWysiwyg.orderedList },
unorderedList: { label: "Bulleted list", mockFormatFn: mockWysiwyg.unorderedList },
quote: { label: "Quote", mockFormatFn: mockWysiwyg.quote },
};
const createActionStates = (state: ActionState): AllActionStates => {
return Object.fromEntries(Object.keys(testCases).map((testKey) => [testKey, state])) as AllActionStates;
};
const defaultActionStates = createActionStates("enabled");
const renderComponent = (props = {}) => {
return render(<FormattingButtons composer={mockWysiwyg} actionStates={defaultActionStates} {...props} />);
};
const classes = {
active: "mx_FormattingButtons_active",
hover: "mx_FormattingButtons_Button_hover",
disabled: "mx_FormattingButtons_disabled",
};
describe("FormattingButtons", () => {
beforeEach(() => {
openLinkModalSpy.mockReturnValue(undefined);
});
afterEach(() => {
jest.resetAllMocks();
});
it("renders in german", async () => {
await setLanguage("de");
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
await setLanguage("en");
});
it("Each button should not have active class when enabled", () => {
renderComponent();
Object.values(testCases).forEach(({ label }) => {
expect(screen.getByLabelText(label)).not.toHaveClass(classes.active);
});
});
it("Each button should have active class when reversed", () => {
const reversedActionStates = createActionStates("reversed");
renderComponent({ actionStates: reversedActionStates });
Object.values(testCases).forEach((testCase) => {
const { label } = testCase;
expect(screen.getByLabelText(label)).toHaveClass(classes.active);
});
});
it("Each button should have disabled class when disabled", () => {
const disabledActionStates = createActionStates("disabled");
renderComponent({ actionStates: disabledActionStates });
Object.values(testCases).forEach((testCase) => {
const { label } = testCase;
expect(screen.getByLabelText(label)).toHaveClass(classes.disabled);
});
});
it("Should call wysiwyg function on button click", async () => {
renderComponent();
for (const testCase of Object.values(testCases)) {
const { label, mockFormatFn } = testCase;
screen.getByLabelText(label).click();
expect(mockFormatFn).toHaveBeenCalledTimes(1);
}
});
it("Each button should display the tooltip on mouse over when not disabled", async () => {
renderComponent();
for (const testCase of Object.values(testCases)) {
const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label));
await waitFor(() => expect(screen.getByText(label)).toBeInTheDocument());
}
});
it("Each button should not display the tooltip on mouse over when disabled", async () => {
const disabledActionStates = createActionStates("disabled");
renderComponent({ actionStates: disabledActionStates });
for (const testCase of Object.values(testCases)) {
const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label));
expect(screen.queryByText(label)).not.toBeInTheDocument();
}
});
it("Each button should have hover style when hovered and enabled", async () => {
renderComponent();
for (const testCase of Object.values(testCases)) {
const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label));
expect(screen.getByLabelText(label)).toHaveClass("mx_FormattingButtons_Button_hover");
}
});
it("Each button should not have hover style when hovered and reversed", async () => {
const reversedActionStates = createActionStates("reversed");
renderComponent({ actionStates: reversedActionStates });
for (const testCase of Object.values(testCases)) {
const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label));
expect(screen.getByLabelText(label)).not.toHaveClass("mx_FormattingButtons_Button_hover");
}
});
it("Does not show indent or unindent button when outside a list", () => {
renderComponent();
expect(screen.queryByLabelText("Indent increase")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Indent decrease")).not.toBeInTheDocument();
});
it("Shows indent and unindent buttons when either a single list type is 'reversed'", () => {
const orderedListActive = { ...defaultActionStates, orderedList: "reversed" };
renderComponent({ actionStates: orderedListActive });
expect(screen.getByLabelText("Indent increase")).toBeInTheDocument();
expect(screen.getByLabelText("Indent decrease")).toBeInTheDocument();
cleanup();
const unorderedListActive = { ...defaultActionStates, unorderedList: "reversed" };
renderComponent({ actionStates: unorderedListActive });
expect(screen.getByLabelText("Indent increase")).toBeInTheDocument();
expect(screen.getByLabelText("Indent decrease")).toBeInTheDocument();
});
});

View file

@ -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 { FormattingFunctions } from "@vector-im/matrix-wysiwyg";
import { render, screen, waitFor } from "jest-matrix-react";
import React from "react";
import userEvent from "@testing-library/user-event";
import { LinkModal } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/LinkModal";
import { mockPlatformPeg } from "../../../../../test-utils";
import * as selection from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
import { SubSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/types";
describe("LinkModal", () => {
const formattingFunctions = {
link: jest.fn(),
removeLinks: jest.fn(),
getLink: jest.fn().mockReturnValue("my initial content"),
} as unknown as FormattingFunctions;
const defaultValue: SubSelection = {
focusNode: null,
anchorNode: null,
focusOffset: 3,
anchorOffset: 4,
isForward: true,
};
const customRender = (isTextEnabled: boolean, onFinished: () => void, isEditing = false) => {
return render(
<LinkModal
composer={formattingFunctions}
isTextEnabled={isTextEnabled}
onFinished={onFinished}
composerContext={{ selection: defaultValue }}
isEditing={isEditing}
/>,
);
};
const selectionSpy = jest.spyOn(selection, "setSelection");
beforeEach(() => mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }));
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it("Should create a link", async () => {
// When
const onFinished = jest.fn();
customRender(false, onFinished);
// Then
expect(screen.getByLabelText("Link")).toBeTruthy();
expect(screen.getByText("Save")).toBeDisabled();
// When
await userEvent.type(screen.getByLabelText("Link"), "l");
// Then
await waitFor(() => {
expect(screen.getByText("Save")).toBeEnabled();
expect(screen.getByLabelText("Link")).toHaveAttribute("value", "l");
});
// When
jest.useFakeTimers();
screen.getByText("Save").click();
jest.runAllTimers();
// Then
await waitFor(() => {
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
expect(onFinished).toHaveBeenCalledTimes(1);
});
// Then
expect(formattingFunctions.link).toHaveBeenCalledWith("l", undefined);
});
it("Should create a link with text", async () => {
// When
const onFinished = jest.fn();
customRender(true, onFinished);
// Then
expect(screen.getByLabelText("Text")).toBeTruthy();
expect(screen.getByLabelText("Link")).toBeTruthy();
expect(screen.getByText("Save")).toBeDisabled();
// When
await userEvent.type(screen.getByLabelText("Text"), "t");
// Then
await waitFor(() => {
expect(screen.getByText("Save")).toBeDisabled();
expect(screen.getByLabelText("Text")).toHaveAttribute("value", "t");
});
// When
await userEvent.type(screen.getByLabelText("Link"), "l");
// Then
await waitFor(() => {
expect(screen.getByText("Save")).toBeEnabled();
expect(screen.getByLabelText("Link")).toHaveAttribute("value", "l");
});
// When
jest.useFakeTimers();
screen.getByText("Save").click();
jest.runAllTimers();
// Then
await waitFor(() => {
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
expect(onFinished).toHaveBeenCalledTimes(1);
});
// Then
expect(formattingFunctions.link).toHaveBeenCalledWith("l", "t");
});
it("Should remove the link", async () => {
// When
const onFinished = jest.fn();
customRender(true, onFinished, true);
await userEvent.click(screen.getByText("Remove"));
// Then
expect(formattingFunctions.removeLinks).toHaveBeenCalledTimes(1);
expect(onFinished).toHaveBeenCalledTimes(1);
});
it("Should display the link in editing", async () => {
// When
customRender(true, jest.fn(), true);
// Then
expect(screen.getByLabelText("Link")).toContainHTML("my initial content");
expect(screen.getByText("Save")).toBeDisabled();
// When
await userEvent.type(screen.getByLabelText("Link"), "l");
// Then
await waitFor(() => expect(screen.getByText("Save")).toBeEnabled());
});
});

View file

@ -0,0 +1,297 @@
/*
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 } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings";
import * as mockKeyboard from "../../../../../../src/Keyboard";
import { createMocks } from "../utils";
import RoomContext from "../../../../../../src/contexts/RoomContext";
describe("PlainTextComposer", () => {
const customRender = (
onChange = (_content: string): void => void 0,
onSend = (): void => void 0,
disabled = false,
initialContent?: string,
) => {
return render(
<PlainTextComposer
onChange={onChange}
onSend={onSend}
disabled={disabled}
initialContent={initialContent}
/>,
);
};
let mockUseSettingValue: jest.SpyInstance;
beforeEach(() => {
// defaults for these tests are:
// ctrlEnterToSend is false
mockUseSettingValue = jest.spyOn(mockUseSettingsHook, "useSettingValue").mockReturnValue(false);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("Should have contentEditable at false when disabled", () => {
// When
customRender(jest.fn(), jest.fn(), true);
// Then
expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false");
});
it("Should have focus", () => {
// When
customRender(jest.fn(), jest.fn(), false);
// Then
expect(screen.getByRole("textbox")).toHaveFocus();
});
it("Should call onChange handler", async () => {
// When
const content = "content";
const onChange = jest.fn();
customRender(onChange, jest.fn());
await userEvent.type(screen.getByRole("textbox"), content);
// Then
expect(onChange).toHaveBeenCalledWith(content);
});
it("Should call onSend when Enter is pressed when ctrlEnterToSend is false", async () => {
//When
const onSend = jest.fn();
customRender(jest.fn(), onSend);
await userEvent.type(screen.getByRole("textbox"), "{enter}");
// Then it sends a message
expect(onSend).toHaveBeenCalledTimes(1);
});
it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
await userEvent.type(screen.getByRole("textbox"), "{enter}");
// Then it does not send a message
expect(onSend).toHaveBeenCalledTimes(0);
});
it("Should only call onSend when ctrl+enter is pressed when ctrlEnterToSend is true on windows", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
await userEvent.type(textBox, "hello");
// Then it does NOT send a message on enter
await userEvent.type(textBox, "{enter}");
expect(onSend).toHaveBeenCalledTimes(0);
// Then it does NOT send a message on windows+enter
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
expect(onSend).toHaveBeenCalledTimes(0);
// Then it does send a message on ctrl+enter
await userEvent.type(textBox, "{control>}{enter}{control/}");
expect(onSend).toHaveBeenCalledTimes(1);
});
it("Should only call onSend when cmd+enter is pressed when ctrlEnterToSend is true on mac", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
Object.defineProperty(mockKeyboard, "IS_MAC", { value: true });
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
await userEvent.type(textBox, "hello");
// Then it does NOT send a message on enter
await userEvent.type(textBox, "{enter}");
expect(onSend).toHaveBeenCalledTimes(0);
// Then it does NOT send a message on ctrl+enter
await userEvent.type(textBox, "{control>}{enter}{control/}");
expect(onSend).toHaveBeenCalledTimes(0);
// Then it does send a message on cmd+enter
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
expect(onSend).toHaveBeenCalledTimes(1);
});
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is false", async () => {
//When
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const inputWithShiftEnter = "new{Shift>}{enter}{/Shift}line";
const expectedInnerHtml = "new\nline";
await userEvent.click(textBox);
await userEvent.type(textBox, inputWithShiftEnter);
// Then it does not send a message, but inserts a newline character
expect(onSend).toHaveBeenCalledTimes(0);
expect(textBox.innerHTML).toBe(expectedInnerHtml);
});
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const keyboardInput = "new{Shift>}{enter}{/Shift}line";
const expectedInnerHtml = "new\nline";
await userEvent.click(textBox);
await userEvent.type(textBox, keyboardInput);
// Then it does not send a message, but inserts a newline character
expect(onSend).toHaveBeenCalledTimes(0);
expect(textBox.innerHTML).toBe(expectedInnerHtml);
});
it("Should not insert div and br tags when enter is pressed when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const enterThenTypeHtml = "<div>hello</div";
await userEvent.click(textBox);
await userEvent.type(textBox, "{enter}hello");
// Then it does not send a message, but inserts a newline character
expect(onSend).toHaveBeenCalledTimes(0);
expect(textBox).not.toContainHTML(enterThenTypeHtml);
});
it("Should not insert div tags when enter is pressed then user types more when ctrlEnterToSend is true", async () => {
//When
mockUseSettingValue.mockReturnValue(true);
const onSend = jest.fn();
customRender(jest.fn(), onSend);
const textBox = screen.getByRole("textbox");
const defaultEnterHtml = "<div><br></div";
await userEvent.click(textBox);
await userEvent.type(textBox, "{enter}");
// Then it does not send a message, but inserts a newline character
expect(onSend).toHaveBeenCalledTimes(0);
expect(textBox).not.toContainHTML(defaultEnterHtml);
});
it("Should clear textbox content when clear is called", async () => {
//When
let composer: {
clear: () => void;
insertText: (text: string) => void;
};
render(
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
{(ref, composerFunctions) => {
composer = composerFunctions;
return null;
}}
</PlainTextComposer>,
);
await userEvent.type(screen.getByRole("textbox"), "content");
expect(screen.getByRole("textbox").innerHTML).toBe("content");
composer!.clear();
// Then
expect(screen.getByRole("textbox").innerHTML).toBeFalsy();
});
it("Should have data-is-expanded when it has two lines", async () => {
let resizeHandler: ResizeObserverCallback = jest.fn();
let editor: Element | null = null;
jest.spyOn(global, "ResizeObserver").mockImplementation((handler) => {
resizeHandler = handler;
return {
observe: (element) => {
editor = element;
},
unobserve: jest.fn(),
disconnect: jest.fn(),
};
});
jest.useFakeTimers();
//When
render(<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />);
// Then
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("false");
expect(editor).toBe(screen.getByRole("textbox"));
// When
resizeHandler(
[{ contentBoxSize: [{ blockSize: 100 }] } as unknown as ResizeObserverEntry],
{} as ResizeObserver,
);
act(() => {
jest.runAllTimers();
});
// Then
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("true");
jest.useRealTimers();
(global.ResizeObserver as jest.Mock).mockRestore();
});
it("Should not render <Autocomplete /> if not wrapped in room context", () => {
customRender();
expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument();
});
it("Should render <Autocomplete /> if wrapped in room context", () => {
const { defaultRoomContext } = createMocks();
render(
<RoomContext.Provider value={defaultRoomContext}>
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} disabled={false} initialContent="" />
</RoomContext.Provider>,
);
expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument();
});
it("Should allow pasting of text values", async () => {
customRender();
const textBox = screen.getByRole("textbox");
await userEvent.click(textBox);
await userEvent.type(textBox, "hello");
await userEvent.paste(" world");
expect(textBox).toHaveTextContent("hello world");
});
});

View file

@ -0,0 +1,124 @@
/*
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 "@testing-library/jest-dom";
import React, { createRef } from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import { WysiwygAutocomplete } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete";
import { getRoomContext, mkStubRoom, stubClient } from "../../../../../test-utils";
import Autocomplete from "../../../../../../src/components/views/rooms/Autocomplete";
import Autocompleter, { ICompletion } from "../../../../../../src/autocomplete/Autocompleter";
import AutocompleteProvider from "../../../../../../src/autocomplete/AutocompleteProvider";
const mockCompletion: ICompletion[] = [
{
type: "user",
completion: "user_1",
completionId: "@user_1:host.local",
range: { start: 1, end: 1 },
component: <div>user_1</div>,
},
{
type: "user",
completion: "user_2",
completionId: "@user_2:host.local",
range: { start: 1, end: 1 },
component: <div>user_2</div>,
},
];
const constructMockProvider = (data: ICompletion[]) =>
({
getCompletions: jest.fn().mockImplementation(async () => data),
getName: jest.fn().mockReturnValue("test provider"),
renderCompletions: jest.fn().mockImplementation((components) => components),
}) as unknown as AutocompleteProvider;
describe("WysiwygAutocomplete", () => {
beforeAll(() => {
// scrollTo not implemented in JSDOM
window.HTMLElement.prototype.scrollTo = function () {};
});
afterAll(() => {
jest.restoreAllMocks();
});
const autocompleteRef = createRef<Autocomplete>();
const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([
{
completions: mockCompletion,
provider: constructMockProvider(mockCompletion),
command: { command: ["truthy"] as RegExpExecArray }, // needed for us to unhide the autocomplete when testing
},
]);
const mockHandleMention = jest.fn();
const mockHandleCommand = jest.fn();
const mockHandleAtRoomMention = jest.fn();
const renderComponent = (props: Partial<React.ComponentProps<typeof WysiwygAutocomplete>> = {}) => {
const mockClient = stubClient();
const mockRoom = mkStubRoom("test_room", "test_room", mockClient);
const mockRoomContext = getRoomContext(mockRoom, {});
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={mockRoomContext}>
<WysiwygAutocomplete
ref={autocompleteRef}
suggestion={null}
handleMention={mockHandleMention}
handleCommand={mockHandleCommand}
handleAtRoomMention={mockHandleAtRoomMention}
{...props}
/>
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
};
it("does not show the autocomplete when room is undefined", () => {
render(
<WysiwygAutocomplete
ref={autocompleteRef}
suggestion={null}
handleMention={mockHandleMention}
handleCommand={mockHandleCommand}
handleAtRoomMention={mockHandleAtRoomMention}
/>,
);
expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument();
});
it("does not call for suggestions with a null suggestion prop", async () => {
// render the component, the default props have suggestion = null
renderComponent();
// check that getCompletions is not called, and we have no suggestions
expect(getCompletionsSpy).not.toHaveBeenCalled();
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
it("calls getCompletions when given a valid suggestion prop", async () => {
renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } });
// wait for getCompletions to have been called
await waitFor(() => {
expect(getCompletionsSpy).toHaveBeenCalled();
});
// check that some suggestions are shown
expect(screen.getByRole("presentation")).toBeInTheDocument();
// and that they are the mock completions
mockCompletion.forEach(({ completion }) => expect(screen.getByText(completion)).toBeInTheDocument());
});
});

View file

@ -0,0 +1,871 @@
/*
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 "@testing-library/jest-dom";
import React from "react";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { flushPromises, mockPlatformPeg, stubClient, mkStubRoom } from "../../../../../test-utils";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import * as EventUtils from "../../../../../../src/utils/EventUtils";
import { Action } from "../../../../../../src/dispatcher/actions";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import {
ComposerContext,
getDefaultContextValue,
} from "../../../../../../src/components/views/rooms/wysiwyg_composer/ComposerContext";
import { createMocks } from "../utils";
import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer";
import { SubSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/types";
import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
import { parseEditorStateTransfer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent";
import Autocompleter, { ICompletion } from "../../../../../../src/autocomplete/Autocompleter";
import AutocompleteProvider from "../../../../../../src/autocomplete/AutocompleteProvider";
import * as Permalinks from "../../../../../../src/utils/permalinks/Permalinks";
import { PermalinkParts } from "../../../../../../src/utils/permalinks/PermalinkConstructor";
describe("WysiwygComposer", () => {
const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => {
const { mockClient, defaultRoomContext } = createMocks();
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<WysiwygComposer
onChange={onChange}
onSend={onSend}
disabled={disabled}
initialContent={initialContent}
/>
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
};
afterEach(() => {
jest.resetAllMocks();
});
it("Should have contentEditable at false when disabled", async () => {
// When
customRender(jest.fn(), jest.fn(), true);
// Then
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false"));
});
describe("Standard behavior", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
customRender(onChange, onSend);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
afterEach(() => {
onChange.mockReset();
onSend.mockReset();
});
it("Should have contentEditable at true", async () => {
// Then
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
it("Should have focus", async () => {
// Then
await waitFor(() => expect(screen.getByRole("textbox")).toHaveFocus());
});
it("Should call onChange handler", async () => {
// When
fireEvent.input(screen.getByRole("textbox"), {
data: "foo bar",
inputType: "insertText",
});
// Then
await waitFor(() => expect(onChange).toHaveBeenCalledWith("foo bar"));
});
it("Should call onSend when Enter is pressed", async () => {
//When
fireEvent(
screen.getByRole("textbox"),
new InputEvent("input", {
inputType: "insertParagraph",
}),
);
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1));
});
it("Should not call onSend when Shift+Enter is pressed", async () => {
//When
await userEvent.type(screen.getByRole("textbox"), "{shift>}{enter}");
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
});
it("Should not call onSend when ctrl+Enter is pressed", async () => {
//When
// Using userEvent.type or .keyboard wasn't working as expected in the case of ctrl+enter
fireEvent(
screen.getByRole("textbox"),
new KeyboardEvent("keydown", {
ctrlKey: true,
code: "Enter",
}),
);
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
});
it("Should not call onSend when alt+Enter is pressed", async () => {
//When
await userEvent.type(screen.getByRole("textbox"), "{alt>}{enter}");
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
});
it("Should not call onSend when meta+Enter is pressed", async () => {
//When
await userEvent.type(screen.getByRole("textbox"), "{meta>}{enter}");
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
});
});
describe("Mentions and commands", () => {
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
const mockCompletions: ICompletion[] = [
{
type: "user",
href: "https://matrix.to/#/@user_1:element.io",
completion: "user_1",
completionId: "@user_1:host.local",
range: { start: 1, end: 1 },
component: <div>user_1</div>,
},
{
type: "user",
href: "https://matrix.to/#/@user_2:element.io",
completion: "user_2",
completionId: "@user_2:host.local",
range: { start: 1, end: 1 },
component: <div>user_2</div>,
},
{
// no href user
type: "user",
href: undefined,
completion: "user_without_href",
completionId: "@user_3:host.local",
range: { start: 1, end: 1 },
component: <div>user_without_href</div>,
},
{
type: "room",
href: "https://matrix.to/#/#room_1:element.io",
completion: "#room_with_completion_id",
completionId: "@room_1:host.local",
range: { start: 1, end: 1 },
component: <div>room_with_completion_id</div>,
},
{
type: "room",
href: "https://matrix.to/#/#room_2:element.io",
completion: "#room_without_completion_id",
range: { start: 1, end: 1 },
component: <div>room_without_completion_id</div>,
},
{
type: "command",
completion: "/spoiler",
range: { start: 1, end: 1 },
component: <div>/spoiler</div>,
},
{
type: "at-room",
completion: "@room",
range: { start: 1, end: 1 },
component: <div>@room</div>,
},
{
type: "community",
completion: "community-completion",
range: { start: 1, end: 1 },
component: <div>community</div>,
},
];
const constructMockProvider = (data: ICompletion[]) =>
({
getCompletions: jest.fn().mockImplementation(async () => data),
getName: jest.fn().mockReturnValue("test provider"),
renderCompletions: jest.fn().mockImplementation((components) => components),
}) as unknown as AutocompleteProvider;
// for each test we will insert input simulating a user mention
const initialInput = "@abc";
const insertMentionInput = async () => {
fireEvent.input(screen.getByRole("textbox"), {
data: initialInput,
inputType: "insertText",
});
// the autocomplete suggestions container has the presentation role, wait for it to be present
expect(await screen.findByRole("presentation")).toBeInTheDocument();
};
beforeEach(async () => {
// setup the required spies
jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([
{
completions: mockCompletions,
provider: constructMockProvider(mockCompletions),
command: { command: ["truthy"] as RegExpExecArray }, // needed for us to unhide the autocomplete when testing
},
]);
jest.spyOn(Permalinks, "parsePermalink").mockReturnValue({
userId: "mockParsedUserId",
} as unknown as PermalinkParts);
// then render the component and wait for the composer to be ready
customRender();
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
afterEach(() => {
jest.clearAllMocks();
});
it("shows the autocomplete when text has @ prefix and autoselects the first item", async () => {
await insertMentionInput();
expect(screen.getByText(mockCompletions[0].completion)).toHaveAttribute("aria-selected", "true");
});
it("pressing up and down arrows allows us to change the autocomplete selection", async () => {
await insertMentionInput();
// press the down arrow - nb using .keyboard allows us to not have to specify a node, which
// means that we know the autocomplete is correctly catching the event
await userEvent.keyboard("{ArrowDown}");
expect(screen.getByText(mockCompletions[0].completion)).toHaveAttribute("aria-selected", "false");
expect(screen.getByText(mockCompletions[1].completion)).toHaveAttribute("aria-selected", "true");
// reverse the process and check again
await userEvent.keyboard("{ArrowUp}");
expect(screen.getByText(mockCompletions[0].completion)).toHaveAttribute("aria-selected", "true");
expect(screen.getByText(mockCompletions[1].completion)).toHaveAttribute("aria-selected", "false");
});
it("pressing enter selects the mention and inserts it into the composer as a link", async () => {
await insertMentionInput();
// press enter
await userEvent.keyboard("{Enter}");
screen.debug();
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
// check that it inserts the completion text as a link
expect(screen.getByRole("link", { name: mockCompletions[0].completion })).toBeInTheDocument();
});
it("pressing escape closes the autocomplete", async () => {
await insertMentionInput();
// press escape
await userEvent.keyboard("{Escape}");
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
});
it("typing with the autocomplete open still works as expected", async () => {
await insertMentionInput();
// add some more text, then check the autocomplete is open AND the text is in the composer
await userEvent.keyboard("extra");
expect(screen.queryByRole("presentation")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toHaveTextContent("@abcextra");
});
it("clicking on a mention in the composer dispatches the correct action", async () => {
await insertMentionInput();
// press enter
await userEvent.keyboard("{Enter}");
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
// click on the user mention link that has been inserted
await userEvent.click(screen.getByRole("link", { name: mockCompletions[0].completion }));
expect(dispatchSpy).toHaveBeenCalledTimes(1);
// this relies on the output from the mock function in mkStubRoom
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
action: Action.ViewUser,
member: expect.objectContaining({
userId: mkStubRoom(undefined, undefined, undefined).getMember("any")?.userId,
}),
}),
);
});
it("selecting a mention without a href closes the autocomplete but does not insert a mention", async () => {
await insertMentionInput();
// select the relevant user by clicking
await userEvent.click(screen.getByText("user_without_href"));
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
// check that it has not inserted a link
expect(screen.queryByRole("link", { name: "user_without_href" })).not.toBeInTheDocument();
});
it("selecting a room mention with a completionId uses client.getRoom", async () => {
await insertMentionInput();
// select the room suggestion by clicking
await userEvent.click(screen.getByText("room_with_completion_id"));
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
// check that it has inserted a link and looked up the name from the mock client
// which will always return 'My room'
expect(screen.getByRole("link", { name: "My room" })).toBeInTheDocument();
});
it("selecting a room mention without a completionId uses client.getRooms", async () => {
await insertMentionInput();
// select the room suggestion
await userEvent.click(screen.getByText("room_without_completion_id"));
// check that it closes the autocomplete
await waitFor(() => {
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
// check that it has inserted a link and falls back to the completion text
expect(screen.getByRole("link", { name: "#room_without_completion_id" })).toBeInTheDocument();
});
it("selecting a command inserts the command", async () => {
await insertMentionInput();
// select the room suggestion
await userEvent.click(screen.getByText("/spoiler"));
// check that it has inserted the plain text
expect(screen.getByText("/spoiler")).toBeInTheDocument();
});
it("selecting an at-room completion inserts @room", async () => {
await insertMentionInput();
// select the room suggestion
await userEvent.click(screen.getByText("@room"));
// check that it has inserted the @room link
expect(screen.getByRole("link", { name: "@room" })).toBeInTheDocument();
});
it("allows a community completion to pass through", async () => {
await insertMentionInput();
// select the room suggestion
await userEvent.click(screen.getByText("community"));
// check that it we still have the initial text
expect(screen.getByText(initialInput)).toBeInTheDocument();
});
});
describe("When emoticons should be replaced by emojis", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "MessageComposerInput.autoReplaceEmoji") return true;
});
customRender(onChange, onSend);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
it("typing a space to trigger an emoji replacement", async () => {
fireEvent.input(screen.getByRole("textbox"), {
data: ":P",
inputType: "insertText",
});
fireEvent.input(screen.getByRole("textbox"), {
data: " ",
inputType: "insertText",
});
await waitFor(() => expect(onChange).toHaveBeenNthCalledWith(3, expect.stringContaining("😛")));
});
it("typing a space to trigger an emoji varitation replacement", async () => {
fireEvent.input(screen.getByRole("textbox"), {
data: ":-P",
inputType: "insertText",
});
fireEvent.input(screen.getByRole("textbox"), {
data: " ",
inputType: "insertText",
});
await waitFor(() => expect(onChange).toHaveBeenNthCalledWith(3, expect.stringContaining("😛")));
});
});
describe("When settings require Ctrl+Enter to send", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "MessageComposerInput.ctrlEnterToSend") return true;
});
customRender(onChange, onSend);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
afterEach(() => {
onChange.mockReset();
onSend.mockReset();
});
it("Should not call onSend when Enter is pressed", async () => {
// When
const textbox = screen.getByRole("textbox");
fireEvent(
textbox,
new InputEvent("input", {
inputType: "insertParagraph",
}),
);
// Then it does not send a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(0));
fireEvent(
textbox,
new InputEvent("input", {
inputType: "insertText",
data: "other",
}),
);
// The focus is on the last text node
await waitFor(() => {
const selection = document.getSelection();
if (selection) {
// eslint-disable-next-line jest/no-conditional-expect
expect(selection.focusNode?.textContent).toEqual("other");
}
});
});
it("Should send a message when Ctrl+Enter is pressed", async () => {
// When
fireEvent(
screen.getByRole("textbox"),
new InputEvent("input", {
inputType: "sendMessage",
}),
);
// Then it sends a message
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1));
});
});
describe("Keyboard navigation", () => {
const { mockClient, defaultRoomContext, mockEvent, editorStateTransfer } = createMocks();
const customRender = (
client = mockClient,
roomContext = defaultRoomContext,
_editorStateTransfer?: EditorStateTransfer,
) => {
return render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ComposerContext.Provider
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
>
<WysiwygComposer
onChange={jest.fn()}
onSend={jest.fn()}
initialContent={
roomContext.room && _editorStateTransfer
? parseEditorStateTransfer(_editorStateTransfer, roomContext.room, client)
: undefined
}
/>
</ComposerContext.Provider>
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
};
afterEach(() => {
jest.resetAllMocks();
});
const setup = async (
editorState?: EditorStateTransfer,
client = stubClient(),
roomContext = defaultRoomContext,
) => {
const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
customRender(client, roomContext, editorState);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
return { textbox: screen.getByRole("textbox"), spyDispatcher };
};
beforeEach(() => {
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
});
describe("In message creation", () => {
it("Should not moving when the composer is filled", async () => {
// When
const { textbox, spyDispatcher } = await setup();
fireEvent.input(textbox, {
data: "word",
inputType: "insertText",
});
// Move at the beginning of the composer
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Then
expect(spyDispatcher).toHaveBeenCalledTimes(0);
});
it("Should moving when the composer is empty", async () => {
// When
const { textbox, spyDispatcher } = await setup();
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Then
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: mockEvent,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
});
});
});
describe("In message editing", () => {
function select(selection: SubSelection) {
return act(async () => {
await setSelection(selection);
// the event is not automatically fired by jest
document.dispatchEvent(new CustomEvent("selectionchange"));
});
}
describe("Moving up", () => {
it("Should not moving when caret is not at beginning of the text", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
const textNode = textbox.firstChild;
await select({
anchorNode: textNode,
anchorOffset: 1,
focusNode: textNode,
focusOffset: 2,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Then
expect(spyDispatcher).toHaveBeenCalledTimes(0);
});
it("Should not moving when the content has changed", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
fireEvent.input(textbox, {
data: "word",
inputType: "insertText",
});
const textNode = textbox.firstChild;
await select({
anchorNode: textNode,
anchorOffset: 0,
focusNode: textNode,
focusOffset: 0,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Then
expect(spyDispatcher).toHaveBeenCalledTimes(0);
});
it("Should moving up", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
const textNode = textbox.firstChild;
await select({
anchorNode: textNode,
anchorOffset: 0,
focusNode: textNode,
focusOffset: 0,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// Then
await waitFor(() =>
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: mockEvent,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
}),
);
});
it("Should moving up in list", async () => {
// When
const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks(
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
);
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext);
const textNode = textbox.firstChild;
await select({
anchorNode: textNode,
anchorOffset: 0,
focusNode: textNode,
focusOffset: 0,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowUp",
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// Then
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: mockEvent,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
});
});
});
describe("Moving down", () => {
it("Should not moving when caret is not at the end of the text", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
const brNode = textbox.lastChild;
await select({
anchorNode: brNode,
anchorOffset: 0,
focusNode: brNode,
focusOffset: 0,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowDown",
});
// Then
expect(spyDispatcher).toHaveBeenCalledTimes(0);
});
it("Should not moving when the content has changed", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
fireEvent.input(textbox, {
data: "word",
inputType: "insertText",
});
const brNode = textbox.lastChild;
await select({
anchorNode: brNode,
anchorOffset: 0,
focusNode: brNode,
focusOffset: 0,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowDown",
});
// Then
expect(spyDispatcher).toHaveBeenCalledTimes(0);
});
it("Should moving down", async () => {
// When
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
// Skipping the BR tag
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
const { length } = textNode.textContent || "";
await select({
anchorNode: textNode,
anchorOffset: length,
focusNode: textNode,
focusOffset: length,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowDown",
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// Then
await waitFor(() =>
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: mockEvent,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
}),
);
});
it("Should moving down in list", async () => {
// When
const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks(
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
);
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext);
// Skipping the BR tag and get the text node inside the last LI tag
const textNode = textbox.childNodes[textbox.childNodes.length - 2].lastChild?.lastChild || textbox;
const { length } = textNode.textContent || "";
await select({
anchorNode: textNode,
anchorOffset: length,
focusNode: textNode,
focusOffset: length,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowDown",
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// Then
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: mockEvent,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
});
});
it("Should close editing", async () => {
// When
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(undefined);
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
// Skipping the BR tag
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
const { length } = textNode.textContent || "";
await select({
anchorNode: textNode,
anchorOffset: length,
focusNode: textNode,
focusOffset: length,
isForward: true,
});
fireEvent.keyDown(textbox, {
key: "ArrowDown",
});
// Wait for event dispatch to happen
await act(async () => {
await flushPromises();
});
// Then
await waitFor(() =>
expect(spyDispatcher).toHaveBeenCalledWith({
action: Action.EditEvent,
event: null,
timelineRenderingType: defaultRoomContext.timelineRenderingType,
}),
);
});
});
});
});
});

View file

@ -0,0 +1,200 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormattingButtons renders in german 1`] = `
<DocumentFragment>
<div
class="mx_FormattingButtons"
>
<button
aria-label="Fett"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.8 19c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 6.8 17V7c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 8.8 5h3.525c1.083 0 2.083.333 3 1 .917.667 1.375 1.592 1.375 2.775 0 .85-.192 1.504-.575 1.963-.383.458-.742.787-1.075.987.417.183.88.525 1.387 1.025.509.5.763 1.25.763 2.25 0 1.483-.542 2.52-1.625 3.113-1.083.591-2.1.887-3.05.887H8.8Zm1.025-2.8h2.6c.8 0 1.287-.204 1.462-.612.175-.409.263-.705.263-.888 0-.183-.088-.48-.263-.887-.175-.409-.687-.613-1.537-.613H9.825v3Zm0-5.7h2.325c.55 0 .95-.142 1.2-.425a1.4 1.4 0 0 0 .375-.95c0-.4-.142-.725-.425-.975-.283-.25-.65-.375-1.1-.375H9.825V10.5Z"
/>
</svg>
</button>
<button
aria-label="Kursiv"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.25 19c-.35 0-.646-.12-.888-.363A1.207 1.207 0 0 1 5 17.75c0-.35.12-.646.362-.887.242-.242.538-.363.888-.363H9l3-9H9.25c-.35 0-.646-.12-.887-.362A1.207 1.207 0 0 1 8 6.25c0-.35.12-.646.363-.888A1.21 1.21 0 0 1 9.25 5h7.5c.35 0 .646.12.887.362.242.242.363.538.363.888s-.12.646-.363.888a1.207 1.207 0 0 1-.887.362H14.5l-3 9h2.25c.35 0 .646.12.887.363.242.241.363.537.363.887s-.12.646-.363.887a1.207 1.207 0 0 1-.887.363h-7.5Z"
/>
</svg>
</button>
<button
aria-label="Unterstrichen"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 21a.967.967 0 0 1-.713-.288A.968.968 0 0 1 5 20a.97.97 0 0 1 .287-.712A.967.967 0 0 1 6 19h12c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.712A.968.968 0 0 1 18 21H6Zm6-4c-1.683 0-2.992-.525-3.925-1.575-.933-1.05-1.4-2.442-1.4-4.175V4.275c0-.35.13-.65.388-.9A1.27 1.27 0 0 1 7.974 3c.35 0 .65.125.9.375s.375.55.375.9V11.4c0 .933.233 1.692.7 2.275.467.583 1.15.875 2.05.875.9 0 1.583-.292 2.05-.875.467-.583.7-1.342.7-2.275V4.275c0-.35.13-.65.387-.9A1.27 1.27 0 0 1 16.05 3c.35 0 .65.125.9.375s.375.55.375.9v6.975c0 1.733-.467 3.125-1.4 4.175C14.992 16.475 13.683 17 12 17Z"
/>
</svg>
</button>
<button
aria-label="Durchgestrichen"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.15 20c-1.267 0-2.392-.375-3.375-1.125-.983-.75-1.692-1.775-2.125-3.075l2.2-.95c.233.8.638 1.458 1.213 1.975.574.517 1.287.775 2.137.775.7 0 1.333-.167 1.9-.5.567-.333.85-.867.85-1.6 0-.3-.058-.575-.175-.825A2.362 2.362 0 0 0 14.3 14h2.8a4.279 4.279 0 0 1 .25 1.5c0 1.433-.513 2.542-1.538 3.325C14.788 19.608 13.567 20 12.15 20ZM3 12a.967.967 0 0 1-.712-.287A.968.968 0 0 1 2 11c0-.283.096-.52.288-.712A.967.967 0 0 1 3 10h18c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.713A.968.968 0 0 1 21 12H3Zm9.05-8.15c1.1 0 2.063.27 2.887.813.825.541 1.463 1.37 1.913 2.487l-2.2.975a2.987 2.987 0 0 0-.838-1.3c-.408-.383-.979-.575-1.712-.575-.683 0-1.25.154-1.7.463-.45.308-.7.737-.75 1.287h-2.4c.033-1.15.487-2.13 1.363-2.937.875-.809 2.02-1.213 3.437-1.213Z"
/>
</svg>
</button>
<button
aria-label="Ungeordnete Liste"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.5 7.5a1.45 1.45 0 0 1-1.06-.44A1.444 1.444 0 0 1 3 6c0-.412.147-.766.44-1.06A1.45 1.45 0 0 1 4.5 4.5c.412 0 .766.147 1.06.44.293.294.44.647.44 1.06 0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44Zm4.787 11.212c.192.192.43.288.713.288h10c.283 0 .52-.096.712-.288A.968.968 0 0 0 21 18a.968.968 0 0 0-.288-.712A.968.968 0 0 0 20 17H10a.967.967 0 0 0-.713.288A.968.968 0 0 0 9 18c0 .283.096.52.287.712Zm0-5.999c.192.191.43.287.713.287h10a.97.97 0 0 0 .712-.287A.968.968 0 0 0 21 12a.968.968 0 0 0-.288-.713A.968.968 0 0 0 20 11H10a.967.967 0 0 0-.713.287A.968.968 0 0 0 9 12c0 .283.096.52.287.713Zm0-6c.192.191.43.287.713.287h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 21 6a.967.967 0 0 0-.288-.713A.968.968 0 0 0 20 5H10a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 6c0 .283.096.52.287.713ZM3.44 19.06c.294.293.648.44 1.06.44a1.45 1.45 0 0 0 1.06-.44c.293-.294.44-.647.44-1.06 0-.413-.147-.766-.44-1.06a1.445 1.445 0 0 0-1.06-.44 1.45 1.45 0 0 0-1.06.44c-.293.294-.44.647-.44 1.06 0 .413.147.766.44 1.06ZM4.5 13.5a1.45 1.45 0 0 1-1.06-.44A1.445 1.445 0 0 1 3 12c0-.412.147-.766.44-1.06a1.45 1.45 0 0 1 1.06-.44c.412 0 .766.147 1.06.44.293.294.44.648.44 1.06 0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44Z"
/>
</svg>
</button>
<button
aria-label="Nummerierte Liste"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 6a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H10a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H10a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H10a1 1 0 0 1-1-1ZM5.604 5.089A.75.75 0 0 1 6 5.75v4.5a.75.75 0 0 1-1.5 0V7.151l-.334.223a.75.75 0 0 1-.832-1.248l1.5-1a.75.75 0 0 1 .77-.037ZM5 13a2.02 2.02 0 0 0-1.139.321 1.846 1.846 0 0 0-.626.719 2.286 2.286 0 0 0-.234.921v.023l-.001.01v.005l.75.001H3a.75.75 0 0 0 1.5.01V15a.789.789 0 0 1 .077-.29.35.35 0 0 1 .116-.14c.04-.027.126-.07.307-.07s.267.043.307.07a.35.35 0 0 1 .116.14.788.788 0 0 1 .076.29v.008a.532.532 0 0 1-.14.352l-2.161 2.351a.748.748 0 0 0-.198.523v.016c0 .414.336.75.75.75h2.5a.75.75 0 0 0 0-1.5h-.82l1.034-1.124C6.809 16 7 15.51 7 15h-.75H7v-.039l-.004-.068a2.285 2.285 0 0 0-.231-.853 1.846 1.846 0 0 0-.626-.719A2.02 2.02 0 0 0 5 13Zm-.5 2.003V15v.01-.008Z"
/>
</svg>
</button>
<button
aria-label="Zitieren"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.719 4.34c.094-.642-.366-1.236-1.028-1.328-.663-.092-1.276.354-1.371.996l-.808 5.478c-.094.642.366 1.237 1.028 1.328.663.092 1.276-.354 1.371-.996l.808-5.478Zm12.115 10.174c.095-.642-.366-1.237-1.028-1.328-.662-.092-1.276.354-1.37.996l-.809 5.478c-.094.642.366 1.236 1.028 1.328.663.092 1.277-.354 1.371-.996l.808-5.478ZM9.318 3.009c.665.077 1.138.662 1.058 1.306l-.022.175a220.467 220.467 0 0 1-.266 2.006c-.161 1.171-.368 2.579-.535 3.386-.13.636-.769 1.049-1.425.921-.656-.127-1.082-.745-.95-1.381.148-.72.345-2.052.509-3.237a190.652 190.652 0 0 0 .262-1.981l.021-.17c.08-.644.684-1.103 1.348-1.025Zm13.17 11.505c.094-.642-.366-1.237-1.028-1.328-.663-.092-1.276.354-1.371.996l-.808 5.478c-.094.642.366 1.236 1.028 1.328.663.092 1.276-.354 1.371-.996l.808-5.478Z"
/>
</svg>
</button>
<button
aria-label="Code"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.958 5.62a1 1 0 0 0-1.916-.574l-4 13.333a1 1 0 0 0 1.916.575l4-13.333ZM5.974 7.232a1 1 0 0 0-1.409.128l-3.333 4a1 1 0 0 0 0 1.28l3.333 4a1 1 0 1 0 1.537-1.28L3.302 12l2.8-3.36a1 1 0 0 0-.128-1.408Zm12.052 0a1 1 0 0 1 1.409.128l3.333 4a1 1 0 0 1 0 1.28l-3.333 4a1 1 0 1 1-1.537-1.28l2.8-3.36-2.8-3.36a1 1 0 0 1 .128-1.408Z"
/>
</svg>
</button>
<button
aria-label="Quelltextblock"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m8.825 12 1.475-1.475c.2-.2.3-.433.3-.7 0-.267-.1-.5-.3-.7-.2-.2-.438-.3-.713-.3-.274 0-.512.1-.712.3L6.7 11.3c-.1.1-.17.208-.213.325a1.107 1.107 0 0 0-.062.375c0 .133.02.258.063.375a.877.877 0 0 0 .212.325l2.175 2.175c.2.2.438.3.713.3.275 0 .512-.1.712-.3.2-.2.3-.433.3-.7 0-.267-.1-.5-.3-.7L8.825 12Zm6.35 0L13.7 13.475c-.2.2-.3.433-.3.7 0 .267.1.5.3.7.2.2.438.3.713.3.274 0 .512-.1.712-.3L17.3 12.7c.1-.1.17-.208.212-.325.042-.117.063-.242.063-.375s-.02-.258-.063-.375a.877.877 0 0 0-.212-.325l-2.175-2.175a.999.999 0 0 0-1.425 0c-.2.2-.3.433-.3.7 0 .267.1.5.3.7L15.175 12ZM5 21c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 3 19V5c0-.55.196-1.02.587-1.413A1.926 1.926 0 0 1 5 3h14c.55 0 1.02.196 1.413.587.39.393.587.863.587 1.413v14c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 19 21H5Zm0-2h14V5H5v14Z"
/>
</svg>
</button>
<button
aria-label="Link"
class="mx_AccessibleButton mx_FormattingButtons_Button mx_FormattingButtons_Button_hover"
role="button"
tabindex="0"
>
<svg
class="mx_FormattingButtons_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
</button>
</div>
</DocumentFragment>
`;