From 3d5d377e58c93f6000f0eeb4e0865512a3253de8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 27 Jan 2023 17:15:24 +0100 Subject: [PATCH] Add Keyboard Up test --- .../rooms/wysiwyg_composer/utils/selection.ts | 4 +- .../EditWysiwygComposer-test.tsx | 213 ++++++++++++++++-- 2 files changed, 190 insertions(+), 27 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts index bd32d116c7b0..6740c8b55cce 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts @@ -66,8 +66,8 @@ export function isCaretAtEnd(editor: HTMLElement): boolean { return false; } - // When we are going cycling across all the timeline message with the keyboard - // The caret is on the last message element but the focusNode and the anchorNode is the editor himself instead of the text in it. + // When we are cycling across all the timeline message with the keyboard + // The caret is on the last text element but focusNode and anchorNode refers to the editor div // In this case, the focusOffset & anchorOffset match the index + 1 of the selected text const isOnLastElement = selection.focusNode === editor && selection.focusOffset === editor.childNodes?.length; if (isOnLastElement) { diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index d9343208c49d..1c1e8444bf2f 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -23,7 +23,14 @@ import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { + createTestClient, + flushPromises, + getRoomContext, + mkEvent, + mkStubRoom, + mockPlatformPeg, +} 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"; @@ -32,38 +39,55 @@ import dis from "../../../../../src/dispatcher/dispatcher"; import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { ActionPayload } from "../../../../../src/dispatcher/payloads"; import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton"; +import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; +import { EventTimeline } from "../../../../../../matrix-js-sdk"; +import * as EventUtils from "../../../../../src/utils/EventUtils"; +import { SubSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/types"; describe("EditWysiwygComposer", () => { 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", - format: "org.matrix.custom.html", - formatted_body: "Replying to this new content", - }, - event: true, - }); - const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; - mockRoom.findEventById = jest.fn((eventId) => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); + function createMocks(eventContent = "Replying to this new content") { + const mockClient = createTestClient(); + const mockEvent = mkEvent({ + type: "m.room.message", + room: "myfakeroom", + user: "myfakeuser", + content: { + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + formatted_body: eventContent, + }, + 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, { + liveTimeline: { getEvents: () => [] } as unknown as EventTimeline, + }); + + const editorStateTransfer = new EditorStateTransfer(mockEvent); - const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + return { defaultRoomContext, editorStateTransfer, mockClient, mockEvent }; + } - const editorStateTransfer = new EditorStateTransfer(mockEvent); + const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks(); - const customRender = (disabled = false, _editorStateTransfer = editorStateTransfer) => { + const customRender = ( + disabled = false, + _editorStateTransfer = editorStateTransfer, + client = mockClient, + roomContext = defaultRoomContext, + ) => { return render( - - + + , @@ -176,12 +200,14 @@ describe("EditWysiwygComposer", () => { }); describe("Edit and save actions", () => { + let spyDispatcher: jest.SpyInstance; beforeEach(async () => { + spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); customRender(); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); }); - const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + // const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); afterEach(() => { spyDispatcher.mockRestore(); }); @@ -204,7 +230,7 @@ describe("EditWysiwygComposer", () => { it("Should send message on save button click", async () => { // When - const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + // const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); fireEvent.input(screen.getByRole("textbox"), { data: "foo bar", inputType: "insertText", @@ -318,4 +344,141 @@ describe("EditWysiwygComposer", () => { await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/)); dis.unregister(dispatcherRef); }); + + describe("Keyboard navigation", () => { + const setup = async ( + editorState = editorStateTransfer, + client = createTestClient(), + roomContext = defaultRoomContext, + ) => { + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + customRender(false, editorState, client, roomContext); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + return { textbox: screen.getByRole("textbox"), spyDispatcher }; + }; + + beforeEach(() => { + mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); + }); + + function select(selection: SubSelection) { + return act(async () => { + await setSelection(selection); + // the event is not automatically fired by jest + document.dispatchEvent(new CustomEvent("selectionchange")); + }); + } + + describe("Moving up", () => { + it("Should not moving when caret is not at beginning of the text", async () => { + // When + const { textbox, spyDispatcher } = await setup(); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 1, + focusNode: textNode, + focusOffset: 2, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should not moving when the content has changed", async () => { + // When + const { textbox, spyDispatcher } = await setup(); + fireEvent.input(textbox, { + data: "word", + inputType: "insertText", + }); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should moving up", async () => { + // When + const { textbox, spyDispatcher } = await setup(); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + await waitFor(() => + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }), + ); + }); + + it("Should moving up in list", async () => { + // When + const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks( + "
  • Content
  • Other Content
", + ); + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); + const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext); + + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); + }); + }); + }); });