diff --git a/package.json b/package.json index 27cf5ea207..15a5e65e12 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.5.0", - "@matrix-org/matrix-wysiwyg": "^1.4.0", + "@matrix-org/matrix-wysiwyg": "^1.1.1", "@matrix-org/react-sdk-module-api": "^0.0.4", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss index 382674f36e..2eee815c3f 100644 --- a/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss +++ b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss @@ -84,10 +84,3 @@ limitations under the License. border-color: $quaternary-content; } } - -.mx_SendWysiwygComposer_AutoCompleteWrapper { - position: relative; - > .mx_Autocomplete { - min-width: 100%; - } -} diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index 849fd3c5d3..a2c66202a4 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -100,11 +100,6 @@ limitations under the License. padding: unset; } } - - /* this selector represents what will become a pill */ - a[data-mention-type] { - cursor: text; - } } .mx_WysiwygComposer_Editor_content_placeholder::before { diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx deleted file mode 100644 index b6051d2a73..0000000000 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, { ForwardedRef, forwardRef } from "react"; -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; -import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; - -import { useRoomContext } from "../../../../../contexts/RoomContext"; -import Autocomplete from "../../Autocomplete"; -import { ICompletion } from "../../../../../autocomplete/Autocompleter"; -import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; - -interface WysiwygAutocompleteProps { - /** - * The suggestion output from the rust model is used to build the query that is - * passed to the `` component - */ - suggestion: MappedSuggestion | null; - - /** - * This handler will be called with the href and display text for a mention on clicking - * a mention in the autocomplete list or pressing enter on a selected item - */ - handleMention: FormattingFunctions["mention"]; -} - -/** - * Builds the query for the `` component from the rust suggestion. This - * will change as we implement handling / commands. - * - * @param suggestion - represents if the rust model is tracking a potential mention - * @returns an empty string if we can not generate a query, otherwise a query beginning - * with @ for a user query, # for a room or space query - */ -function buildQuery(suggestion: MappedSuggestion | null): string { - if (!suggestion || !suggestion.keyChar || suggestion.type === "command") { - // if we have an empty key character, we do not build a query - // TODO implement the command functionality - return ""; - } - - return `${suggestion.keyChar}${suggestion.text}`; -} - -/** - * Given a room type mention, determine the text that should be displayed in the mention - * TODO expand this function to more generally handle outputting the display text from a - * given completion - * - * @param completion - the item selected from the autocomplete, currently treated as a room completion - * @param client - the MatrixClient is required for us to look up the correct room mention text - * @returns the text to display in the mention - */ -function getRoomMentionText(completion: ICompletion, client: MatrixClient): string { - const roomId = completion.completionId; - const alias = completion.completion; - - let roomForAutocomplete: Room | null | undefined; - - // Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias - // that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now - if (roomId) { - roomForAutocomplete = client.getRoom(roomId); - } else if (!alias.startsWith("#")) { - roomForAutocomplete = client.getRoom(alias); - } else { - roomForAutocomplete = client.getRooms().find((r) => { - return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias); - }); - } - - // if we haven't managed to find the room, use the alias as a fallback - return roomForAutocomplete?.name || alias; -} - -/** - * Given the current suggestion from the rust model and a handler function, this component - * will display the legacy `` component (as used in the BasicMessageComposer) - * and call the handler function with the required arguments when a mention is selected - * - * @param props.ref - the ref will be attached to the rendered `` component - */ -const WysiwygAutocomplete = forwardRef( - ({ suggestion, handleMention }: WysiwygAutocompleteProps, ref: ForwardedRef): JSX.Element | null => { - const { room } = useRoomContext(); - const client = useMatrixClientContext(); - - function handleConfirm(completion: ICompletion): void { - if (!completion.href) return; - - switch (completion.type) { - case "user": - handleMention(completion.href, completion.completion); - break; - case "room": { - handleMention(completion.href, getRoomMentionText(completion, client)); - break; - } - // TODO implement the command functionality - // case "command": - // console.log("/command functionality not yet in place"); - // break; - default: - break; - } - } - - return room ? ( -
- -
- ) : null; - }, -); - -WysiwygAutocomplete.displayName = "WysiwygAutocomplete"; - -export { WysiwygAutocomplete }; diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index ca42ba22ec..42a375143b 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -14,21 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { memo, MutableRefObject, ReactNode, useEffect, useRef } from "react"; +import React, { memo, MutableRefObject, ReactNode, useEffect } from "react"; import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg"; import classNames from "classnames"; -import Autocomplete from "../../Autocomplete"; -import { WysiwygAutocomplete } from "./WysiwygAutocomplete"; import { FormattingButtons } from "./FormattingButtons"; import { Editor } from "./Editor"; import { useInputEventProcessor } from "../hooks/useInputEventProcessor"; import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { useIsFocused } from "../hooks/useIsFocused"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; -import defaultDispatcher from "../../../../../dispatcher/dispatcher"; -import { Action } from "../../../../../dispatcher/actions"; -import { parsePermalink } from "../../../../../utils/permalinks/Permalinks"; interface WysiwygComposerProps { disabled?: boolean; @@ -53,20 +47,9 @@ export const WysiwygComposer = memo(function WysiwygComposer({ rightComponent, children, }: WysiwygComposerProps) { - const { room } = useRoomContext(); - const autocompleteRef = useRef(null); + const inputEventProcessor = useInputEventProcessor(onSend, initialContent); - const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent); - const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion } = useWysiwyg({ - initialContent, - inputEventProcessor, - }); - const { isFocused, onFocus } = useIsFocused(); - - const isReady = isWysiwygReady && !disabled; - const computedPlaceholder = (!content && placeholder) || undefined; - - useSetCursorPosition(!isReady, ref); + const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor }); useEffect(() => { if (!disabled && content !== null) { @@ -74,32 +57,11 @@ export const WysiwygComposer = memo(function WysiwygComposer({ } }, [onChange, content, disabled]); - useEffect(() => { - function handleClick(e: Event): void { - e.preventDefault(); - if ( - e.target && - e.target instanceof HTMLAnchorElement && - e.target.getAttribute("data-mention-type") === "user" - ) { - const parsedLink = parsePermalink(e.target.href); - if (room && parsedLink?.userId) - defaultDispatcher.dispatch({ - action: Action.ViewUser, - member: room.getMember(parsedLink.userId), - }); - } - } + const isReady = isWysiwygReady && !disabled; + useSetCursorPosition(!isReady, ref); - const mentions = ref.current?.querySelectorAll("a[data-mention-type]"); - if (mentions) { - mentions.forEach((mention) => mention.addEventListener("click", handleClick)); - } - - return () => { - if (mentions) mentions.forEach((mention) => mention.removeEventListener("click", handleClick)); - }; - }, [ref, room, content]); + const { isFocused, onFocus } = useIsFocused(); + const computedPlaceholder = (!content && placeholder) || undefined; return (
- void, - autocompleteRef: React.RefObject, initialContent?: string, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { const roomContext = useRoomContext(); @@ -53,10 +51,6 @@ export function useInputEventProcessor( const send = (): void => { event.stopPropagation?.(); event.preventDefault?.(); - // do not send the message if we have the autocomplete open, regardless of settings - if (autocompleteRef?.current && !autocompleteRef.current.state.hide) { - return; - } onSend(); }; @@ -71,13 +65,12 @@ export function useInputEventProcessor( roomContext, composerContext, mxClient, - autocompleteRef, ); } else { return handleInputEvent(event, send, isCtrlEnterToSend); } }, - [isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient, autocompleteRef], + [isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient], ); } @@ -92,51 +85,12 @@ function handleKeyboardEvent( roomContext: IRoomState, composerContext: ComposerContextState, mxClient: MatrixClient, - autocompleteRef: React.RefObject, ): KeyboardEvent | null { const { editorStateTransfer } = composerContext; const isEditing = Boolean(editorStateTransfer); const isEditorModified = isEditing ? initialContent !== composer.content() : composer.content().length !== 0; const action = getKeyBindingsManager().getMessageComposerAction(event); - const autocompleteIsOpen = autocompleteRef?.current && !autocompleteRef.current.state.hide; - - // we need autocomplete to take priority when it is open for using enter to select - if (autocompleteIsOpen) { - let handled = false; - const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event); - const component = autocompleteRef.current; - if (component && component.countCompletions() > 0) { - switch (autocompleteAction) { - case KeyBindingAction.ForceCompleteAutocomplete: - case KeyBindingAction.CompleteAutocomplete: - autocompleteRef.current.onConfirmCompletion(); - handled = true; - break; - case KeyBindingAction.PrevSelectionInAutocomplete: - autocompleteRef.current.moveSelection(-1); - handled = true; - break; - case KeyBindingAction.NextSelectionInAutocomplete: - autocompleteRef.current.moveSelection(1); - handled = true; - break; - case KeyBindingAction.CancelAutocomplete: - autocompleteRef.current.onEscape(event as {} as React.KeyboardEvent); - handled = true; - break; - default: - break; // don't return anything, allow event to pass through - } - } - - if (handled) { - event.preventDefault(); - event.stopPropagation(); - return event; - } - } - switch (action) { case KeyBindingAction.SendMessage: send(); diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 5c4fd1941a..4dcf8d504e 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -93,15 +93,15 @@ describe("SendWysiwygComposer", () => { customRender(jest.fn(), jest.fn(), false, true); // Then - expect(await screen.findByTestId("WysiwygComposer")).toBeInTheDocument(); + await waitFor(() => expect(screen.getByTestId("WysiwygComposer")).toBeTruthy()); }); - it("Should render PlainTextComposer when isRichTextEnabled is at false", async () => { + it("Should render PlainTextComposer when isRichTextEnabled is at false", () => { // When customRender(jest.fn(), jest.fn(), false, false); // Then - expect(await screen.findByTestId("PlainTextComposer")).toBeInTheDocument(); + expect(screen.getByTestId("PlainTextComposer")).toBeTruthy(); }); describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx deleted file mode 100644 index e4de34c269..0000000000 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import "@testing-library/jest-dom"; -import React, { createRef } from "react"; -import { render, screen, waitFor } from "@testing-library/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:
user_1
, - }, - { - type: "user", - completion: "user_2", - completionId: "@user_2:host.local", - range: { start: 1, end: 1 }, - component:
user_2
, - }, -]; - -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(); - 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 renderComponent = (props = {}) => { - const mockClient = stubClient(); - const mockRoom = mkStubRoom("test_room", "test_room", mockClient); - const mockRoomContext = getRoomContext(mockRoom, {}); - - return render( - - - - - , - ); - }; - - it("does not show the autocomplete when room is undefined", () => { - render(); - 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()); - }); -}); diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index 3f9694e2a3..3ab7d768d6 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -21,7 +21,7 @@ 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 { createTestClient, flushPromises, mockPlatformPeg } from "../../../../../test-utils"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../src/dispatcher/actions"; @@ -36,25 +36,11 @@ 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( - - - - - , + , ); }; @@ -62,12 +48,12 @@ describe("WysiwygComposer", () => { jest.resetAllMocks(); }); - it("Should have contentEditable at false when disabled", async () => { + it("Should have contentEditable at false when disabled", () => { // When customRender(jest.fn(), jest.fn(), true); // Then - await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false")); + expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false"); }); describe("Standard behavior", () => { @@ -158,199 +144,6 @@ describe("WysiwygComposer", () => { }); }); - describe("Mentions", () => { - const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); - - const mockCompletions: ICompletion[] = [ - { - type: "user", - href: "www.user1.com", - completion: "user_1", - completionId: "@user_1:host.local", - range: { start: 1, end: 1 }, - component:
user_1
, - }, - { - type: "user", - href: "www.user2.com", - completion: "user_2", - completionId: "@user_2:host.local", - range: { start: 1, end: 1 }, - component:
user_2
, - }, - { - // no href user - type: "user", - completion: "user_without_href", - completionId: "@user_3:host.local", - range: { start: 1, end: 1 }, - component:
user_without_href
, - }, - { - type: "room", - href: "www.room1.com", - completion: "#room_with_completion_id", - completionId: "@room_1:host.local", - range: { start: 1, end: 1 }, - component:
room_with_completion_id
, - }, - { - type: "room", - href: "www.room2.com", - completion: "#room_without_completion_id", - range: { start: 1, end: 1 }, - component:
room_without_completion_id
, - }, - ]; - - 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 insertMentionInput = async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - 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}"); - - // 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("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(); - }); - }); - describe("When settings require Ctrl+Enter to send", () => { const onChange = jest.fn(); const onSend = jest.fn(); @@ -448,11 +241,10 @@ describe("WysiwygComposer", () => { const setup = async ( editorState?: EditorStateTransfer, - client = stubClient(), + client = createTestClient(), 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 }; diff --git a/test/components/views/rooms/wysiwyg_composer/utils.ts b/test/components/views/rooms/wysiwyg_composer/utils.ts index 82b2fd537d..0eb99b251d 100644 --- a/test/components/views/rooms/wysiwyg_composer/utils.ts +++ b/test/components/views/rooms/wysiwyg_composer/utils.ts @@ -16,12 +16,12 @@ limitations under the License. import { EventTimeline, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { getRoomContext, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; export function createMocks(eventContent = "Replying to this new content") { - const mockClient = stubClient(); + const mockClient = createTestClient(); const mockEvent = mkEvent({ type: "m.room.message", room: "myfakeroom", diff --git a/yarn.lock b/yarn.lock index dd1991a173..ca1ba5485b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1664,10 +1664,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== -"@matrix-org/matrix-wysiwyg@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-1.4.0.tgz#b04f4c05c8117c0917f9a1401bb1d9c5f976052c" - integrity sha512-NIxX1oia61zut/DA7fUCCQfOhWKLbVDmPrDeUeX40NgXZRROhLPF1/jcOKgAnXK8yqflmNrVlX/dlUVcfj/kqw== +"@matrix-org/matrix-wysiwyg@^1.1.1": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-1.3.0.tgz#647837be552c1a96ca8157e0dc0d7d8f897fcbe2" + integrity sha512-uHcPYP+mriJZcI54lDBpO+wPGDli/+VEL/NjuW+BBgt7PLViSa4xaGdD7K+yUBgntRdbJ/J4fo+lYB06kqF+sA== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14"