From 177d861a2a2d33390bcef35df5461bd3d447a30e Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Mon, 20 Mar 2023 14:19:37 +0000 Subject: [PATCH 01/50] create autocomplete component --- .../components/WysiwygAutocomplete.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx new file mode 100644 index 00000000000..520fc810aeb --- /dev/null +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -0,0 +1,109 @@ +/* +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, useRef } 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 { + suggestion: MappedSuggestion | null; + handleMention: FormattingFunctions["mention"]; +} + +// Helper function that takes the rust suggestion and builds the query for the +// Autocomplete. This will change as we implement / commands. +// Returns an empty string if we don't want to show the suggestion menu. +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}`; +} + +// Helper function to get the mention text for a room +function getRoomMentionText(completion: ICompletion, client: MatrixClient): string { + const alias = completion.completion; + const roomId = completion.completionId; + + let roomForAutocomplete: Room | undefined; + if (roomId || alias[0] !== "#") { + roomForAutocomplete = client.getRoom(roomId || alias) ?? undefined; + } else { + roomForAutocomplete = client.getRooms().find((r) => { + return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias); + }); + } + + return roomForAutocomplete?.name || alias; +} + +const WysiwygAutocomplete = forwardRef( + ({ suggestion, handleMention }: WysiwygAutocompleteProps, ref: ForwardedRef): JSX.Element | null => { + const { room } = useRoomContext(); + const client = useMatrixClientContext(); + + const autocompleteIndexRef = useRef(0); + + 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; + } + case "command": + // TODO implement the command functionality + console.log("/command functionality not yet in place"); + break; + default: + break; + } + } + + function handleSelectionChange(completionIndex: number): void { + autocompleteIndexRef.current = completionIndex; + } + + return room ? ( +
+ +
+ ) : null; + }, +); + +WysiwygAutocomplete.displayName = "WysiwygAutocomplete"; + +export { WysiwygAutocomplete }; From a52aa382a05f465d640b6753488f1ef32a8ac8d8 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Mon, 20 Mar 2023 14:20:10 +0000 Subject: [PATCH 02/50] use autocomplete component and add click handlers --- .../components/WysiwygComposer.tsx | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 42a375143b1..61d4dca8951 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -14,15 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { memo, MutableRefObject, ReactNode, useEffect } from "react"; +import React, { memo, MutableRefObject, ReactNode, useEffect, useRef } 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; @@ -47,9 +53,20 @@ export const WysiwygComposer = memo(function WysiwygComposer({ rightComponent, children, }: WysiwygComposerProps) { - const inputEventProcessor = useInputEventProcessor(onSend, initialContent); + const { room } = useRoomContext(); + const autocompleteRef = useRef(null); - const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor }); + 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); useEffect(() => { if (!disabled && content !== null) { @@ -57,11 +74,28 @@ export const WysiwygComposer = memo(function WysiwygComposer({ } }, [onChange, content, disabled]); - const isReady = isWysiwygReady && !disabled; - useSetCursorPosition(!isReady, ref); + useEffect(() => { + function handleClick(e): void { + e.preventDefault(); - const { isFocused, onFocus } = useIsFocused(); - const computedPlaceholder = (!content && placeholder) || undefined; + const { target } = e; + + if (target.getAttribute("data-mention-type") === "user") { + console.log("here"); + const parsedLink = parsePermalink(target.href); + if (room && parsedLink?.userId) + defaultDispatcher.dispatch({ + action: Action.ViewUser, + member: room.getMember(parsedLink.userId), + }); + } + } + + const mentions = ref.current?.querySelectorAll("a[data-mention-type]") || []; + mentions.forEach((mention) => mention.addEventListener("click", handleClick)); + + return () => mentions.forEach((mention) => mention.removeEventListener("click", handleClick)); + }, [ref, room, content]); return (
+ Date: Mon, 20 Mar 2023 14:20:24 +0000 Subject: [PATCH 03/50] handle focus when autocomplete open --- .../hooks/useInputEventProcessor.ts | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index def7d74bc01..cf3e7d6be15 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -32,9 +32,11 @@ import { useMatrixClientContext } from "../../../../../contexts/MatrixClientCont import { isCaretAtEnd, isCaretAtStart } from "../utils/selection"; import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/event"; import { endEditing } from "../utils/editing"; +import Autocomplete from "../../Autocomplete"; export function useInputEventProcessor( onSend: () => void, + autocompleteRef: React.RefObject, initialContent?: string, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { const roomContext = useRoomContext(); @@ -51,6 +53,10 @@ 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(); }; @@ -65,12 +71,13 @@ export function useInputEventProcessor( roomContext, composerContext, mxClient, + autocompleteRef, ); } else { return handleInputEvent(event, send, isCtrlEnterToSend); } }, - [isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient], + [isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient, autocompleteRef], ); } @@ -85,12 +92,51 @@ 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(); From 05cc8a3443066f013b277acfbe5c26b9036880e8 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Mon, 20 Mar 2023 14:20:39 +0000 Subject: [PATCH 04/50] add placeholder for pill styling --- res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index a2c66202a4e..849fd3c5d3d 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -100,6 +100,11 @@ 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 { From be01b92b5e64308b43cb1ac8c420d33c61e80f44 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Mon, 20 Mar 2023 14:20:53 +0000 Subject: [PATCH 05/50] make width of autocomplete sensible --- .../views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss index 2eee815c3fc..382674f36e7 100644 --- a/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss +++ b/res/css/views/rooms/wysiwyg_composer/_SendWysiwygComposer.pcss @@ -84,3 +84,10 @@ limitations under the License. border-color: $quaternary-content; } } + +.mx_SendWysiwygComposer_AutoCompleteWrapper { + position: relative; + > .mx_Autocomplete { + min-width: 100%; + } +} From 7ba881d87d8c18150c6ac53233077c7f071155e3 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Mon, 20 Mar 2023 14:29:09 +0000 Subject: [PATCH 06/50] fix TS issue --- .../components/WysiwygComposer.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 61d4dca8951..50c9d013476 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -75,14 +75,14 @@ export const WysiwygComposer = memo(function WysiwygComposer({ }, [onChange, content, disabled]); useEffect(() => { - function handleClick(e): void { + function handleClick(e: Event): void { e.preventDefault(); - - const { target } = e; - - if (target.getAttribute("data-mention-type") === "user") { - console.log("here"); - const parsedLink = parsePermalink(target.href); + 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, From 03c65097ef478c5fe6a1c91274949b6e0b2e0f72 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Tue, 21 Mar 2023 12:40:07 +0000 Subject: [PATCH 07/50] bump package json to new version of wysiwyg --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15a5e65e12f..27cf5ea2079 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.1.1", + "@matrix-org/matrix-wysiwyg": "^1.4.0", "@matrix-org/react-sdk-module-api": "^0.0.4", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", From cbc3723d01b91288ca1540fcada50b94ed834499 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Tue, 21 Mar 2023 12:45:42 +0000 Subject: [PATCH 08/50] yarn --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index ca1ba5485b3..dd1991a173c 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.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/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/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" From 6b34a8049026856e34be7588f27e96f12ba829a4 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Tue, 21 Mar 2023 12:49:18 +0000 Subject: [PATCH 09/50] get composer tests passing --- .../wysiwyg_composer/components/WysiwygComposer-test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3ab7d768d64..a3962500cdc 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 { createTestClient, flushPromises, mockPlatformPeg } from "../../../../../test-utils"; +import { createTestClient, flushPromises, mockPlatformPeg, stubClient } from "../../../../../test-utils"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../src/dispatcher/actions"; @@ -245,6 +245,8 @@ describe("WysiwygComposer", () => { roomContext = defaultRoomContext, ) => { const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + stubClient(); + customRender(client, roomContext, editorState); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); return { textbox: screen.getByRole("textbox"), spyDispatcher }; From a4e1cdf591c87c57fac255ff1582487a723b3d2a Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Tue, 21 Mar 2023 13:27:13 +0000 Subject: [PATCH 10/50] rough setup of new autocomplete tests --- .../components/WysiwygAutocomplete-test.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx new file mode 100644 index 00000000000..8c1ba7afd16 --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -0,0 +1,61 @@ +/* +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 from "react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +// import userEvent from "@testing-library/user-event"; + +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../../../../src/contexts/RoomContext"; +import { WysiwygAutocomplete } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete"; +import { createMocks } from "../utils"; +import { stubClient } from "../../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; + +describe("WysiwygAutocomplete", () => { + const renderComponent = (props = {}) => { + const { mockClient, defaultRoomContext } = createMocks(); + + return render( + + + + + , + ); + }; + + beforeEach(() => { + stubClient(); + MatrixClientPeg.get(); + }); + + it("does not show anything when room is undefined", () => { + renderComponent(); + expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); + }); + + it("does something when we have a valid suggestion", () => { + renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } }); + screen.debug(); + }); +}); From 1c34cdc504834f94b9ef1f6091712acbb1845d80 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Tue, 21 Mar 2023 17:05:22 +0000 Subject: [PATCH 11/50] WIP cypress tests --- cypress/e2e/composer/composer.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index 48467ac6564..dee6f61830b 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -185,5 +185,9 @@ describe("Composer", () => { cy.get(".mx_EventTile_body a").should("have.attr", "href").and("include", "https://matrix.org/"); }); }); + + // describe.only("mentions", () => { + // it("does something", () => {}); + // }); }); }); From ff12d9286ba006dbe783cd1267eabda9fadba36d Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 10:20:07 +0000 Subject: [PATCH 12/50] add test id --- .../rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 520fc810aeb..98c531544f4 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -90,7 +90,7 @@ const WysiwygAutocomplete = forwardRef( } return room ? ( -
+
Date: Wed, 22 Mar 2023 10:20:24 +0000 Subject: [PATCH 13/50] tidy up test setup --- .../components/WysiwygAutocomplete-test.tsx | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 8c1ba7afd16..4363f9b3a57 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -15,47 +15,99 @@ limitations under the License. */ import "@testing-library/jest-dom"; -import React from "react"; -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import React, { createRef } from "react"; +import { render, screen } from "@testing-library/react"; // import userEvent from "@testing-library/user-event"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../../../../src/contexts/RoomContext"; import { WysiwygAutocomplete } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete"; -import { createMocks } from "../utils"; -import { stubClient } from "../../../../../test-utils"; -import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; +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("hello"), + renderCompletions: jest.fn().mockReturnValue(mockCompletion.map((c) => c.component)), + } as unknown as AutocompleteProvider); describe("WysiwygAutocomplete", () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + const autocompleteRef = createRef(); + const AutocompleterSpy = jest + .spyOn(Autocompleter.prototype, "getCompletions") + .mockResolvedValue([ + { completions: mockCompletion, provider: constructMockProvider(mockCompletion), command: {} }, + ]); + const renderComponent = (props = {}) => { - const { mockClient, defaultRoomContext } = createMocks(); + const mockClient = stubClient(); + const mockRoom = mkStubRoom("test_room", "test_room", mockClient); + const mockRoomContext = getRoomContext(mockRoom, {}); return render( - + , ); }; - beforeEach(() => { - stubClient(); - MatrixClientPeg.get(); + it("does not show any suggestions when room is undefined", () => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); }); - it("does not show anything when room is undefined", () => { + it("does not show any suggestions with a null suggestion prop", () => { + // default in renderComponent is a null suggestion renderComponent(); - expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); + expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); }); - it("does something when we have a valid suggestion", () => { + it.only("calls Autocompleter when given a valid suggestion prop", () => { + // default in renderComponent is a null suggestion renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } }); + expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); + jest.runAllTimers(); + console.log(autocompleteRef); + screen.debug(); }); }); From 2ad0eabd30b735b194d3a9eeff6d60499cdfcb7c Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 10:57:48 +0000 Subject: [PATCH 14/50] progress on getting something rendering --- .../components/WysiwygAutocomplete-test.tsx | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 4363f9b3a57..7c3091c96f8 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -16,7 +16,7 @@ limitations under the License. import "@testing-library/jest-dom"; import React, { createRef } from "react"; -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; // import userEvent from "@testing-library/user-event"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; @@ -47,19 +47,29 @@ const constructMockProvider = (data: ICompletion[]) => ({ getCompletions: jest.fn().mockImplementation(async () => data), getName: jest.fn().mockReturnValue("hello"), - renderCompletions: jest.fn().mockReturnValue(mockCompletion.map((c) => c.component)), + renderCompletions: jest.fn().mockImplementation((...args) => { + mockCompletion.map((c) => c.component); + }), } as unknown as AutocompleteProvider); describe("WysiwygAutocomplete", () => { - beforeAll(() => jest.useFakeTimers()); + beforeAll(() => { + // scrollTo not implemented in JSDOM + window.HTMLElement.prototype.scrollTo = function () {}; + jest.useFakeTimers(); + }); afterAll(() => jest.useRealTimers()); const autocompleteRef = createRef(); - const AutocompleterSpy = jest - .spyOn(Autocompleter.prototype, "getCompletions") - .mockResolvedValue([ - { completions: mockCompletion, provider: constructMockProvider(mockCompletion), command: {} }, + const AutocompleterSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockImplementation((...args) => { + return Promise.resolve([ + { + completions: mockCompletion, + provider: constructMockProvider(mockCompletion), + command: { command: "truthy" }, + }, ]); + }); const renderComponent = (props = {}) => { const mockClient = stubClient(); @@ -101,12 +111,15 @@ describe("WysiwygAutocomplete", () => { expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); }); - it.only("calls Autocompleter when given a valid suggestion prop", () => { + it.only("calls Autocompleter when given a valid suggestion prop", async () => { // default in renderComponent is a null suggestion renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } }); expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); jest.runAllTimers(); - console.log(autocompleteRef); + + await waitFor(() => { + expect(autocompleteRef.current?.state.completions).not.toEqual([]); + }); screen.debug(); }); From 191f58f4d523b0b7c4f620701dce42d17eca03c4 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 10:59:57 +0000 Subject: [PATCH 15/50] get the list item components rendering --- .../components/WysiwygAutocomplete-test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 7c3091c96f8..d3b72ecf942 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -46,10 +46,8 @@ const mockCompletion: ICompletion[] = [ const constructMockProvider = (data: ICompletion[]) => ({ getCompletions: jest.fn().mockImplementation(async () => data), - getName: jest.fn().mockReturnValue("hello"), - renderCompletions: jest.fn().mockImplementation((...args) => { - mockCompletion.map((c) => c.component); - }), + getName: jest.fn().mockReturnValue("test provider"), + renderCompletions: jest.fn().mockImplementation((components) => components), } as unknown as AutocompleteProvider); describe("WysiwygAutocomplete", () => { From 37b204342c1fd68d7b6e6cb8413964c4877a145c Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 11:25:44 +0000 Subject: [PATCH 16/50] make tests work as expected --- .../components/WysiwygAutocomplete-test.tsx | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index d3b72ecf942..d6d4ea3aad7 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -54,12 +54,18 @@ describe("WysiwygAutocomplete", () => { beforeAll(() => { // scrollTo not implemented in JSDOM window.HTMLElement.prototype.scrollTo = function () {}; - jest.useFakeTimers(); }); - afterAll(() => jest.useRealTimers()); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); const autocompleteRef = createRef(); - const AutocompleterSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockImplementation((...args) => { + const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockImplementation((...args) => { return Promise.resolve([ { completions: mockCompletion, @@ -90,8 +96,8 @@ describe("WysiwygAutocomplete", () => { ); }; - it("does not show any suggestions when room is undefined", () => { - const { container } = render( + it("does not show the autocomplete when room is undefined", () => { + render( { }} />, ); - expect(container).toBeEmptyDOMElement(); + expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument(); }); - it("does not show any suggestions with a null suggestion prop", () => { - // default in renderComponent is a null suggestion + it("does not call for suggestions with a null suggestion prop", async () => { + // render the component, the default props have suggestion = null renderComponent(); - expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); + + // check that getCompletions is not called, and we have no suggestions + expect(getCompletionsSpy).not.toHaveBeenCalled(); + expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); }); - it.only("calls Autocompleter when given a valid suggestion prop", async () => { + it("calls getCompletions when given a valid suggestion prop", async () => { // default in renderComponent is a null suggestion renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } }); - expect(screen.getByTestId("autocomplete-wrapper")).toBeEmptyDOMElement(); - jest.runAllTimers(); + // wait for getCompletions to have been called await waitFor(() => { - expect(autocompleteRef.current?.state.completions).not.toEqual([]); + expect(getCompletionsSpy).toHaveBeenCalled(); }); - screen.debug(); + // check that some suggestions are shown + expect(screen.queryByRole("presentation")).toBeInTheDocument(); + + // and that they are the mock completions + mockCompletion.forEach(({ completion }) => expect(screen.getByText(completion)).toBeInTheDocument()); }); }); From 74c3a23f4f16f365d3b98e5cd13e36773ea4ac1f Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 13:10:00 +0000 Subject: [PATCH 17/50] fix TS error --- .../components/WysiwygAutocomplete-test.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index d6d4ea3aad7..dc9ae5e47a8 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -65,15 +65,13 @@ describe("WysiwygAutocomplete", () => { }); const autocompleteRef = createRef(); - const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockImplementation((...args) => { - return Promise.resolve([ - { - completions: mockCompletion, - provider: constructMockProvider(mockCompletion), - command: { command: "truthy" }, - }, - ]); - }); + 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 renderComponent = (props = {}) => { const mockClient = stubClient(); From 013c22ec303acece7080859ddcc46af868f79211 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 13:22:53 +0000 Subject: [PATCH 18/50] remove unused cypress test --- cypress/e2e/composer/composer.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index dee6f61830b..48467ac6564 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -185,9 +185,5 @@ describe("Composer", () => { cy.get(".mx_EventTile_body a").should("have.attr", "href").and("include", "https://matrix.org/"); }); }); - - // describe.only("mentions", () => { - // it("does something", () => {}); - // }); }); }); From ab0e84bf8fa6d3ad6c3c1e5a2ced1a86fad1d120 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 14:03:11 +0000 Subject: [PATCH 19/50] remove commented import --- .../wysiwyg_composer/components/WysiwygAutocomplete-test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index dc9ae5e47a8..be3b2ea50da 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -17,7 +17,6 @@ limitations under the License. import "@testing-library/jest-dom"; import React, { createRef } from "react"; import { render, screen, waitFor } from "@testing-library/react"; -// import userEvent from "@testing-library/user-event"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../../../../src/contexts/RoomContext"; From 99a98f1537658c96f12b8b853ecacf177ddd5140 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 14:03:33 +0000 Subject: [PATCH 20/50] wrap wysiwyg composer tests in contexts --- .../components/WysiwygComposer-test.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 a3962500cdc..2f9a509863f 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -39,8 +39,18 @@ import { parseEditorStateTransfer } from "../../../../../../src/components/views describe("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { + const { mockClient, defaultRoomContext } = createMocks(); return render( - , + + + {" "} + + , ); }; From 14cd94725bafd637e248a461935889c9f4b19f11 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 14:27:37 +0000 Subject: [PATCH 21/50] add required autocomplete mocks for composer tests --- .../components/WysiwygAutocomplete-test.tsx | 1 + .../components/WysiwygComposer-test.tsx | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index be3b2ea50da..7b79a4503f6 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -42,6 +42,7 @@ const mockCompletion: ICompletion[] = [ component:
user_2
, }, ]; + const constructMockProvider = (data: ICompletion[]) => ({ getCompletions: jest.fn().mockImplementation(async () => data), 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 2f9a509863f..b4f01912837 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -36,10 +36,45 @@ 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"; + +// needed for the autocomplete +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("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { - const { mockClient, defaultRoomContext } = createMocks(); + const mockClient = stubClient(); + const { defaultRoomContext } = createMocks(); + 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 + }, + ]); return render( From 8fe63907821aadbf4fa90949c0a34d0e916649bd Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 14:32:26 +0000 Subject: [PATCH 22/50] fix type issue --- .../views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 50c9d013476..798c36de457 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -91,7 +91,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ } } - const mentions = ref.current?.querySelectorAll("a[data-mention-type]") || []; + const mentions = ref.current?.querySelectorAll("a[data-mention-type]"); mentions.forEach((mention) => mention.addEventListener("click", handleClick)); return () => mentions.forEach((mention) => mention.removeEventListener("click", handleClick)); From a97d57c7f0cf5dec8387193fbf0ef7a330b36958 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 14:53:33 +0000 Subject: [PATCH 23/50] fix TS issue --- .../rooms/wysiwyg_composer/components/WysiwygComposer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 798c36de457..ca42ba22ec4 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -92,9 +92,13 @@ export const WysiwygComposer = memo(function WysiwygComposer({ } const mentions = ref.current?.querySelectorAll("a[data-mention-type]"); - mentions.forEach((mention) => mention.addEventListener("click", handleClick)); + if (mentions) { + mentions.forEach((mention) => mention.addEventListener("click", handleClick)); + } - return () => mentions.forEach((mention) => mention.removeEventListener("click", handleClick)); + return () => { + if (mentions) mentions.forEach((mention) => mention.removeEventListener("click", handleClick)); + }; }, [ref, room, content]); return ( From 9d4b33089b4b9b7ce3dc34ad5422af3e35f8eb03 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 16:41:10 +0000 Subject: [PATCH 24/50] fix console error in test run --- .../components/WysiwygComposer-test.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) 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 b4f01912837..7cebd1e5299 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -68,13 +68,7 @@ describe("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { const mockClient = stubClient(); const { defaultRoomContext } = createMocks(); - 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 - }, - ]); + return render( @@ -89,16 +83,26 @@ describe("WysiwygComposer", () => { ); }; + beforeEach(() => { + 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 + }, + ]); + }); + afterEach(() => { jest.resetAllMocks(); }); - it("Should have contentEditable at false when disabled", () => { + it("Should have contentEditable at false when disabled", async () => { // When customRender(jest.fn(), jest.fn(), true); // Then - expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false"); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "false")); }); describe("Standard behavior", () => { From cf9456c6d8b5eed278f6df71ee5bd42b2e65c3d9 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 17:21:33 +0000 Subject: [PATCH 25/50] fix errors in send wysiwyg composer --- .../wysiwyg_composer/SendWysiwygComposer-test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 4dcf8d504ee..c6c74aa458f 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -22,7 +22,7 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { flushPromises } from "../../../../test-utils"; +import { flushPromises, stubClient } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/"; import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu"; import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; @@ -44,7 +44,8 @@ describe("SendWysiwygComposer", () => { jest.resetAllMocks(); }); - const { defaultRoomContext, mockClient } = createMocks(); + const mockClient = stubClient(); + const { defaultRoomContext } = createMocks(); const registerId = defaultDispatcher.register((payload) => { switch (payload.action) { @@ -93,15 +94,16 @@ describe("SendWysiwygComposer", () => { customRender(jest.fn(), jest.fn(), false, true); // Then - await waitFor(() => expect(screen.getByTestId("WysiwygComposer")).toBeTruthy()); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + expect(screen.getByTestId("WysiwygComposer")).toBeInTheDocument(); }); - it("Should render PlainTextComposer when isRichTextEnabled is at false", () => { + it("Should render PlainTextComposer when isRichTextEnabled is at false", async () => { // When customRender(jest.fn(), jest.fn(), false, false); // Then - expect(screen.getByTestId("PlainTextComposer")).toBeTruthy(); + expect(await screen.findByTestId("PlainTextComposer")).toBeInTheDocument(); }); describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( From 9c2e71aa489e4355ca9a8d74aba448ffe601ae12 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 17:23:01 +0000 Subject: [PATCH 26/50] fix edit wysiwyg composer test --- .../rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 0098859ea22..00671a2e579 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -22,7 +22,7 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { flushPromises, mkEvent } from "../../../../test-utils"; +import { flushPromises, mkEvent, stubClient } from "../../../../test-utils"; import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji"; @@ -38,7 +38,8 @@ describe("EditWysiwygComposer", () => { jest.resetAllMocks(); }); - const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks(); + const mockClient = stubClient(); + const { editorStateTransfer, defaultRoomContext, mockEvent } = createMocks(); const customRender = ( disabled = false, From d3bf2f5d61f861c4500f5d44dc54606d2e81c67e Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 22 Mar 2023 18:02:03 +0000 Subject: [PATCH 27/50] fix send wysiwyg composer test --- .../views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index c6c74aa458f..fd13e6b9283 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -94,8 +94,7 @@ describe("SendWysiwygComposer", () => { customRender(jest.fn(), jest.fn(), false, true); // Then - await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); - expect(screen.getByTestId("WysiwygComposer")).toBeInTheDocument(); + await waitFor(() => expect(screen.getByTestId("WysiwygComposer")).toBeInTheDocument()); }); it("Should render PlainTextComposer when isRichTextEnabled is at false", async () => { From 84576bc48d3b8c0e6178c90b137d320ebe66392e Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:00:45 +0000 Subject: [PATCH 28/50] improve autocomplete tests --- .../components/WysiwygAutocomplete-test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 7b79a4503f6..8b6d822a18a 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -77,6 +77,7 @@ describe("WysiwygAutocomplete", () => { const mockClient = stubClient(); const mockRoom = mkStubRoom("test_room", "test_room", mockClient); const mockRoomContext = getRoomContext(mockRoom, {}); + const mockHandleMention = jest.fn(); return render( @@ -84,9 +85,7 @@ describe("WysiwygAutocomplete", () => { @@ -126,7 +125,7 @@ describe("WysiwygAutocomplete", () => { }); // check that some suggestions are shown - expect(screen.queryByRole("presentation")).toBeInTheDocument(); + expect(screen.getByRole("presentation")).toBeInTheDocument(); // and that they are the mock completions mockCompletion.forEach(({ completion }) => expect(screen.getByText(completion)).toBeInTheDocument()); From 26dfec7eda22e3ce5a222adec1c96209b84df5d7 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:00:54 +0000 Subject: [PATCH 29/50] expand composer tests --- .../components/WysiwygComposer-test.tsx | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) 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 7cebd1e5299..e5a598f33cf 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -43,6 +43,7 @@ import AutocompleteProvider from "../../../../../../src/autocomplete/Autocomplet const mockCompletion: ICompletion[] = [ { type: "user", + href: "www.user1.com", completion: "user_1", completionId: "@user_1:host.local", range: { start: 1, end: 1 }, @@ -50,11 +51,20 @@ const mockCompletion: ICompletion[] = [ }, { 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_3", + completionId: "@user_3:host.local", + range: { start: 1, end: 1 }, + component:
user_3
, + }, ]; const constructMockProvider = (data: ICompletion[]) => @@ -193,6 +203,85 @@ describe("WysiwygComposer", () => { }); }); + describe.only("Mentions", () => { + beforeEach(async () => { + customRender(); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + }); + + it("shows the autocomplete when text has @ prefix and autoselects the first item", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + // the autocomplete suggestions container has the presentation role + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + expect(screen.getByText(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "true"); + }); + + it("pressing up and down arrows allows us to change the autocomplete selection", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + // the autocomplete will open with the first item selected + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + // so 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(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "false"); + expect(screen.getByText(mockCompletion[1].completion)).toHaveAttribute("aria-selected", "true"); + + // reverse the process and check again + await userEvent.keyboard("{ArrowUp}"); + expect(screen.getByText(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "true"); + expect(screen.getByText(mockCompletion[1].completion)).toHaveAttribute("aria-selected", "false"); + }); + + it("pressing enter selects the mention and inserts it into the composer as a link", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + // 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: mockCompletion[0].completion })).toBeInTheDocument(); + }); + + it("selecting a mention without a href closes the autocomplete but does not insert a mention", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + // select the third user + await userEvent.keyboard("{ArrowDown}{ArrowDown}{Enter}"); + + // 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: mockCompletion[2].completion })).not.toBeInTheDocument(); + }); + }); + describe("When settings require Ctrl+Enter to send", () => { const onChange = jest.fn(); const onSend = jest.fn(); From 036a04bbfb0a8a7990aac29c689b66935ba0dea4 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:22:01 +0000 Subject: [PATCH 30/50] comment out todo code for coverage --- .../wysiwyg_composer/components/WysiwygAutocomplete.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 98c531544f4..7ddf868dac3 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -76,10 +76,10 @@ const WysiwygAutocomplete = forwardRef( handleMention(completion.href, getRoomMentionText(completion, client)); break; } - case "command": - // TODO implement the command functionality - console.log("/command functionality not yet in place"); - break; + // TODO implement the command functionality + // case "command": + // console.log("/command functionality not yet in place"); + // break; default: break; } From 75ecc15c34a9bfa39a11430c6265fa96606a4c2d Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:22:18 +0000 Subject: [PATCH 31/50] improve autocomplete test code --- .../components/WysiwygAutocomplete-test.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 8b6d822a18a..971fbfc81f7 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -94,15 +94,7 @@ describe("WysiwygAutocomplete", () => { }; it("does not show the autocomplete when room is undefined", () => { - render( - , - ); + render(); expect(screen.queryByTestId("autocomplete-wrapper")).not.toBeInTheDocument(); }); From a48f87b41afbca3d41a3f1c786929fb1a519387f Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:22:36 +0000 Subject: [PATCH 32/50] add some room autocompletion tests for composer --- .../components/WysiwygComposer-test.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 e5a598f33cf..7b5a4bcefba 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -65,6 +65,21 @@ const mockCompletion: ICompletion[] = [ range: { start: 1, end: 1 }, component:
user_3
, }, + { + type: "room", + href: "www.room1.com", + completion: "room1", + completionId: "@room_1:host.local", + range: { start: 1, end: 1 }, + component:
room_1
, + }, + { + type: "room", + href: "www.room2.com", + completion: "#room2", + range: { start: 1, end: 1 }, + component:
room_2
, + }, ]; const constructMockProvider = (data: ICompletion[]) => @@ -280,6 +295,49 @@ describe("WysiwygComposer", () => { // check that it has not inserted a link expect(screen.queryByRole("link", { name: mockCompletion[2].completion })).not.toBeInTheDocument(); }); + + it("clicking a room mention with a completionId uses client.getRoom", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + // select the room suggestion + await userEvent.click(screen.getByText("room_1")); + + // 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("clicking a room mention without a completionId uses client.getRooms", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + screen.debug(); + // select the room suggestion + await userEvent.click(screen.getByText("room_2")); + + // check that it closes the autocomplete + await waitFor(() => { + expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); + }); + + // check that it has inserted a link and tried to find the room + // but it won't find the room, so falls back to the completion + expect(screen.getByRole("link", { name: "#room2" })).toBeInTheDocument(); + }); }); describe("When settings require Ctrl+Enter to send", () => { From 587ea1a115d7b2b39745469011cadd3a43759c39 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:27:46 +0000 Subject: [PATCH 33/50] tidy up tests --- .../wysiwyg_composer/components/WysiwygAutocomplete-test.tsx | 2 +- .../rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 971fbfc81f7..58c974c58bc 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -72,12 +72,12 @@ describe("WysiwygAutocomplete", () => { 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, {}); - const mockHandleMention = jest.fn(); return render( 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 7b5a4bcefba..00d88c50169 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -218,7 +218,7 @@ describe("WysiwygComposer", () => { }); }); - describe.only("Mentions", () => { + describe("Mentions", () => { beforeEach(async () => { customRender(); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); From 6c4d8930a2d0f8ba2234c7b902edd595e0db8e24 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 11:50:00 +0000 Subject: [PATCH 34/50] add clicking on user link test --- .../components/WysiwygComposer-test.tsx | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) 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 00d88c50169..a802d46c4a5 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -38,6 +38,8 @@ import { setSelection } from "../../../../../../src/components/views/rooms/wysiw 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"; // needed for the autocomplete const mockCompletion: ICompletion[] = [ @@ -116,6 +118,10 @@ describe("WysiwygComposer", () => { 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); }); afterEach(() => { @@ -219,10 +225,16 @@ describe("WysiwygComposer", () => { }); describe("Mentions", () => { + let dispatchSpy: jest.SpyInstance; + beforeEach(async () => { + dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); customRender(); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); }); + afterEach(() => { + jest.resetAllMocks(); + }); it("shows the autocomplete when text has @ prefix and autoselects the first item", async () => { fireEvent.input(screen.getByRole("textbox"), { @@ -276,6 +288,35 @@ describe("WysiwygComposer", () => { expect(screen.getByRole("link", { name: mockCompletion[0].completion })).toBeInTheDocument(); }); + it("clicking on a mention in the composer dispatches the correct action", async () => { + fireEvent.input(screen.getByRole("textbox"), { + data: "@abc", + inputType: "insertText", + }); + + expect(await screen.findByRole("presentation")).toBeInTheDocument(); + + // 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: mockCompletion[0].completion })); + expect(dispatchSpy).toHaveBeenCalledTimes(1); + + // this relies on the output from the mock room, which maybe we could tidy up + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.ViewUser, + member: expect.objectContaining({ + userId: "@member:domain.bla", + }), + }); + }); + it("selecting a mention without a href closes the autocomplete but does not insert a mention", async () => { fireEvent.input(screen.getByRole("textbox"), { data: "@abc", @@ -284,7 +325,7 @@ describe("WysiwygComposer", () => { expect(await screen.findByRole("presentation")).toBeInTheDocument(); - // select the third user + // select the third user using the keyboard await userEvent.keyboard("{ArrowDown}{ArrowDown}{Enter}"); // check that it closes the autocomplete @@ -296,7 +337,7 @@ describe("WysiwygComposer", () => { expect(screen.queryByRole("link", { name: mockCompletion[2].completion })).not.toBeInTheDocument(); }); - it("clicking a room mention with a completionId uses client.getRoom", async () => { + it("selecting a room mention with a completionId uses client.getRoom", async () => { fireEvent.input(screen.getByRole("textbox"), { data: "@abc", inputType: "insertText", @@ -304,7 +345,7 @@ describe("WysiwygComposer", () => { expect(await screen.findByRole("presentation")).toBeInTheDocument(); - // select the room suggestion + // select the room suggestion by clicking await userEvent.click(screen.getByText("room_1")); // check that it closes the autocomplete @@ -317,7 +358,7 @@ describe("WysiwygComposer", () => { expect(screen.getByRole("link", { name: "My room" })).toBeInTheDocument(); }); - it("clicking a room mention without a completionId uses client.getRooms", async () => { + it("selecting a room mention without a completionId uses client.getRooms", async () => { fireEvent.input(screen.getByRole("textbox"), { data: "@abc", inputType: "insertText", @@ -325,7 +366,6 @@ describe("WysiwygComposer", () => { expect(await screen.findByRole("presentation")).toBeInTheDocument(); - screen.debug(); // select the room suggestion await userEvent.click(screen.getByText("room_2")); From d47234b15d1d32ceeabb70740eeb8a0ffaa8eb3f Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 13:22:38 +0000 Subject: [PATCH 35/50] use stubClient, not createTestClient --- test/components/views/rooms/wysiwyg_composer/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/utils.ts b/test/components/views/rooms/wysiwyg_composer/utils.ts index 0eb99b251db..82b2fd537dd 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 { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { getRoomContext, mkEvent, mkStubRoom, stubClient } 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 = createTestClient(); + const mockClient = stubClient(); const mockEvent = mkEvent({ type: "m.room.message", room: "myfakeroom", From 78100d907dbe18d5d4ed494560738fb336223f14 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 13:23:00 +0000 Subject: [PATCH 36/50] consolidate mentions test and setup in single describe --- .../components/WysiwygComposer-test.tsx | 221 ++++++++---------- 1 file changed, 95 insertions(+), 126 deletions(-) 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 a802d46c4a5..549b85ac9fa 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 { createTestClient, flushPromises, mockPlatformPeg, stubClient } from "../../../../../test-utils"; +import { flushPromises, mockPlatformPeg, stubClient } from "../../../../../test-utils"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../src/dispatcher/actions"; @@ -41,61 +41,9 @@ import AutocompleteProvider from "../../../../../../src/autocomplete/Autocomplet import * as Permalinks from "../../../../../../src/utils/permalinks/Permalinks"; import { PermalinkParts } from "../../../../../../src/utils/permalinks/PermalinkConstructor"; -// needed for the autocomplete -const mockCompletion: 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_3", - completionId: "@user_3:host.local", - range: { start: 1, end: 1 }, - component:
user_3
, - }, - { - type: "room", - href: "www.room1.com", - completion: "room1", - completionId: "@room_1:host.local", - range: { start: 1, end: 1 }, - component:
room_1
, - }, - { - type: "room", - href: "www.room2.com", - completion: "#room2", - range: { start: 1, end: 1 }, - component:
room_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("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { - const mockClient = stubClient(); - const { defaultRoomContext } = createMocks(); - + const { mockClient, defaultRoomContext } = createMocks(); return render( @@ -104,26 +52,12 @@ describe("WysiwygComposer", () => { onSend={onSend} disabled={disabled} initialContent={initialContent} - />{" "} + /> , ); }; - beforeEach(() => { - 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 - }, - ]); - - jest.spyOn(Permalinks, "parsePermalink").mockReturnValue({ - userId: "mockParsedUserId", - } as unknown as PermalinkParts); - }); - afterEach(() => { jest.resetAllMocks(); }); @@ -225,56 +159,112 @@ describe("WysiwygComposer", () => { }); describe("Mentions", () => { - let dispatchSpy: jest.SpyInstance; + 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_3", + completionId: "@user_3:host.local", + range: { start: 1, end: 1 }, + component:
user_3
, + }, + { + type: "room", + href: "www.room1.com", + completion: "room1", + completionId: "@room_1:host.local", + range: { start: 1, end: 1 }, + component:
room_1
, + }, + { + type: "room", + href: "www.room2.com", + completion: "#room2", + range: { start: 1, end: 1 }, + component:
room_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); + + // 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 () => { - dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); + // 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.resetAllMocks(); + jest.clearAllMocks(); }); it("shows the autocomplete when text has @ prefix and autoselects the first item", async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - // the autocomplete suggestions container has the presentation role - expect(await screen.findByRole("presentation")).toBeInTheDocument(); - expect(screen.getByText(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "true"); + 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 () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - // the autocomplete will open with the first item selected - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // so 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(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "false"); - expect(screen.getByText(mockCompletion[1].completion)).toHaveAttribute("aria-selected", "true"); + 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(mockCompletion[0].completion)).toHaveAttribute("aria-selected", "true"); - expect(screen.getByText(mockCompletion[1].completion)).toHaveAttribute("aria-selected", "false"); + 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 () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // press enter await userEvent.keyboard("{Enter}"); @@ -285,16 +275,11 @@ describe("WysiwygComposer", () => { }); // check that it inserts the completion text as a link - expect(screen.getByRole("link", { name: mockCompletion[0].completion })).toBeInTheDocument(); + expect(screen.getByRole("link", { name: mockCompletions[0].completion })).toBeInTheDocument(); }); it("clicking on a mention in the composer dispatches the correct action", async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // press enter await userEvent.keyboard("{Enter}"); @@ -305,7 +290,7 @@ describe("WysiwygComposer", () => { }); // click on the user mention link that has been inserted - await userEvent.click(screen.getByRole("link", { name: mockCompletion[0].completion })); + await userEvent.click(screen.getByRole("link", { name: mockCompletions[0].completion })); expect(dispatchSpy).toHaveBeenCalledTimes(1); // this relies on the output from the mock room, which maybe we could tidy up @@ -318,12 +303,7 @@ describe("WysiwygComposer", () => { }); it("selecting a mention without a href closes the autocomplete but does not insert a mention", async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // select the third user using the keyboard await userEvent.keyboard("{ArrowDown}{ArrowDown}{Enter}"); @@ -334,16 +314,11 @@ describe("WysiwygComposer", () => { }); // check that it has not inserted a link - expect(screen.queryByRole("link", { name: mockCompletion[2].completion })).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: mockCompletions[2].completion })).not.toBeInTheDocument(); }); it("selecting a room mention with a completionId uses client.getRoom", async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // select the room suggestion by clicking await userEvent.click(screen.getByText("room_1")); @@ -359,12 +334,7 @@ describe("WysiwygComposer", () => { }); it("selecting a room mention without a completionId uses client.getRooms", async () => { - fireEvent.input(screen.getByRole("textbox"), { - data: "@abc", - inputType: "insertText", - }); - - expect(await screen.findByRole("presentation")).toBeInTheDocument(); + await insertMentionInput(); // select the room suggestion await userEvent.click(screen.getByText("room_2")); @@ -477,11 +447,10 @@ describe("WysiwygComposer", () => { const setup = async ( editorState?: EditorStateTransfer, - client = createTestClient(), + client = stubClient(), roomContext = defaultRoomContext, ) => { const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); - stubClient(); customRender(client, roomContext, editorState); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); From 7457b4e4dd803d35ecf451142aecab86250b5c5e Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 14:30:40 +0000 Subject: [PATCH 37/50] revert unneeded changes --- .../rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx | 5 ++--- .../rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 00671a2e579..0098859ea22 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -22,7 +22,7 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { flushPromises, mkEvent, stubClient } from "../../../../test-utils"; +import { flushPromises, mkEvent } from "../../../../test-utils"; import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji"; @@ -38,8 +38,7 @@ describe("EditWysiwygComposer", () => { jest.resetAllMocks(); }); - const mockClient = stubClient(); - const { editorStateTransfer, defaultRoomContext, mockEvent } = createMocks(); + const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks(); const customRender = ( disabled = false, diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index fd13e6b9283..f379fdce682 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -22,7 +22,7 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { flushPromises, stubClient } from "../../../../test-utils"; +import { flushPromises } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/"; import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu"; import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; @@ -44,8 +44,7 @@ describe("SendWysiwygComposer", () => { jest.resetAllMocks(); }); - const mockClient = stubClient(); - const { defaultRoomContext } = createMocks(); + const { defaultRoomContext, mockClient } = createMocks(); const registerId = defaultDispatcher.register((payload) => { switch (payload.action) { From a834963b4e3a0ec5fb4d3d43561e38b12cea2009 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 14:31:39 +0000 Subject: [PATCH 38/50] improve consistency --- .../views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index f379fdce682..5c4fd1941ac 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -93,7 +93,7 @@ describe("SendWysiwygComposer", () => { customRender(jest.fn(), jest.fn(), false, true); // Then - await waitFor(() => expect(screen.getByTestId("WysiwygComposer")).toBeInTheDocument()); + expect(await screen.findByTestId("WysiwygComposer")).toBeInTheDocument(); }); it("Should render PlainTextComposer when isRichTextEnabled is at false", async () => { From 0b1e510cda29117d1d17e829be6b21d0df8d439a Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 14:33:12 +0000 Subject: [PATCH 39/50] remove unneccesary hook --- .../wysiwyg_composer/components/WysiwygAutocomplete-test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 58c974c58bc..45012976683 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -60,10 +60,6 @@ describe("WysiwygAutocomplete", () => { jest.restoreAllMocks(); }); - beforeEach(() => { - jest.clearAllMocks(); - }); - const autocompleteRef = createRef(); const getCompletionsSpy = jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([ { From cde55ab18dd321ea684a8335c6d8e310343e73e2 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 14:35:50 +0000 Subject: [PATCH 40/50] remove comment --- .../wysiwyg_composer/components/WysiwygAutocomplete-test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 45012976683..e4de34c2691 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -104,7 +104,6 @@ describe("WysiwygAutocomplete", () => { }); it("calls getCompletions when given a valid suggestion prop", async () => { - // default in renderComponent is a null suggestion renderComponent({ suggestion: { keyChar: "@", text: "abc", type: "mention" } }); // wait for getCompletions to have been called From 57d4db5a432493e751a7b153710a17693ed820f6 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 14:58:20 +0000 Subject: [PATCH 41/50] improve assertion --- .../components/WysiwygComposer-test.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 549b85ac9fa..791df19676c 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 } from "../../../../../test-utils"; +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"; @@ -251,7 +251,7 @@ describe("WysiwygComposer", () => { it("pressing up and down arrows allows us to change the autocomplete selection", async () => { await insertMentionInput(); - // so press the down arrow - nb using .keyboard allows us to not have to specify a node, which + // 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"); @@ -293,13 +293,15 @@ describe("WysiwygComposer", () => { await userEvent.click(screen.getByRole("link", { name: mockCompletions[0].completion })); expect(dispatchSpy).toHaveBeenCalledTimes(1); - // this relies on the output from the mock room, which maybe we could tidy up - expect(dispatchSpy).toHaveBeenCalledWith({ - action: Action.ViewUser, - member: expect.objectContaining({ - userId: "@member:domain.bla", + // 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 () => { From 21cc283386990ac1d176b49632eabe8d14ded834 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 15:09:27 +0000 Subject: [PATCH 42/50] improve test variables for legibility --- .../components/WysiwygComposer-test.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) 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 791df19676c..3f9694e2a3c 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -181,25 +181,25 @@ describe("WysiwygComposer", () => { { // no href user type: "user", - completion: "user_3", + completion: "user_without_href", completionId: "@user_3:host.local", range: { start: 1, end: 1 }, - component:
user_3
, + component:
user_without_href
, }, { type: "room", href: "www.room1.com", - completion: "room1", + completion: "#room_with_completion_id", completionId: "@room_1:host.local", range: { start: 1, end: 1 }, - component:
room_1
, + component:
room_with_completion_id
, }, { type: "room", href: "www.room2.com", - completion: "#room2", + completion: "#room_without_completion_id", range: { start: 1, end: 1 }, - component:
room_2
, + component:
room_without_completion_id
, }, ]; @@ -307,8 +307,8 @@ describe("WysiwygComposer", () => { it("selecting a mention without a href closes the autocomplete but does not insert a mention", async () => { await insertMentionInput(); - // select the third user using the keyboard - await userEvent.keyboard("{ArrowDown}{ArrowDown}{Enter}"); + // select the relevant user by clicking + await userEvent.click(screen.getByText("user_without_href")); // check that it closes the autocomplete await waitFor(() => { @@ -316,14 +316,14 @@ describe("WysiwygComposer", () => { }); // check that it has not inserted a link - expect(screen.queryByRole("link", { name: mockCompletions[2].completion })).not.toBeInTheDocument(); + 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_1")); + await userEvent.click(screen.getByText("room_with_completion_id")); // check that it closes the autocomplete await waitFor(() => { @@ -339,16 +339,15 @@ describe("WysiwygComposer", () => { await insertMentionInput(); // select the room suggestion - await userEvent.click(screen.getByText("room_2")); + 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 tried to find the room - // but it won't find the room, so falls back to the completion - expect(screen.getByRole("link", { name: "#room2" })).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(); }); }); From ec61bef6e929f1fa086d0c495333a922552100b8 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Thu, 23 Mar 2023 15:16:59 +0000 Subject: [PATCH 43/50] improve comment --- .../wysiwyg_composer/components/WysiwygAutocomplete.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 7ddf868dac3..205e9b88c49 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -41,7 +41,10 @@ function buildQuery(suggestion: MappedSuggestion | null): string { return `${suggestion.keyChar}${suggestion.text}`; } -// Helper function to get the mention text for a room +// Helper function to get the mention text for a room as this is less straightforward +// than it is for determining the text we display for a user. +// TODO determine if it's worth bringing the switch case into this function to make it +// into a more general `getMentionText` component function getRoomMentionText(completion: ICompletion, client: MatrixClient): string { const alias = completion.completion; const roomId = completion.completionId; From 9b964d1db4114eb21e383ee0192fe09a9f610185 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 24 Mar 2023 13:28:54 +0000 Subject: [PATCH 44/50] remove code from autocomplete --- .../wysiwyg_composer/components/WysiwygAutocomplete.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 205e9b88c49..2182f7aed85 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ForwardedRef, forwardRef, useRef } from "react"; +import React, { ForwardedRef, forwardRef } from "react"; import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg"; @@ -66,8 +66,6 @@ const WysiwygAutocomplete = forwardRef( const { room } = useRoomContext(); const client = useMatrixClientContext(); - const autocompleteIndexRef = useRef(0); - function handleConfirm(completion: ICompletion): void { if (!completion.href) return; @@ -88,17 +86,12 @@ const WysiwygAutocomplete = forwardRef( } } - function handleSelectionChange(completionIndex: number): void { - autocompleteIndexRef.current = completionIndex; - } - return room ? (
From c277cbef5f830fd05b06c78ce0654ffd50a83d0d Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 24 Mar 2023 13:38:42 +0000 Subject: [PATCH 45/50] Translate comments to TSDoc --- .../components/WysiwygAutocomplete.tsx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 2182f7aed85..6403fd7174f 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -28,9 +28,14 @@ interface WysiwygAutocompleteProps { handleMention: FormattingFunctions["mention"]; } -// Helper function that takes the rust suggestion and builds the query for the -// Autocomplete. This will change as we implement / commands. -// Returns an empty string if we don't want to show the suggestion menu. +/** + * 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 @@ -41,10 +46,15 @@ function buildQuery(suggestion: MappedSuggestion | null): string { return `${suggestion.keyChar}${suggestion.text}`; } -// Helper function to get the mention text for a room as this is less straightforward -// than it is for determining the text we display for a user. -// TODO determine if it's worth bringing the switch case into this function to make it -// into a more general `getMentionText` component +/** + * 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 alias = completion.completion; const roomId = completion.completionId; From 108b3d89f11237f21a831aae64c0fc451c0ac42f Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 24 Mar 2023 13:52:28 +0000 Subject: [PATCH 46/50] split if logic up and add comments --- .../components/WysiwygAutocomplete.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 6403fd7174f..cc1800986d4 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -56,18 +56,23 @@ function buildQuery(suggestion: MappedSuggestion | null): string { * @returns the text to display in the mention */ function getRoomMentionText(completion: ICompletion, client: MatrixClient): string { - const alias = completion.completion; const roomId = completion.completionId; + const alias = completion.completion; + + let roomForAutocomplete: Room | null | undefined; - let roomForAutocomplete: Room | undefined; - if (roomId || alias[0] !== "#") { - roomForAutocomplete = client.getRoom(roomId || alias) ?? undefined; + // try to find the room name for the mention text by roomId then alias + 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); }); } + // but if we haven't managed to find the room, use the alias as a fallback return roomForAutocomplete?.name || alias; } From e2522d565647e4dea469952cb6c50a4e71c624df Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 24 Mar 2023 14:04:31 +0000 Subject: [PATCH 47/50] add more TSDoc --- .../components/WysiwygAutocomplete.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index cc1800986d4..2c8b4973184 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -24,7 +24,16 @@ 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"]; } @@ -76,6 +85,13 @@ function getRoomMentionText(completion: ICompletion, client: MatrixClient): stri 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(); From b5a5af91e2660c2751c5391fb1082ab5bf7a4f34 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 24 Mar 2023 16:11:49 +0000 Subject: [PATCH 48/50] update comment --- .../wysiwyg_composer/components/WysiwygAutocomplete.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 2c8b4973184..b6051d2a73e 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -70,7 +70,8 @@ function getRoomMentionText(completion: ICompletion, client: MatrixClient): stri let roomForAutocomplete: Room | null | undefined; - // try to find the room name for the mention text by roomId then alias + // 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("#")) { @@ -81,7 +82,7 @@ function getRoomMentionText(completion: ICompletion, client: MatrixClient): stri }); } - // but if we haven't managed to find the room, use the alias as a fallback + // if we haven't managed to find the room, use the alias as a fallback return roomForAutocomplete?.name || alias; } From 71796f5062aa4bb7245e538f6d4f3bc37e2f4556 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Wed, 29 Mar 2023 12:09:21 +0100 Subject: [PATCH 49/50] bump rich text editor to 1.4.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 51c99b4f9a9..7d22bc5f8b2 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.4.1", "@matrix-org/react-sdk-module-api": "^0.0.4", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/yarn.lock b/yarn.lock index 794aa45b8c8..8bb210a09f9 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.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== -"@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.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-1.4.1.tgz#80c392f036dc4d6ef08a91c4964a68682e977079" + integrity sha512-B8sxY3pE2XyRyQ1g7cx0YjGaDZ1A0Uh5XxS/lNdxQ/0ctRJj6IBy7KtiUjxDRdA15ioZnf6aoJBRkBSr02qhaw== "@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" From df8c3d8df5846738eb29714228326126f551cd72 Mon Sep 17 00:00:00 2001 From: Alun Turner Date: Fri, 31 Mar 2023 13:50:52 +0100 Subject: [PATCH 50/50] fix TS error --- .../rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index b6051d2a73e..5ad7b078841 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -99,7 +99,7 @@ const WysiwygAutocomplete = forwardRef( const client = useMatrixClientContext(); function handleConfirm(completion: ICompletion): void { - if (!completion.href) return; + if (!completion.href || !client) return; switch (completion.type) { case "user":