Open message in editing mode when keyboard up is pressed (RTE) (#10079)
Move to previous message when arrow up is pressed in the main composer (RTE)
This commit is contained in:
parent
f1a08cd572
commit
2b66cfc25f
11 changed files with 487 additions and 365 deletions
|
@ -489,6 +489,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
e2eStatus={this.props.e2eStatus}
|
e2eStatus={this.props.e2eStatus}
|
||||||
menuPosition={menuPosition}
|
menuPosition={menuPosition}
|
||||||
placeholder={this.renderPlaceholderText()}
|
placeholder={this.renderPlaceholderText()}
|
||||||
|
eventRelation={this.props.relation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
import { IEventRelation } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { SubSelection } from "./types";
|
import { SubSelection } from "./types";
|
||||||
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
||||||
|
@ -29,6 +30,7 @@ export function getDefaultContextValue(defaultValue?: Partial<ComposerContextSta
|
||||||
export interface ComposerContextState {
|
export interface ComposerContextState {
|
||||||
selection: SubSelection;
|
selection: SubSelection;
|
||||||
editorStateTransfer?: EditorStateTransfer;
|
editorStateTransfer?: EditorStateTransfer;
|
||||||
|
eventRelation?: IEventRelation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComposerContext = createContext<ComposerContextState>(getDefaultContextValue());
|
export const ComposerContext = createContext<ComposerContextState>(getDefaultContextValue());
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
|
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react";
|
||||||
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
||||||
import { WysiwygComposer } from "./components/WysiwygComposer";
|
import { WysiwygComposer } from "./components/WysiwygComposer";
|
||||||
|
@ -48,6 +49,7 @@ interface SendWysiwygComposerProps {
|
||||||
onChange: (content: string) => void;
|
onChange: (content: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
menuPosition: MenuProps;
|
menuPosition: MenuProps;
|
||||||
|
eventRelation?: IEventRelation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default needed for React.lazy
|
// Default needed for React.lazy
|
||||||
|
@ -55,10 +57,11 @@ export default function SendWysiwygComposer({
|
||||||
isRichTextEnabled,
|
isRichTextEnabled,
|
||||||
e2eStatus,
|
e2eStatus,
|
||||||
menuPosition,
|
menuPosition,
|
||||||
|
eventRelation,
|
||||||
...props
|
...props
|
||||||
}: SendWysiwygComposerProps): JSX.Element {
|
}: SendWysiwygComposerProps): JSX.Element {
|
||||||
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||||
const defaultContextValue = useRef(getDefaultContextValue());
|
const defaultContextValue = useRef(getDefaultContextValue({ eventRelation }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerContext.Provider value={defaultContextValue.current}>
|
<ComposerContext.Provider value={defaultContextValue.current}>
|
||||||
|
|
|
@ -33,7 +33,7 @@ function getFormattedContent(editorStateTransfer: EditorStateTransfer): string {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEditorStateTransfer(
|
export function parseEditorStateTransfer(
|
||||||
editorStateTransfer: EditorStateTransfer,
|
editorStateTransfer: EditorStateTransfer,
|
||||||
room: Room,
|
room: Room,
|
||||||
mxClient: MatrixClient,
|
mxClient: MatrixClient,
|
||||||
|
@ -64,7 +64,7 @@ function parseEditorStateTransfer(
|
||||||
// this.saveStoredEditorState();
|
// this.saveStoredEditorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialContent(editorStateTransfer: EditorStateTransfer): string {
|
export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
const mxClient = useMatrixClientContext();
|
const mxClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { ComposerContextState, useComposerContext } from "../ComposerContext";
|
||||||
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||||
import { isCaretAtEnd, isCaretAtStart } from "../utils/selection";
|
import { isCaretAtEnd, isCaretAtStart } from "../utils/selection";
|
||||||
import { getEventsFromEditorStateTransfer } from "../utils/event";
|
import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/event";
|
||||||
import { endEditing } from "../utils/editing";
|
import { endEditing } from "../utils/editing";
|
||||||
|
|
||||||
export function useInputEventProcessor(
|
export function useInputEventProcessor(
|
||||||
|
@ -87,7 +87,8 @@ function handleKeyboardEvent(
|
||||||
mxClient: MatrixClient,
|
mxClient: MatrixClient,
|
||||||
): KeyboardEvent | null {
|
): KeyboardEvent | null {
|
||||||
const { editorStateTransfer } = composerContext;
|
const { editorStateTransfer } = composerContext;
|
||||||
const isEditorModified = initialContent !== composer.content();
|
const isEditing = Boolean(editorStateTransfer);
|
||||||
|
const isEditorModified = isEditing ? initialContent !== composer.content() : composer.content().length !== 0;
|
||||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
@ -95,14 +96,21 @@ function handleKeyboardEvent(
|
||||||
send();
|
send();
|
||||||
return null;
|
return null;
|
||||||
case KeyBindingAction.EditPrevMessage: {
|
case KeyBindingAction.EditPrevMessage: {
|
||||||
// If not in edition
|
|
||||||
// Or if the caret is not at the beginning of the editor
|
// Or if the caret is not at the beginning of the editor
|
||||||
// Or the editor is modified
|
// Or the editor is modified
|
||||||
if (!editorStateTransfer || !isCaretAtStart(editor) || isEditorModified) {
|
if (!isCaretAtStart(editor) || isEditorModified) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDispatched = dispatchEditEvent(event, false, editorStateTransfer, roomContext, mxClient);
|
const isDispatched = dispatchEditEvent(
|
||||||
|
event,
|
||||||
|
false,
|
||||||
|
editorStateTransfer,
|
||||||
|
composerContext,
|
||||||
|
roomContext,
|
||||||
|
mxClient,
|
||||||
|
);
|
||||||
|
|
||||||
if (isDispatched) {
|
if (isDispatched) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -117,7 +125,14 @@ function handleKeyboardEvent(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDispatched = dispatchEditEvent(event, true, editorStateTransfer, roomContext, mxClient);
|
const isDispatched = dispatchEditEvent(
|
||||||
|
event,
|
||||||
|
true,
|
||||||
|
editorStateTransfer,
|
||||||
|
composerContext,
|
||||||
|
roomContext,
|
||||||
|
mxClient,
|
||||||
|
);
|
||||||
if (!isDispatched) {
|
if (!isDispatched) {
|
||||||
endEditing(roomContext);
|
endEditing(roomContext);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -134,11 +149,14 @@ function handleKeyboardEvent(
|
||||||
function dispatchEditEvent(
|
function dispatchEditEvent(
|
||||||
event: KeyboardEvent,
|
event: KeyboardEvent,
|
||||||
isForward: boolean,
|
isForward: boolean,
|
||||||
editorStateTransfer: EditorStateTransfer,
|
editorStateTransfer: EditorStateTransfer | undefined,
|
||||||
|
composerContext: ComposerContextState,
|
||||||
roomContext: IRoomState,
|
roomContext: IRoomState,
|
||||||
mxClient: MatrixClient,
|
mxClient: MatrixClient,
|
||||||
): boolean {
|
): boolean {
|
||||||
const foundEvents = getEventsFromEditorStateTransfer(editorStateTransfer, roomContext, mxClient);
|
const foundEvents = editorStateTransfer
|
||||||
|
? getEventsFromEditorStateTransfer(editorStateTransfer, roomContext, mxClient)
|
||||||
|
: getEventsFromRoom(composerContext, roomContext);
|
||||||
if (!foundEvents) {
|
if (!foundEvents) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -146,7 +164,7 @@ function dispatchEditEvent(
|
||||||
const newEvent = findEditableEvent({
|
const newEvent = findEditableEvent({
|
||||||
events: foundEvents,
|
events: foundEvents,
|
||||||
isForward,
|
isForward,
|
||||||
fromEventId: editorStateTransfer.getEvent().getId(),
|
fromEventId: editorStateTransfer?.getEvent().getId(),
|
||||||
});
|
});
|
||||||
if (newEvent) {
|
if (newEvent) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
|
@ -15,9 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
|
||||||
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
import { IRoomState } from "../../../../structures/RoomView";
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
import { ComposerContextState } from "../ComposerContext";
|
||||||
|
|
||||||
// From EditMessageComposer private get events(): MatrixEvent[]
|
// From EditMessageComposer private get events(): MatrixEvent[]
|
||||||
export function getEventsFromEditorStateTransfer(
|
export function getEventsFromEditorStateTransfer(
|
||||||
|
@ -44,3 +46,14 @@ export function getEventsFromEditorStateTransfer(
|
||||||
const isInThread = Boolean(editorStateTransfer.getEvent().getThread());
|
const isInThread = Boolean(editorStateTransfer.getEvent().getThread());
|
||||||
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void
|
||||||
|
export function getEventsFromRoom(
|
||||||
|
composerContext: ComposerContextState,
|
||||||
|
roomContext: IRoomState,
|
||||||
|
): MatrixEvent[] | undefined {
|
||||||
|
const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name;
|
||||||
|
return roomContext.liveTimeline
|
||||||
|
?.getEvents()
|
||||||
|
.concat(isReplyingToThread ? [] : roomContext.room?.getPendingEvents() || []);
|
||||||
|
}
|
||||||
|
|
|
@ -44,15 +44,21 @@ export function isCaretAtStart(editor: HTMLElement): boolean {
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
|
|
||||||
// No selection or the caret is not at the beginning of the selected element
|
// No selection or the caret is not at the beginning of the selected element
|
||||||
if (!selection || selection.anchorOffset !== 0) {
|
if (!selection) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When we are pressing keyboard up in an empty main composer, the selection is on the editor with an anchorOffset at O or 1 (yes, this is strange)
|
||||||
|
const isOnFirstElement = selection.anchorNode === editor && selection.anchorOffset <= 1;
|
||||||
|
if (isOnFirstElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// In case of nested html elements (list, code blocks), we are going through all the first child
|
// In case of nested html elements (list, code blocks), we are going through all the first child
|
||||||
let child = editor.firstChild;
|
let child = editor.firstChild;
|
||||||
do {
|
do {
|
||||||
if (child === selection.anchorNode) {
|
if (child === selection.anchorNode) {
|
||||||
return true;
|
return selection.anchorOffset === 0;
|
||||||
}
|
}
|
||||||
} while ((child = child?.firstChild || null));
|
} while ((child = child?.firstChild || null));
|
||||||
|
|
||||||
|
|
|
@ -17,21 +17,12 @@ limitations under the License.
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { EventTimeline, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
import { flushPromises, mkEvent } from "../../../../test-utils";
|
||||||
import {
|
|
||||||
createTestClient,
|
|
||||||
flushPromises,
|
|
||||||
getRoomContext,
|
|
||||||
mkEvent,
|
|
||||||
mkStubRoom,
|
|
||||||
mockPlatformPeg,
|
|
||||||
} from "../../../../test-utils";
|
|
||||||
import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
||||||
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
|
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
|
||||||
import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji";
|
import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji";
|
||||||
|
@ -40,43 +31,13 @@ import dis from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { ActionPayload } from "../../../../../src/dispatcher/payloads";
|
import { ActionPayload } from "../../../../../src/dispatcher/payloads";
|
||||||
import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton";
|
import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton";
|
||||||
import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
|
import { createMocks } from "./utils";
|
||||||
import * as EventUtils from "../../../../../src/utils/EventUtils";
|
|
||||||
import { SubSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/types";
|
|
||||||
|
|
||||||
describe("EditWysiwygComposer", () => {
|
describe("EditWysiwygComposer", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function createMocks(eventContent = "Replying <strong>to</strong> 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: (): MatrixEvent[] => [] } as unknown as EventTimeline,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorStateTransfer = new EditorStateTransfer(mockEvent);
|
|
||||||
|
|
||||||
return { defaultRoomContext, editorStateTransfer, mockClient, mockEvent };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks();
|
const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks();
|
||||||
|
|
||||||
const customRender = (
|
const customRender = (
|
||||||
|
@ -342,290 +303,4 @@ describe("EditWysiwygComposer", () => {
|
||||||
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/));
|
await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/));
|
||||||
dis.unregister(dispatcherRef);
|
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(
|
|
||||||
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Moving down", () => {
|
|
||||||
it("Should not moving when caret is not at the end of the text", async () => {
|
|
||||||
// When
|
|
||||||
const { textbox, spyDispatcher } = await setup();
|
|
||||||
const brNode = textbox.lastChild;
|
|
||||||
await select({
|
|
||||||
anchorNode: brNode,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusNode: brNode,
|
|
||||||
focusOffset: 0,
|
|
||||||
isForward: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(textbox, {
|
|
||||||
key: "ArrowDown",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 brNode = textbox.lastChild;
|
|
||||||
await select({
|
|
||||||
anchorNode: brNode,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusNode: brNode,
|
|
||||||
focusOffset: 0,
|
|
||||||
isForward: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(textbox, {
|
|
||||||
key: "ArrowDown",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(spyDispatcher).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should moving down", async () => {
|
|
||||||
// When
|
|
||||||
const { textbox, spyDispatcher } = await setup();
|
|
||||||
// Skipping the BR tag
|
|
||||||
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
|
|
||||||
const { length } = textNode.textContent || "";
|
|
||||||
await select({
|
|
||||||
anchorNode: textNode,
|
|
||||||
anchorOffset: length,
|
|
||||||
focusNode: textNode,
|
|
||||||
focusOffset: length,
|
|
||||||
isForward: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(textbox, {
|
|
||||||
key: "ArrowDown",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 down in list", async () => {
|
|
||||||
// When
|
|
||||||
const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks(
|
|
||||||
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
|
|
||||||
);
|
|
||||||
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
|
|
||||||
const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext);
|
|
||||||
|
|
||||||
// Skipping the BR tag and get the text node inside the last LI tag
|
|
||||||
const textNode = textbox.childNodes[textbox.childNodes.length - 2].lastChild?.lastChild || textbox;
|
|
||||||
const { length } = textNode.textContent || "";
|
|
||||||
await select({
|
|
||||||
anchorNode: textNode,
|
|
||||||
anchorOffset: length,
|
|
||||||
focusNode: textNode,
|
|
||||||
focusOffset: length,
|
|
||||||
isForward: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(textbox, {
|
|
||||||
key: "ArrowDown",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for event dispatch to happen
|
|
||||||
await act(async () => {
|
|
||||||
await flushPromises();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(spyDispatcher).toBeCalledWith({
|
|
||||||
action: Action.EditEvent,
|
|
||||||
event: mockEvent,
|
|
||||||
timelineRenderingType: defaultRoomContext.timelineRenderingType,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should close editing", async () => {
|
|
||||||
// When
|
|
||||||
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(undefined);
|
|
||||||
const { textbox, spyDispatcher } = await setup();
|
|
||||||
// Skipping the BR tag
|
|
||||||
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
|
|
||||||
const { length } = textNode.textContent || "";
|
|
||||||
await select({
|
|
||||||
anchorNode: textNode,
|
|
||||||
anchorOffset: length,
|
|
||||||
focusNode: textNode,
|
|
||||||
focusOffset: length,
|
|
||||||
isForward: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(textbox, {
|
|
||||||
key: "ArrowDown",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for event dispatch to happen
|
|
||||||
await act(async () => {
|
|
||||||
await flushPromises();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(spyDispatcher).toBeCalledWith({
|
|
||||||
action: Action.EditEvent,
|
|
||||||
event: null,
|
|
||||||
timelineRenderingType: defaultRoomContext.timelineRenderingType,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,12 +22,12 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
|
||||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
import { flushPromises } from "../../../../test-utils";
|
||||||
import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
|
||||||
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/";
|
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/";
|
||||||
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu";
|
||||||
import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
|
import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
|
||||||
|
import { createMocks } from "./utils";
|
||||||
|
|
||||||
jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({
|
jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({
|
||||||
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
|
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
|
||||||
|
@ -44,20 +44,7 @@ describe("SendWysiwygComposer", () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockClient = createTestClient();
|
const { defaultRoomContext, mockClient } = createMocks();
|
||||||
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 registerId = defaultDispatcher.register((payload) => {
|
const registerId = defaultDispatcher.register((payload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
|
|
@ -16,25 +16,38 @@ limitations under the License.
|
||||||
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import { mockPlatformPeg } from "../../../../../test-utils";
|
import { createTestClient, flushPromises, mockPlatformPeg } from "../../../../../test-utils";
|
||||||
|
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||||
|
import * as EventUtils from "../../../../../../src/utils/EventUtils";
|
||||||
|
import { Action } from "../../../../../../src/dispatcher/actions";
|
||||||
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
|
import RoomContext from "../../../../../../src/contexts/RoomContext";
|
||||||
|
import {
|
||||||
|
ComposerContext,
|
||||||
|
getDefaultContextValue,
|
||||||
|
} from "../../../../../../src/components/views/rooms/wysiwyg_composer/ComposerContext";
|
||||||
|
import { createMocks } from "../utils";
|
||||||
|
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";
|
||||||
|
|
||||||
describe("WysiwygComposer", () => {
|
describe("WysiwygComposer", () => {
|
||||||
const customRender = (
|
const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => {
|
||||||
onChange = (_content: string) => void 0,
|
|
||||||
onSend = () => void 0,
|
|
||||||
disabled = false,
|
|
||||||
initialContent?: string,
|
|
||||||
) => {
|
|
||||||
return render(
|
return render(
|
||||||
<WysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} initialContent={initialContent} />,
|
<WysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} initialContent={initialContent} />,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("Should have contentEditable at false when disabled", () => {
|
it("Should have contentEditable at false when disabled", () => {
|
||||||
// When
|
// When
|
||||||
customRender(jest.fn(), jest.fn(), true);
|
customRender(jest.fn(), jest.fn(), true);
|
||||||
|
@ -191,4 +204,359 @@ describe("WysiwygComposer", () => {
|
||||||
await waitFor(() => expect(onSend).toBeCalledTimes(1));
|
await waitFor(() => expect(onSend).toBeCalledTimes(1));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Keyboard navigation", () => {
|
||||||
|
const { mockClient, defaultRoomContext, mockEvent, editorStateTransfer } = createMocks();
|
||||||
|
|
||||||
|
const customRender = (
|
||||||
|
client = mockClient,
|
||||||
|
roomContext = defaultRoomContext,
|
||||||
|
_editorStateTransfer?: EditorStateTransfer,
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<RoomContext.Provider value={roomContext}>
|
||||||
|
<ComposerContext.Provider
|
||||||
|
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
|
||||||
|
>
|
||||||
|
<WysiwygComposer
|
||||||
|
onChange={jest.fn()}
|
||||||
|
onSend={jest.fn()}
|
||||||
|
initialContent={
|
||||||
|
roomContext.room && _editorStateTransfer
|
||||||
|
? parseEditorStateTransfer(_editorStateTransfer, roomContext.room, client)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ComposerContext.Provider>
|
||||||
|
</RoomContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const setup = async (
|
||||||
|
editorState?: EditorStateTransfer,
|
||||||
|
client = createTestClient(),
|
||||||
|
roomContext = defaultRoomContext,
|
||||||
|
) => {
|
||||||
|
const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
|
||||||
|
customRender(client, roomContext, editorState);
|
||||||
|
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
|
||||||
|
return { textbox: screen.getByRole("textbox"), spyDispatcher };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
|
||||||
|
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("In message creation", () => {
|
||||||
|
it("Should not moving when the composer is filled", async () => {
|
||||||
|
// When
|
||||||
|
const { textbox, spyDispatcher } = await setup();
|
||||||
|
fireEvent.input(textbox, {
|
||||||
|
data: "word",
|
||||||
|
inputType: "insertText",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move at the beginning of the composer
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowUp",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spyDispatcher).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should moving when the composer is empty", async () => {
|
||||||
|
// When
|
||||||
|
const { textbox, spyDispatcher } = await setup();
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowUp",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spyDispatcher).toBeCalledWith({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: mockEvent,
|
||||||
|
timelineRenderingType: defaultRoomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("In message editing", () => {
|
||||||
|
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(editorStateTransfer);
|
||||||
|
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(editorStateTransfer);
|
||||||
|
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(editorStateTransfer);
|
||||||
|
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(
|
||||||
|
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Moving down", () => {
|
||||||
|
it("Should not moving when caret is not at the end of the text", async () => {
|
||||||
|
// When
|
||||||
|
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
|
||||||
|
const brNode = textbox.lastChild;
|
||||||
|
await select({
|
||||||
|
anchorNode: brNode,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusNode: brNode,
|
||||||
|
focusOffset: 0,
|
||||||
|
isForward: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowDown",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spyDispatcher).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not moving when the content has changed", async () => {
|
||||||
|
// When
|
||||||
|
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
|
||||||
|
fireEvent.input(textbox, {
|
||||||
|
data: "word",
|
||||||
|
inputType: "insertText",
|
||||||
|
});
|
||||||
|
const brNode = textbox.lastChild;
|
||||||
|
await select({
|
||||||
|
anchorNode: brNode,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusNode: brNode,
|
||||||
|
focusOffset: 0,
|
||||||
|
isForward: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowDown",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spyDispatcher).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should moving down", async () => {
|
||||||
|
// When
|
||||||
|
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
|
||||||
|
// Skipping the BR tag
|
||||||
|
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
|
||||||
|
const { length } = textNode.textContent || "";
|
||||||
|
await select({
|
||||||
|
anchorNode: textNode,
|
||||||
|
anchorOffset: length,
|
||||||
|
focusNode: textNode,
|
||||||
|
focusOffset: length,
|
||||||
|
isForward: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowDown",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 down in list", async () => {
|
||||||
|
// When
|
||||||
|
const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks(
|
||||||
|
"<ul><li><strong>Content</strong></li><li>Other Content</li></ul>",
|
||||||
|
);
|
||||||
|
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent);
|
||||||
|
const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext);
|
||||||
|
|
||||||
|
// Skipping the BR tag and get the text node inside the last LI tag
|
||||||
|
const textNode = textbox.childNodes[textbox.childNodes.length - 2].lastChild?.lastChild || textbox;
|
||||||
|
const { length } = textNode.textContent || "";
|
||||||
|
await select({
|
||||||
|
anchorNode: textNode,
|
||||||
|
anchorOffset: length,
|
||||||
|
focusNode: textNode,
|
||||||
|
focusOffset: length,
|
||||||
|
isForward: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowDown",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for event dispatch to happen
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(spyDispatcher).toBeCalledWith({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: mockEvent,
|
||||||
|
timelineRenderingType: defaultRoomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should close editing", async () => {
|
||||||
|
// When
|
||||||
|
jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(undefined);
|
||||||
|
const { textbox, spyDispatcher } = await setup(editorStateTransfer);
|
||||||
|
// Skipping the BR tag
|
||||||
|
const textNode = textbox.childNodes[textbox.childNodes.length - 2];
|
||||||
|
const { length } = textNode.textContent || "";
|
||||||
|
await select({
|
||||||
|
anchorNode: textNode,
|
||||||
|
anchorOffset: length,
|
||||||
|
focusNode: textNode,
|
||||||
|
focusOffset: length,
|
||||||
|
isForward: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(textbox, {
|
||||||
|
key: "ArrowDown",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for event dispatch to happen
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(spyDispatcher).toBeCalledWith({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: defaultRoomContext.timelineRenderingType,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
49
test/components/views/rooms/wysiwyg_composer/utils.ts
Normal file
49
test/components/views/rooms/wysiwyg_composer/utils.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
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 { EventTimeline, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
||||||
|
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
|
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
export function createMocks(eventContent = "Replying <strong>to</strong> 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: (): MatrixEvent[] => [] } as unknown as EventTimeline,
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorStateTransfer = new EditorStateTransfer(mockEvent);
|
||||||
|
|
||||||
|
return { defaultRoomContext, editorStateTransfer, mockClient, mockEvent };
|
||||||
|
}
|
Loading…
Reference in a new issue