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(
+ "",
+ );
+ 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,
+ });
+ });
+ });
+ });
});