mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-04 20:01:58 +00:00
Absorb the matrix-react-sdk repository (#28192)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Florian Duros <florian.duros@ormaz.fr> Co-authored-by: Kim Brose <kim.brose@nordeck.net> Co-authored-by: Florian Duros <florianduros@element.io> Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: dbkr <986903+dbkr@users.noreply.github.com> Co-authored-by: ElementRobot <releases@riot.im> Co-authored-by: dbkr <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: David Langley <davidl@element.io> Co-authored-by: Michael Weimann <michaelw@matrix.org> Co-authored-by: Timshel <Timshel@users.noreply.github.com> Co-authored-by: Sahil Silare <32628578+sahil9001@users.noreply.github.com> Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Hubert Chathi <hubert@uhoreg.ca> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
committed by
GitHub
parent
2b99496025
commit
c05c429803
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 { renderHook } from "@testing-library/react-hooks";
|
||||
import { act } from "jest-matrix-react";
|
||||
|
||||
import { usePlainTextListeners } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners";
|
||||
|
||||
describe("setContent", () => {
|
||||
it("calling with a string calls the onChange argument", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const { result } = renderHook(() => usePlainTextListeners("initialContent", mockOnChange));
|
||||
|
||||
const newContent = "new content";
|
||||
act(() => {
|
||||
result.current.setContent(newContent);
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(newContent);
|
||||
});
|
||||
|
||||
it("calling with no argument and no editor ref does not call onChange", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const { result } = renderHook(() => usePlainTextListeners("initialContent", mockOnChange));
|
||||
|
||||
act(() => {
|
||||
result.current.setContent();
|
||||
});
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calling with no argument and a valid editor ref calls onChange with the editorRef innerHTML", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
// create a div to represent the editor and append some content
|
||||
const mockEditor = document.createElement("div");
|
||||
const mockEditorText = "some text content";
|
||||
const textNode = document.createTextNode(mockEditorText);
|
||||
mockEditor.appendChild(textNode);
|
||||
|
||||
const { result } = renderHook(() => usePlainTextListeners("initialContent", mockOnChange));
|
||||
|
||||
// @ts-ignore in order to allow us to reassign the ref without complaint
|
||||
result.current.ref.current = mockEditor;
|
||||
|
||||
act(() => {
|
||||
result.current.setContent();
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(mockEditor.innerHTML);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Suggestion,
|
||||
findSuggestionInText,
|
||||
getMappedSuggestion,
|
||||
processCommand,
|
||||
processEmojiReplacement,
|
||||
processMention,
|
||||
processSelectionChange,
|
||||
} from "../../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion";
|
||||
|
||||
function createMockPlainTextSuggestionPattern(props: Partial<Suggestion> = {}): Suggestion {
|
||||
return {
|
||||
mappedSuggestion: { keyChar: "/", type: "command", text: "some text", ...props.mappedSuggestion },
|
||||
node: document.createTextNode(""),
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockCustomSuggestionPattern(props: Partial<Suggestion> = {}): Suggestion {
|
||||
return {
|
||||
mappedSuggestion: { keyChar: "", type: "custom", text: "🙂", ...props.mappedSuggestion },
|
||||
node: document.createTextNode(":)"),
|
||||
startOffset: 0,
|
||||
endOffset: 2,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
describe("processCommand", () => {
|
||||
it("does not change parent hook state if suggestion is null", () => {
|
||||
// create a mockSuggestion using the text node above
|
||||
const mockSetSuggestion = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
|
||||
// call the function with a null suggestion
|
||||
processCommand("should not be seen", null, mockSetSuggestion, mockSetText);
|
||||
|
||||
// check that the parent state setter has not been called
|
||||
expect(mockSetText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can change the parent hook state when required", () => {
|
||||
// create a div and append a text node to it with some initial text
|
||||
const editorDiv = document.createElement("div");
|
||||
const initialText = "text";
|
||||
const textNode = document.createTextNode(initialText);
|
||||
editorDiv.appendChild(textNode);
|
||||
|
||||
// create a mockSuggestion using the text node above
|
||||
const mockSuggestion = createMockPlainTextSuggestionPattern({ node: textNode });
|
||||
const mockSetSuggestion = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
const replacementText = "/replacement text";
|
||||
|
||||
processCommand(replacementText, mockSuggestion, mockSetSuggestion, mockSetText);
|
||||
|
||||
// check that the text has changed and includes a trailing space
|
||||
expect(mockSetText).toHaveBeenCalledWith(`${replacementText} `);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processEmojiReplacement", () => {
|
||||
it("does not change parent hook state if suggestion is null", () => {
|
||||
// create a mockSuggestion using the text node above
|
||||
const mockSetSuggestion = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
|
||||
// call the function with a null suggestion
|
||||
processEmojiReplacement(null, mockSetSuggestion, mockSetText);
|
||||
|
||||
// check that the parent state setter has not been called
|
||||
expect(mockSetText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can change the parent hook state when required", () => {
|
||||
// create a div and append a text node to it with some initial text
|
||||
const editorDiv = document.createElement("div");
|
||||
const initialText = ":)";
|
||||
const textNode = document.createTextNode(initialText);
|
||||
editorDiv.appendChild(textNode);
|
||||
|
||||
// create a mockSuggestion using the text node above
|
||||
const mockSuggestion = createMockCustomSuggestionPattern({ node: textNode });
|
||||
const mockSetSuggestion = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
const replacementText = "🙂";
|
||||
|
||||
processEmojiReplacement(mockSuggestion, mockSetSuggestion, mockSetText);
|
||||
|
||||
// check that the text has changed and includes a trailing space
|
||||
expect(mockSetText).toHaveBeenCalledWith(replacementText);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processMention", () => {
|
||||
// TODO refactor and expand tests when mentions become <a> tags
|
||||
it("returns early when suggestion is null", () => {
|
||||
const mockSetSuggestion = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
processMention("href", "displayName", new Map(), null, mockSetSuggestion, mockSetText);
|
||||
|
||||
expect(mockSetSuggestion).not.toHaveBeenCalled();
|
||||
expect(mockSetText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can insert a mention into a text node", () => {
|
||||
// make a text node and an editor div, set the cursor inside the text node and then
|
||||
// append node to editor, then editor to document
|
||||
const textNode = document.createTextNode("@a");
|
||||
const mockEditor = document.createElement("div");
|
||||
mockEditor.appendChild(textNode);
|
||||
document.body.appendChild(mockEditor);
|
||||
document.getSelection()?.setBaseAndExtent(textNode, 1, textNode, 1);
|
||||
|
||||
// call the util function
|
||||
const href = "href";
|
||||
const displayName = "displayName";
|
||||
const mockSetSuggestionData = jest.fn();
|
||||
const mockSetText = jest.fn();
|
||||
processMention(
|
||||
href,
|
||||
displayName,
|
||||
new Map([["style", "test"]]),
|
||||
{ node: textNode, startOffset: 0, endOffset: 2 } as unknown as Suggestion,
|
||||
mockSetSuggestionData,
|
||||
mockSetText,
|
||||
);
|
||||
|
||||
// check that the editor has a single child
|
||||
expect(mockEditor.children).toHaveLength(1);
|
||||
const linkElement = mockEditor.firstElementChild as HTMLElement;
|
||||
|
||||
// and that the child is an <a> tag with the expected attributes and content
|
||||
expect(linkElement).toBeInstanceOf(HTMLAnchorElement);
|
||||
expect(linkElement).toHaveAttribute(href, href);
|
||||
expect(linkElement).toHaveAttribute("contenteditable", "false");
|
||||
expect(linkElement).toHaveAttribute("style", "test");
|
||||
expect(linkElement.textContent).toBe(displayName);
|
||||
|
||||
expect(mockSetText).toHaveBeenCalledWith();
|
||||
expect(mockSetSuggestionData).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processSelectionChange", () => {
|
||||
function createMockEditorRef(element: HTMLDivElement | null = null): React.RefObject<HTMLDivElement> {
|
||||
return { current: element } as React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function appendEditorWithTextNodeContaining(initialText = ""): [HTMLDivElement, Node] {
|
||||
// create the elements/nodes
|
||||
const mockEditor = document.createElement("div");
|
||||
const textNode = document.createTextNode(initialText);
|
||||
|
||||
// append text node to the editor, editor to the document body
|
||||
mockEditor.appendChild(textNode);
|
||||
document.body.appendChild(mockEditor);
|
||||
|
||||
return [mockEditor, textNode];
|
||||
}
|
||||
|
||||
const mockSetSuggestion = jest.fn();
|
||||
beforeEach(() => {
|
||||
mockSetSuggestion.mockClear();
|
||||
});
|
||||
|
||||
it("returns early if current editorRef is null", () => {
|
||||
const mockEditorRef = createMockEditorRef(null);
|
||||
// we monitor for the call to document.createNodeIterator to indicate an early return
|
||||
const nodeIteratorSpy = jest.spyOn(document, "createNodeIterator");
|
||||
|
||||
processSelectionChange(mockEditorRef, jest.fn());
|
||||
expect(nodeIteratorSpy).not.toHaveBeenCalled();
|
||||
|
||||
// tidy up to avoid potential impacts on other tests
|
||||
nodeIteratorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("calls setSuggestion with null if selection is not a cursor", () => {
|
||||
const [mockEditor, textNode] = appendEditorWithTextNodeContaining("content");
|
||||
const mockEditorRef = createMockEditorRef(mockEditor);
|
||||
|
||||
// create a selection in the text node that has different start and end locations ie it
|
||||
// is not a cursor
|
||||
document.getSelection()?.setBaseAndExtent(textNode, 0, textNode, 4);
|
||||
|
||||
// process the selection and check that we do not attempt to set the suggestion
|
||||
processSelectionChange(mockEditorRef, mockSetSuggestion);
|
||||
expect(mockSetSuggestion).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("calls setSuggestion with null if selection cursor is not inside a text node", () => {
|
||||
const [mockEditor] = appendEditorWithTextNodeContaining("content");
|
||||
const mockEditorRef = createMockEditorRef(mockEditor);
|
||||
|
||||
// create a selection that points at the editor element, not the text node it contains
|
||||
document.getSelection()?.setBaseAndExtent(mockEditor, 0, mockEditor, 0);
|
||||
|
||||
// process the selection and check that we do not attempt to set the suggestion
|
||||
processSelectionChange(mockEditorRef, mockSetSuggestion);
|
||||
expect(mockSetSuggestion).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("calls setSuggestion with null if we have an existing suggestion but no command match", () => {
|
||||
const [mockEditor, textNode] = appendEditorWithTextNodeContaining("content");
|
||||
const mockEditorRef = createMockEditorRef(mockEditor);
|
||||
|
||||
// create a selection in the text node that has identical start and end locations, ie it is a cursor
|
||||
document.getSelection()?.setBaseAndExtent(textNode, 0, textNode, 0);
|
||||
|
||||
// the call to process the selection will have an existing suggestion in state due to the second
|
||||
// argument being non-null, expect that we clear this suggestion now that the text is not a command
|
||||
processSelectionChange(mockEditorRef, mockSetSuggestion);
|
||||
expect(mockSetSuggestion).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("calls setSuggestion with the expected arguments when text node is valid command", () => {
|
||||
const commandText = "/potentialCommand";
|
||||
const [mockEditor, textNode] = appendEditorWithTextNodeContaining(commandText);
|
||||
const mockEditorRef = createMockEditorRef(mockEditor);
|
||||
|
||||
// create a selection in the text node that has identical start and end locations, ie it is a cursor
|
||||
document.getSelection()?.setBaseAndExtent(textNode, 3, textNode, 3);
|
||||
|
||||
// process the change and check the suggestion that is set looks as we expect it to
|
||||
processSelectionChange(mockEditorRef, mockSetSuggestion);
|
||||
expect(mockSetSuggestion).toHaveBeenCalledWith({
|
||||
mappedSuggestion: {
|
||||
keyChar: "/",
|
||||
type: "command",
|
||||
text: "potentialCommand",
|
||||
},
|
||||
node: textNode,
|
||||
startOffset: 0,
|
||||
endOffset: commandText.length,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat a command outside the first text node to be a suggestion", () => {
|
||||
const [mockEditor] = appendEditorWithTextNodeContaining("some text in first node");
|
||||
const [, commandTextNode] = appendEditorWithTextNodeContaining("/potentialCommand");
|
||||
|
||||
const mockEditorRef = createMockEditorRef(mockEditor);
|
||||
|
||||
// create a selection in the text node that has identical start and end locations, ie it is a cursor
|
||||
document.getSelection()?.setBaseAndExtent(commandTextNode, 3, commandTextNode, 3);
|
||||
|
||||
// process the change and check the suggestion that is set looks as we expect it to
|
||||
processSelectionChange(mockEditorRef, mockSetSuggestion);
|
||||
expect(mockSetSuggestion).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findSuggestionInText", () => {
|
||||
const command = "/someCommand";
|
||||
const userMention = "@userMention";
|
||||
const roomMention = "#roomMention";
|
||||
|
||||
const mentionTestCases = [userMention, roomMention];
|
||||
const allTestCases = [command, userMention, roomMention];
|
||||
|
||||
it("returns null if content does not contain any mention or command characters", () => {
|
||||
expect(findSuggestionInText("hello", 1, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if content contains a command but is not the first text node", () => {
|
||||
expect(findSuggestionInText(command, 1, false)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if the offset is outside the content length", () => {
|
||||
expect(findSuggestionInText("hi", 30, true)).toBeNull();
|
||||
expect(findSuggestionInText("hi", -10, true)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(allTestCases)("returns an object when the whole input is special case: %s", (text) => {
|
||||
const expected = {
|
||||
mappedSuggestion: getMappedSuggestion(text),
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
};
|
||||
// test for cursor immediately before and after special character, before end, at end
|
||||
expect(findSuggestionInText(text, 0, true)).toEqual(expected);
|
||||
expect(findSuggestionInText(text, 1, true)).toEqual(expected);
|
||||
expect(findSuggestionInText(text, text.length - 2, true)).toEqual(expected);
|
||||
expect(findSuggestionInText(text, text.length, true)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns null when a command is followed by other text", () => {
|
||||
const followingText = " followed by something";
|
||||
|
||||
// check for cursor inside and outside the command
|
||||
expect(findSuggestionInText(command + followingText, command.length - 2, true)).toBeNull();
|
||||
expect(findSuggestionInText(command + followingText, command.length + 2, true)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(mentionTestCases)("returns an object when a %s is followed by other text", (mention) => {
|
||||
const followingText = " followed by something else";
|
||||
expect(findSuggestionInText(mention + followingText, mention.length - 2, true)).toEqual({
|
||||
mappedSuggestion: getMappedSuggestion(mention),
|
||||
startOffset: 0,
|
||||
endOffset: mention.length,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null if there is a command surrounded by text", () => {
|
||||
const precedingText = "text before the command ";
|
||||
const followingText = " text after the command";
|
||||
expect(
|
||||
findSuggestionInText(precedingText + command + followingText, precedingText.length + 4, true),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each(mentionTestCases)("returns an object if %s is surrounded by text", (mention) => {
|
||||
const precedingText = "I want to mention ";
|
||||
const followingText = " in my message";
|
||||
|
||||
const textInput = precedingText + mention + followingText;
|
||||
const expected = {
|
||||
mappedSuggestion: getMappedSuggestion(mention),
|
||||
startOffset: precedingText.length,
|
||||
endOffset: precedingText.length + mention.length,
|
||||
};
|
||||
|
||||
// when the cursor is immediately before the special character
|
||||
expect(findSuggestionInText(textInput, precedingText.length, true)).toEqual(expected);
|
||||
// when the cursor is inside the mention
|
||||
expect(findSuggestionInText(textInput, precedingText.length + 3, true)).toEqual(expected);
|
||||
// when the cursor is right at the end of the mention
|
||||
expect(findSuggestionInText(textInput, precedingText.length + mention.length, true)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns null for text content with an email address", () => {
|
||||
const emailInput = "send to user@test.com";
|
||||
expect(findSuggestionInText(emailInput, 15, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for double slashed command", () => {
|
||||
const doubleSlashCommand = "//not a command";
|
||||
expect(findSuggestionInText(doubleSlashCommand, 4, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for slash separated text", () => {
|
||||
const slashSeparatedInput = "please to this/that/the other";
|
||||
expect(findSuggestionInText(slashSeparatedInput, 21, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an object for a mention that contains punctuation", () => {
|
||||
const mentionWithPunctuation = "@userX14#5a_-";
|
||||
const precedingText = "mention ";
|
||||
const mentionInput = precedingText + mentionWithPunctuation;
|
||||
expect(findSuggestionInText(mentionInput, 12, true)).toEqual({
|
||||
mappedSuggestion: getMappedSuggestion(mentionWithPunctuation),
|
||||
startOffset: precedingText.length,
|
||||
endOffset: precedingText.length + mentionWithPunctuation.length,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when user inputs any whitespace after the special character", () => {
|
||||
const mentionWithSpaceAfter = "@ somebody";
|
||||
expect(findSuggestionInText(mentionWithSpaceAfter, 2, true)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an object for an emoji suggestion", () => {
|
||||
const emoiticon = ":)";
|
||||
const precedingText = "hello ";
|
||||
const mentionInput = precedingText + emoiticon;
|
||||
expect(findSuggestionInText(mentionInput, precedingText.length, true, true)).toEqual({
|
||||
mappedSuggestion: getMappedSuggestion(emoiticon, true),
|
||||
startOffset: precedingText.length,
|
||||
endOffset: precedingText.length + emoiticon.length,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMappedSuggestion", () => {
|
||||
it("returns null when the first character is not / # @", () => {
|
||||
expect(getMappedSuggestion("Zzz")).toBe(null);
|
||||
});
|
||||
|
||||
it("returns the expected mapped suggestion when first character is # or @", () => {
|
||||
expect(getMappedSuggestion("@user-mention")).toEqual({
|
||||
type: "mention",
|
||||
keyChar: "@",
|
||||
text: "user-mention",
|
||||
});
|
||||
expect(getMappedSuggestion("#room-mention")).toEqual({
|
||||
type: "mention",
|
||||
keyChar: "#",
|
||||
text: "room-mention",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the expected mapped suggestion when first character is /", () => {
|
||||
expect(getMappedSuggestion("/command")).toEqual({
|
||||
type: "command",
|
||||
keyChar: "/",
|
||||
text: "command",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the expected mapped suggestion when the text is a plain text emoiticon", () => {
|
||||
expect(getMappedSuggestion(":)", true)).toEqual({
|
||||
type: "custom",
|
||||
keyChar: "",
|
||||
text: "🙂",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
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 { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { TimelineRenderingType } from "../../../../../../../src/contexts/RoomContext";
|
||||
import { mkStubRoom, stubClient } from "../../../../../../test-utils";
|
||||
import ContentMessages from "../../../../../../../src/ContentMessages";
|
||||
import { IRoomState } from "../../../../../../../src/components/structures/RoomView";
|
||||
import {
|
||||
handleClipboardEvent,
|
||||
isEventToHandleAsClipboardEvent,
|
||||
} from "../../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/utils";
|
||||
|
||||
const mockClient = stubClient();
|
||||
const mockRoom = mkStubRoom("mock room", "mock room", mockClient);
|
||||
const mockRoomState = {
|
||||
room: mockRoom,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
replyToEvent: {} as unknown as MatrixEvent,
|
||||
} as unknown as IRoomState;
|
||||
|
||||
const sendContentListToRoomSpy = jest.spyOn(ContentMessages.sharedInstance(), "sendContentListToRoom");
|
||||
const sendContentToRoomSpy = jest.spyOn(ContentMessages.sharedInstance(), "sendContentToRoom");
|
||||
const fetchSpy = jest.spyOn(window, "fetch");
|
||||
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
describe("handleClipboardEvent", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createMockClipboardEvent(props: any): ClipboardEvent {
|
||||
return { clipboardData: { files: [], types: [] }, ...props } as ClipboardEvent;
|
||||
}
|
||||
|
||||
it("returns false if it is not a paste event", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "copy" });
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if clipboard data is null", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "paste", clipboardData: null });
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if room is undefined", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "paste" });
|
||||
const { room, ...roomStateWithoutRoom } = mockRoomState;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
roomStateWithoutRoom,
|
||||
mockClient,
|
||||
);
|
||||
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if room clipboardData files and types are empty", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: [], types: [] },
|
||||
});
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("handles event and calls sendContentListToRoom when data files are present", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
originalEvent.clipboardData?.files,
|
||||
mockRoom.roomId,
|
||||
undefined, // this is the event relation, an optional arg
|
||||
mockClient,
|
||||
mockRoomState.timelineRenderingType,
|
||||
);
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls sendContentListToRoom with eventRelation when present", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
originalEvent.clipboardData?.files,
|
||||
mockRoom.roomId,
|
||||
mockEventRelation, // this is the event relation, an optional arg
|
||||
mockClient,
|
||||
mockRoomState.timelineRenderingType,
|
||||
);
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls the error handler when sentContentListToRoom errors", async () => {
|
||||
const mockErrorMessage = "something went wrong";
|
||||
sendContentListToRoomSpy.mockRejectedValueOnce(new Error(mockErrorMessage));
|
||||
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
});
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls the error handler when data types has text/html but data can not be parsed", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: {
|
||||
files: [],
|
||||
types: ["text/html"],
|
||||
getData: jest.fn().mockReturnValue("<div>invalid html"),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith("Failed to handle pasted content as Safari inserted content");
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("calls fetch when data types has text/html and data can parsed", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: {
|
||||
files: [],
|
||||
types: ["text/html"],
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient, mockEventRelation);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(fetchSpy).toHaveBeenCalledWith("blob:");
|
||||
});
|
||||
|
||||
it("calls error handler when fetch fails", async () => {
|
||||
const mockErrorMessage = "fetch failed";
|
||||
fetchSpy.mockRejectedValueOnce(mockErrorMessage);
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: {
|
||||
files: [],
|
||||
types: ["text/html"],
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
});
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls sendContentToRoom when parsing is successful", async () => {
|
||||
fetchSpy.mockResolvedValueOnce({
|
||||
url: "test/file",
|
||||
blob: () => {
|
||||
return Promise.resolve({ type: "image/jpeg" } as Blob);
|
||||
},
|
||||
} as Response);
|
||||
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: {
|
||||
files: [],
|
||||
types: ["text/html"],
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendContentToRoomSpy).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
mockRoom.roomId,
|
||||
mockEventRelation,
|
||||
mockClient,
|
||||
mockRoomState.replyToEvent,
|
||||
);
|
||||
});
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls error handler when parsing is not successful", async () => {
|
||||
fetchSpy.mockResolvedValueOnce({
|
||||
url: "test/file",
|
||||
blob: () => {
|
||||
return Promise.resolve({ type: "image/jpeg" } as Blob);
|
||||
},
|
||||
} as Response);
|
||||
const mockErrorMessage = "sendContentToRoom failed";
|
||||
sendContentToRoomSpy.mockRejectedValueOnce(mockErrorMessage);
|
||||
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: {
|
||||
files: [],
|
||||
types: ["text/html"],
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
});
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEventToHandleAsClipboardEvent", () => {
|
||||
it("returns true for ClipboardEvent", () => {
|
||||
const input = new ClipboardEvent("clipboard");
|
||||
expect(isEventToHandleAsClipboardEvent(input)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for special case input", () => {
|
||||
const input = new InputEvent("insertFromPaste", { inputType: "insertFromPaste" });
|
||||
Object.assign(input, { dataTransfer: "not null" });
|
||||
expect(isEventToHandleAsClipboardEvent(input)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for regular InputEvent", () => {
|
||||
const input = new InputEvent("input");
|
||||
expect(isEventToHandleAsClipboardEvent(input)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for other input", () => {
|
||||
const input = new KeyboardEvent("keyboard");
|
||||
expect(isEventToHandleAsClipboardEvent(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user