312 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| 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 "@testing-library/jest-dom";
 | |
| import React from "react";
 | |
| import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
 | |
| 
 | |
| 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 { 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";
 | |
| import { ChevronFace } from "../../../../../src/components/structures/ContextMenu";
 | |
| import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload";
 | |
| import { ActionPayload } from "../../../../../src/dispatcher/payloads";
 | |
| import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton";
 | |
| import { createMocks } from "./utils";
 | |
| 
 | |
| describe("EditWysiwygComposer", () => {
 | |
|     afterEach(() => {
 | |
|         jest.resetAllMocks();
 | |
|     });
 | |
| 
 | |
|     const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks();
 | |
| 
 | |
|     const customRender = (
 | |
|         disabled = false,
 | |
|         _editorStateTransfer = editorStateTransfer,
 | |
|         client = mockClient,
 | |
|         roomContext = defaultRoomContext,
 | |
|     ) => {
 | |
|         return render(
 | |
|             <MatrixClientContext.Provider value={client}>
 | |
|                 <RoomContext.Provider value={roomContext}>
 | |
|                     <EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} />
 | |
|                 </RoomContext.Provider>
 | |
|             </MatrixClientContext.Provider>,
 | |
|         );
 | |
|     };
 | |
| 
 | |
|     beforeAll(
 | |
|         async () => {
 | |
|             // Load the dynamic import
 | |
|             const component = customRender(false);
 | |
|             await component.findByRole("textbox");
 | |
|             component.unmount();
 | |
|         },
 | |
|         // it can take a while to load the wasm
 | |
|         20000,
 | |
|     );
 | |
| 
 | |
|     it("Should not render the component when not ready", async () => {
 | |
|         // When
 | |
|         const { rerender } = customRender(false);
 | |
|         await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
 | |
| 
 | |
|         rerender(
 | |
|             <MatrixClientContext.Provider value={mockClient}>
 | |
|                 <RoomContext.Provider value={{ ...defaultRoomContext, room: undefined }}>
 | |
|                     <EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} />
 | |
|                 </RoomContext.Provider>
 | |
|             </MatrixClientContext.Provider>,
 | |
|         );
 | |
| 
 | |
|         // Then
 | |
|         await waitFor(() => expect(screen.queryByRole("textbox")).toBeNull());
 | |
|     });
 | |
| 
 | |
|     describe("Initialize with content", () => {
 | |
|         it("Should initialize useWysiwyg with html content", async () => {
 | |
|             // When
 | |
|             customRender(false, editorStateTransfer);
 | |
| 
 | |
|             // Then
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"), {
 | |
|                 timeout: 2000,
 | |
|             });
 | |
| 
 | |
|             await waitFor(() =>
 | |
|                 expect(screen.getByRole("textbox")).toContainHTML(mockEvent.getContent()["formatted_body"]),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it("Should initialize useWysiwyg with plain text content", async () => {
 | |
|             // When
 | |
|             const mockEvent = mkEvent({
 | |
|                 type: "m.room.message",
 | |
|                 room: "myfakeroom",
 | |
|                 user: "myfakeuser",
 | |
|                 content: {
 | |
|                     msgtype: "m.text",
 | |
|                     body: "Replying to this",
 | |
|                 },
 | |
|                 event: true,
 | |
|             });
 | |
|             const editorStateTransfer = new EditorStateTransfer(mockEvent);
 | |
|             customRender(false, editorStateTransfer);
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
 | |
| 
 | |
|             // Then
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toContainHTML(mockEvent.getContent()["body"]));
 | |
|         });
 | |
| 
 | |
|         it("Should ignore when formatted_body is not filled", async () => {
 | |
|             // When
 | |
|             const mockEvent = mkEvent({
 | |
|                 type: "m.room.message",
 | |
|                 room: "myfakeroom",
 | |
|                 user: "myfakeuser",
 | |
|                 content: {
 | |
|                     msgtype: "m.text",
 | |
|                     body: "Replying to this",
 | |
|                     format: "org.matrix.custom.html",
 | |
|                 },
 | |
|                 event: true,
 | |
|             });
 | |
| 
 | |
|             const editorStateTransfer = new EditorStateTransfer(mockEvent);
 | |
|             customRender(false, editorStateTransfer);
 | |
| 
 | |
|             // Then
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
 | |
|         });
 | |
| 
 | |
|         it("Should strip <mx-reply> tag from initial content", async () => {
 | |
|             // When
 | |
|             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: "<mx-reply>Reply</mx-reply>My content",
 | |
|                 },
 | |
|                 event: true,
 | |
|             });
 | |
| 
 | |
|             const editorStateTransfer = new EditorStateTransfer(mockEvent);
 | |
|             customRender(false, editorStateTransfer);
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
 | |
| 
 | |
|             // Then
 | |
|             await waitFor(() => {
 | |
|                 expect(screen.getByRole("textbox")).not.toContainHTML("<mx-reply>Reply</mx-reply>");
 | |
|                 expect(screen.getByRole("textbox")).toContainHTML("My content");
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("Edit and save actions", () => {
 | |
|         let spyDispatcher: jest.SpyInstance<void, [payload: ActionPayload, sync?: boolean]>;
 | |
|         beforeEach(async () => {
 | |
|             spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
 | |
|             customRender();
 | |
|             await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
 | |
|         });
 | |
| 
 | |
|         afterEach(() => {
 | |
|             spyDispatcher.mockRestore();
 | |
|         });
 | |
| 
 | |
|         it("Should cancel edit on cancel button click", async () => {
 | |
|             // When
 | |
|             screen.getByText("Cancel").click();
 | |
| 
 | |
|             // Then
 | |
|             expect(spyDispatcher).toHaveBeenCalledWith({
 | |
|                 action: Action.EditEvent,
 | |
|                 event: null,
 | |
|                 timelineRenderingType: defaultRoomContext.timelineRenderingType,
 | |
|             });
 | |
|             expect(spyDispatcher).toHaveBeenCalledWith({
 | |
|                 action: Action.FocusSendMessageComposer,
 | |
|                 context: defaultRoomContext.timelineRenderingType,
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         it("Should send message on save button click", async () => {
 | |
|             // When
 | |
|             fireEvent.input(screen.getByRole("textbox"), {
 | |
|                 data: "foo bar",
 | |
|                 inputType: "insertText",
 | |
|             });
 | |
|             await waitFor(() => expect(screen.getByText("Save")).not.toHaveAttribute("disabled"));
 | |
| 
 | |
|             // Then
 | |
|             screen.getByText("Save").click();
 | |
|             const expectedContent = {
 | |
|                 "body": ` * foo bar`,
 | |
|                 "format": "org.matrix.custom.html",
 | |
|                 "formatted_body": ` * foo bar`,
 | |
|                 "m.new_content": {
 | |
|                     body: "foo bar",
 | |
|                     format: "org.matrix.custom.html",
 | |
|                     formatted_body: "foo bar",
 | |
|                     msgtype: "m.text",
 | |
|                 },
 | |
|                 "m.relates_to": {
 | |
|                     event_id: mockEvent.getId(),
 | |
|                     rel_type: "m.replace",
 | |
|                 },
 | |
|                 "msgtype": "m.text",
 | |
|             };
 | |
|             await waitFor(() =>
 | |
|                 expect(mockClient.sendMessage).toHaveBeenCalledWith(mockEvent.getRoomId(), null, expectedContent),
 | |
|             );
 | |
| 
 | |
|             expect(spyDispatcher).toHaveBeenCalledWith({ action: "message_sent" });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     it("Should focus when receiving an Action.FocusEditMessageComposer action", async () => {
 | |
|         // Given we don't have focus
 | |
|         customRender();
 | |
|         screen.getByLabelText("Bold").focus();
 | |
|         expect(screen.getByRole("textbox")).not.toHaveFocus();
 | |
| 
 | |
|         // When we send the right action
 | |
|         defaultDispatcher.dispatch({
 | |
|             action: Action.FocusEditMessageComposer,
 | |
|             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(true);
 | |
|         screen.getByLabelText("Bold").focus();
 | |
|         expect(screen.getByRole("textbox")).not.toHaveFocus();
 | |
| 
 | |
|         // When we send an action that would cause us to get focus
 | |
|         defaultDispatcher.dispatch({
 | |
|             action: Action.FocusEditMessageComposer,
 | |
|             context: null,
 | |
|         });
 | |
|         // (Send a second event to exercise the clearTimeout logic)
 | |
|         defaultDispatcher.dispatch({
 | |
|             action: Action.FocusEditMessageComposer,
 | |
|             context: null,
 | |
|         });
 | |
| 
 | |
|         // Wait for event dispatch to happen
 | |
|         await act(async () => {
 | |
|             await flushPromises();
 | |
|         });
 | |
| 
 | |
|         // Then we don't get it because we are disabled
 | |
|         expect(screen.getByRole("textbox")).not.toHaveFocus();
 | |
|     });
 | |
| 
 | |
|     it("Should add emoji", async () => {
 | |
|         // When
 | |
| 
 | |
|         // We are not testing here the emoji button (open modal, select emoji ...)
 | |
|         // Instead we are directly firing an emoji to make the test easier to write
 | |
|         jest.spyOn(EmojiButton, "EmojiButton").mockImplementation(
 | |
|             ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
 | |
|                 return (
 | |
|                     <button aria-label="Emoji" type="button" onClick={() => addEmoji("🦫")}>
 | |
|                         Emoji
 | |
|                     </button>
 | |
|                 );
 | |
|             },
 | |
|         );
 | |
|         render(
 | |
|             <MatrixClientContext.Provider value={mockClient}>
 | |
|                 <RoomContext.Provider value={defaultRoomContext}>
 | |
|                     <EditWysiwygComposer editorStateTransfer={editorStateTransfer} />
 | |
|                     <Emoji menuPosition={{ chevronFace: ChevronFace.Top }} />
 | |
|                 </RoomContext.Provider>
 | |
|             </MatrixClientContext.Provider>,
 | |
|         );
 | |
|         // Same behavior as in RoomView.tsx
 | |
|         // RoomView is re-dispatching the composer messages.
 | |
|         // It adds the composerType fields where the value refers if the composer is in editing or not
 | |
|         // The listeners in the RTE ignore the message if the composerType is missing in the payload
 | |
|         const dispatcherRef = defaultDispatcher.register((payload: ActionPayload) => {
 | |
|             defaultDispatcher.dispatch<ComposerInsertPayload>({
 | |
|                 ...(payload as ComposerInsertPayload),
 | |
|                 composerType: ComposerType.Edit,
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         screen.getByLabelText("Emoji").click();
 | |
| 
 | |
|         // Then
 | |
|         await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/));
 | |
|         defaultDispatcher.unregister(dispatcherRef);
 | |
|     });
 | |
| });
 |