From fb751c3c7b7ae1e821eba5cde224cd874f85f616 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 26 Oct 2022 17:16:13 +0200 Subject: [PATCH] Add test for plain mode --- .../components/PlainTextComposer.tsx | 8 +- .../components/WysiwygComposer.tsx | 2 +- .../hooks/usePlainTextInitialization.ts | 3 - .../hooks/usePlainTextListeners.ts | 18 +- .../SendWysiwygComposer-test.tsx | 165 +++++++++++------- .../components/PlainTextComposer-test.tsx | 94 ++++++++++ .../components/WysiwygComposer-test.tsx | 30 +--- 7 files changed, 213 insertions(+), 107 deletions(-) create mode 100644 test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index 64c940c2fd..e15b5ef57f 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -43,7 +43,13 @@ export function PlainTextComposer({ usePlainTextInitialization(initialContent, ref); useSetCursorPosition(disabled, ref); - return
+ return
{ children?.(ref, composerFunctions) }
; diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 73125a910a..974e89f0ce 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -52,7 +52,7 @@ export const WysiwygComposer = memo(function WysiwygComposer( useSetCursorPosition(!isReady, ref); return ( -
+
{ children?.(ref, wysiwyg) } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts index dcaacd98ea..abf2a6a6d2 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts @@ -16,13 +16,10 @@ limitations under the License. import { RefObject, useEffect } from "react"; -import { setCursorPositionAtTheEnd } from "./utils"; - export function usePlainTextInitialization(initialContent: string, ref: RefObject) { useEffect(() => { if (ref.current) { ref.current.innerText = initialContent; - setCursorPositionAtTheEnd(ref.current); } }, [ref, initialContent]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 98d09c12b0..02063ddcfb 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -16,7 +16,7 @@ limitations under the License. import { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react"; -import { useInputEventProcessor } from "./useInputEventProcessor"; +import { useSettingValue } from "../../../../../hooks/useSettings"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; @@ -26,25 +26,25 @@ export function usePlainTextListeners(onChange: (content: string) => void, onSen const ref = useRef(); const send = useCallback((() => { if (ref.current) { - ref.current.innerText = ''; + ref.current.innerHTML = ''; } onSend(); }), [ref, onSend]); - const inputEventProcessor = useInputEventProcessor(send); - const onInput = useCallback((event: SyntheticEvent) => { if (isDivElement(event.target)) { - onChange(event.target.innerText); + onChange(event.target.innerHTML); } - inputEventProcessor(event.nativeEvent); - }, [onChange, inputEventProcessor]); + }, [onChange]); + const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onKeyDown = useCallback((event: KeyboardEvent) => { - if (event.key === 'Enter') { + if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) { + event.preventDefault(); + event.stopPropagation(); send(); } - }, [send]); + }, [isCtrlEnter, send]); return { ref, onInput, onPaste: onInput, onKeyDown }; } diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 573ae451be..c2e1b8a1fb 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -26,6 +26,8 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; +import * as useComposerFunctions + from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions"; const mockClear = jest.fn(); @@ -68,86 +70,119 @@ describe('SendWysiwygComposer', () => { const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); - const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => { + const customRender = ( + onChange = (_content: string) => void 0, + onSend = () => void 0, + disabled = false, + isRichTextEnabled = true) => { return render( - + , ); }; - it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => { - // Given we don't have focus - customRender(jest.fn(), jest.fn()); - screen.getByLabelText('Bold').focus(); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + it('Should render WysiwygComposer when isRichTextEnabled is at true', () => { + // When + customRender(jest.fn(), jest.fn(), false, true); - // When we send the right action - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - - // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + // Then + expect(screen.getByTestId('WysiwygComposer')).toBeTruthy(); }); - it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => { - // Given we don't have focus - customRender(jest.fn(), jest.fn()); - screen.getByLabelText('Bold').focus(); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + it('Should render PlainTextComposer when isRichTextEnabled is at false', () => { + // When + customRender(jest.fn(), jest.fn(), false, false); - // When we send the right action - defaultDispatcher.dispatch({ - action: Action.ClearAndFocusSendMessageComposer, - context: null, - }); - - // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); - expect(mockClear).toBeCalledTimes(1); + // Then + expect(screen.getByTestId('PlainTextComposer')).toBeTruthy(); }); - it('Should focus when receiving a reply_to_event action', async () => { - // Given we don't have focus - customRender(jest.fn(), jest.fn()); - screen.getByLabelText('Bold').focus(); - expect(screen.getByRole('textbox')).not.toHaveFocus(); + describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])( + 'Should focus when receiving an Action.FocusSendMessageComposer action', + ({ isRichTextEnabled }) => { + afterEach(() => { + jest.resetAllMocks(); + }); - // When we send the right action - defaultDispatcher.dispatch({ - action: "reply_to_event", - context: null, + it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => { + // Given we don't have focus + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + document.head.focus(); + // screen.getByLabelText('Bold').focus(); + // expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + }); + + it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => { + // Given we don't have focus + const mock = jest.spyOn(useComposerFunctions, 'useComposerFunctions'); + mock.mockReturnValue({ clear: mockClear }); + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + // screen.getByLabelText('Bold').focus(); + // expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: Action.ClearAndFocusSendMessageComposer, + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + expect(mockClear).toBeCalledTimes(1); + + mock.mockRestore(); + }); + + it('Should focus when receiving a reply_to_event action', async () => { + // Given we don't have focus + customRender(jest.fn(), jest.fn(), false, isRichTextEnabled); + // screen.getByLabelText('Bold').focus(); + // expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send the right action + defaultDispatcher.dispatch({ + action: "reply_to_event", + context: null, + }); + + // Then the component gets the focus + await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); + }); + + it('Should not focus when disabled', async () => { + // Given we don't have focus and we are disabled + customRender(jest.fn(), jest.fn(), true, isRichTextEnabled); + expect(screen.getByRole('textbox')).not.toHaveFocus(); + + // When we send an action that would cause us to get focus + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + // (Send a second event to exercise the clearTimeout logic) + defaultDispatcher.dispatch({ + action: Action.FocusSendMessageComposer, + context: null, + }); + + // Wait for event dispatch to happen + await new Promise((r) => setTimeout(r, 200)); + + // Then we don't get it because we are disabled + expect(screen.getByRole('textbox')).not.toHaveFocus(); + }); }); - - // Then the component gets the focus - await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus()); - }); - - it('Should not focus when disabled', async () => { - // Given we don't have focus and we are disabled - customRender(jest.fn(), jest.fn(), true); - expect(screen.getByRole('textbox')).not.toHaveFocus(); - - // When we send an action that would cause us to get focus - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - // (Send a second event to exercise the clearTimeout logic) - defaultDispatcher.dispatch({ - action: Action.FocusSendMessageComposer, - context: null, - }); - - // Wait for event dispatch to happen - await new Promise((r) => setTimeout(r, 200)); - - // Then we don't get it because we are disabled - expect(screen.getByRole('textbox')).not.toHaveFocus(); - }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx new file mode 100644 index 0000000000..5d1b03020c --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -0,0 +1,94 @@ +/* +Copyright 2022 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 from 'react'; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { PlainTextComposer } + from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer"; + +// Work around missing ClipboardEvent type +class MyClipboardEvent {} +window.ClipboardEvent = MyClipboardEvent as any; + +describe('PlainTextComposer', () => { + const customRender = ( + onChange = (_content: string) => void 0, + onSend = () => void 0, + disabled = false, + initialContent?: string) => { + return render( + , + ); + }; + + it('Should have contentEditable at false when disabled', () => { + // When + customRender(jest.fn(), jest.fn(), true); + + // Then + expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false"); + }); + + it('Should have focus', () => { + // When + customRender(jest.fn(), jest.fn(), false); + + // Then + expect(screen.getByRole('textbox')).toHaveFocus(); + }); + + it('Should call onChange handler', async () => { + // When + const content = 'content'; + const onChange = jest.fn(); + customRender(onChange, jest.fn()); + await userEvent.type(screen.getByRole('textbox'), content); + + // Then + expect(onChange).toBeCalledWith(content); + }); + + it('Should call onSend when Enter is pressed', async () => { + //When + const onSend = jest.fn(); + customRender(jest.fn(), onSend); + await userEvent.type(screen.getByRole('textbox'), '{enter}'); + + // Then it sends a message + expect(onSend).toBeCalledTimes(1); + }); + + it('Should clear textbox content when clear is called', async () => { + //When + let composer; + render( + + { (ref, composerFunctions) => { + composer = composerFunctions; + return null; + } } + , + ); + await userEvent.type(screen.getByRole('textbox'), 'content'); + expect(screen.getByRole('textbox').innerHTML).toBe('content'); + composer.clear(); + + // Then + expect(screen.getByRole('textbox').innerHTML).toBeFalsy(); + }); +}); 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 cb9c37071c..7e3db04abc 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -19,10 +19,6 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import { IRoomState } from "../../../../../../src/components/structures/RoomView"; -import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; @@ -54,36 +50,14 @@ jest.mock("@matrix-org/matrix-wysiwyg", () => ({ })); describe('WysiwygComposer', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - const mockClient = createTestClient(); - const mockEvent = mkEvent({ - type: "m.room.message", - room: 'myfakeroom', - user: 'myfakeuser', - content: { "msgtype": "m.text", "body": "Replying to this" }, - event: true, - }); - const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any; - mockRoom.findEventById = jest.fn(eventId => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); - - const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); - const customRender = ( onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false, initialContent?: string) => { return render( - - - - - , + , + ); };