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
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
Loading…
Add table
Add a link
Reference in a new issue