diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index ea49e9fcbe..29f2a87206 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -239,7 +239,11 @@ interface ISendMessageComposerProps extends MatrixClientProps { toggleStickerPickerOpen: () => void; } -export class SendMessageComposer extends React.Component { +interface SendMessageComposerState { + isReady: boolean; +} + +export class SendMessageComposer extends React.Component { public static contextType = RoomContext; public declare context: React.ContextType; @@ -257,6 +261,7 @@ export class SendMessageComposer extends React.Component { @@ -272,6 +277,7 @@ export class SendMessageComposer extends React.Component { describe("for a Room", () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); - it("Renders a SendMessageComposer and MessageComposerButtons by default", () => { - wrapAndRender({ room }); + it("Renders a SendMessageComposer and MessageComposerButtons by default", async () => { + await wrapAndRender({ room }); expect(screen.getByLabelText("Send a message…")).toBeInTheDocument(); }); - it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => { - wrapAndRender({ room }, false); + it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", async () => { + await wrapAndRender({ room }, false); expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument(); expect(screen.getByText("You do not have permission to post to this room")).toBeInTheDocument(); }); - it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => { - wrapAndRender( + it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", async () => { + await wrapAndRender( { room }, true, false, @@ -135,15 +135,17 @@ describe("MessageComposer", () => { let roomContext: IRoomState; let resizeNotifier: ResizeNotifier; - beforeEach(() => { + beforeEach(async () => { jest.useFakeTimers(); resizeNotifier = { notifyTimelineHeightChanged: jest.fn(), } as unknown as ResizeNotifier; - roomContext = wrapAndRender({ - room, - resizeNotifier, - }).roomContext; + roomContext = ( + await wrapAndRender({ + room, + resizeNotifier, + }) + ).roomContext; }); it("should call notifyTimelineHeightChanged() for the same context", () => { @@ -185,8 +187,8 @@ describe("MessageComposer", () => { [true, false].forEach((value: boolean) => { describe(`when ${setting} = ${value}`, () => { beforeEach(async () => { - SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); - wrapAndRender({ room }); + await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); + await wrapAndRender({ room }); await act(async () => { await userEvent.click(screen.getByLabelText("More options")); }); @@ -230,14 +232,14 @@ describe("MessageComposer", () => { }); }); - it("should not render the send button", () => { - wrapAndRender({ room }); + it("should not render the send button", async () => { + await wrapAndRender({ room }); expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument(); }); describe("when a message has been entered", () => { beforeEach(async () => { - const renderResult = wrapAndRender({ room }).renderResult; + const renderResult = (await wrapAndRender({ room })).renderResult; await addTextToComposerRTL(renderResult, "Hello"); }); @@ -259,7 +261,7 @@ describe("MessageComposer", () => { describe("when a non-resize event occurred in UIStore", () => { beforeEach(async () => { - wrapAndRender({ room }); + await wrapAndRender({ room }); await openStickerPicker(); resizeCallback("test", {}); }); @@ -271,7 +273,7 @@ describe("MessageComposer", () => { describe("when a resize to narrow event occurred in UIStore", () => { beforeEach(async () => { - wrapAndRender({ room }, true, true); + await wrapAndRender({ room }, true, true); await openStickerPicker(); resizeCallback(UI_EVENTS.Resize, {}); }); @@ -293,7 +295,7 @@ describe("MessageComposer", () => { describe("when a resize to non-narrow event occurred in UIStore", () => { beforeEach(async () => { - wrapAndRender({ room }, true, false); + await wrapAndRender({ room }, true, false); await openStickerPicker(); resizeCallback(UI_EVENTS.Resize, {}); }); @@ -315,13 +317,13 @@ describe("MessageComposer", () => { }); describe("when not replying to an event", () => { - it("should pass the expected placeholder to SendMessageComposer", () => { - wrapAndRender({ room }); + it("should pass the expected placeholder to SendMessageComposer", async () => { + await wrapAndRender({ room }); expect(screen.getByLabelText("Send a message…")).toBeInTheDocument(); }); - it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => { - wrapAndRender({ + it("and an e2e status it should pass the expected placeholder to SendMessageComposer", async () => { + await wrapAndRender({ room, e2eStatus: E2EStatus.Normal, }); @@ -334,8 +336,8 @@ describe("MessageComposer", () => { let props: Partial>; const checkPlaceholder = (expected: string) => { - it("should pass the expected placeholder to SendMessageComposer", () => { - wrapAndRender(props); + it("should pass the expected placeholder to SendMessageComposer", async () => { + await wrapAndRender(props); expect(screen.getByLabelText(expected)).toBeInTheDocument(); }); }; @@ -393,7 +395,7 @@ describe("MessageComposer", () => { describe("when clicking start a voice message", () => { beforeEach(async () => { - wrapAndRender({ room }); + await wrapAndRender({ room }); await startVoiceMessage(); await flushPromises(); }); @@ -406,7 +408,7 @@ describe("MessageComposer", () => { describe("when recording a voice broadcast and trying to start a voice message", () => { beforeEach(async () => { setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started); - wrapAndRender({ room }); + await wrapAndRender({ room }); await startVoiceMessage(); await waitEnoughCyclesForModal(); }); @@ -420,7 +422,7 @@ describe("MessageComposer", () => { describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => { beforeEach(async () => { setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped); - wrapAndRender({ room }); + await wrapAndRender({ room }); await startVoiceMessage(); await waitEnoughCyclesForModal(); }); @@ -436,7 +438,7 @@ describe("MessageComposer", () => { const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!); it("should not show the stickers button", async () => { - wrapAndRender({ room: localRoom }); + await wrapAndRender({ room: localRoom }); await act(async () => { await userEvent.click(screen.getByLabelText("More options")); }); @@ -448,7 +450,7 @@ describe("MessageComposer", () => { const room = mkStubRoom("!roomId:server", "Room 1", cli); const messageText = "Test Text"; await SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true); - const { renderResult, rawComponent } = wrapAndRender({ room }); + const { renderResult, rawComponent } = await wrapAndRender({ room }, true, false, undefined, true); const { unmount, rerender } = renderResult; await act(async () => { @@ -490,11 +492,12 @@ describe("MessageComposer", () => { }, 10000); }); -function wrapAndRender( +async function wrapAndRender( props: Partial> = {}, canSendMessages = true, narrow = false, tombstone?: MatrixEvent, + ignoreWaitForRender = false, ) { const mockClient = MatrixClientPeg.safeGet(); const roomId = "myroomid"; @@ -527,9 +530,15 @@ function wrapAndRender( ); + + const renderResult = render(getRawComponent(props, roomContext, mockClient)); + if (!ignoreWaitForRender && canSendMessages && !tombstone) { + await waitFor(() => expect(renderResult.getByRole("textbox")).toBeInTheDocument()); + } + return { rawComponent: getRawComponent(props, roomContext, mockClient), - renderResult: render(getRawComponent(props, roomContext, mockClient)), + renderResult, roomContext, }; } diff --git a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx index aa45525c4f..09cc76191e 100644 --- a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { fireEvent, render, waitFor } from "jest-matrix-react"; +import { fireEvent, render, waitFor, screen } from "jest-matrix-react"; import { IContent, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; @@ -369,12 +369,15 @@ describe("", () => { ); - const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { - return render(getRawComponent(props, roomContext, client)); + const getComponent = async (props = {}, roomContext = defaultRoomContext, client = mockClient) => { + const renderResult = render(getRawComponent(props, roomContext, client)); + // Wait for the composer to be rendered + await waitFor(() => expect(screen.getByRole("textbox")).toBeInTheDocument()); + return renderResult; }; - it("renders text and placeholder correctly", () => { - const { container } = getComponent({ placeholder: "placeholder string" }); + it("renders text and placeholder correctly", async () => { + const { container } = await getComponent({ placeholder: "placeholder string" }); expect(container.querySelectorAll('[aria-label="placeholder string"]')).toHaveLength(1); @@ -383,9 +386,9 @@ describe("", () => { expect(container.textContent).toBe("Test Text"); }); - it("correctly persists state to and from localStorage", () => { + it("correctly persists state to and from localStorage", async () => { const props = { replyToEvent: mockEvent }; - const { container, unmount, rerender } = getComponent(props); + const { container, unmount, rerender } = await getComponent(props); addTextToComposer(container, "Test Text"); @@ -403,7 +406,7 @@ describe("", () => { // ensure the correct model is re-loaded rerender(getRawComponent(props)); - expect(container.textContent).toBe("Test Text"); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent("Test Text")); expect(spyDispatcher).toHaveBeenCalledWith({ action: "reply_to_event", event: mockEvent, @@ -417,8 +420,8 @@ describe("", () => { expect(container.textContent).toBe(""); }); - it("persists state correctly without replyToEvent onbeforeunload", () => { - const { container } = getComponent(); + it("persists state correctly without replyToEvent onbeforeunload", async () => { + const { container } = await getComponent(); addTextToComposer(container, "Hello World"); @@ -437,7 +440,7 @@ describe("", () => { it("persists to session history upon sending", async () => { mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const { container } = getComponent({ replyToEvent: mockEvent }); + const { container } = await getComponent({ replyToEvent: mockEvent }); addTextToComposer(container, "This is a message"); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); @@ -458,7 +461,7 @@ describe("", () => { }); }); - it("correctly sends a message", () => { + it("correctly sends a message", async () => { mocked(doMaybeLocalRoomAction).mockImplementation( (roomId: string, fn: (actualRoomId: string) => Promise, _client?: MatrixClient) => { return fn(roomId); @@ -466,7 +469,7 @@ describe("", () => { ); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const { container } = getComponent(); + const { container } = await getComponent(); addTextToComposer(container, "test message"); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); @@ -495,7 +498,7 @@ describe("", () => { }); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const { container } = getComponent({ replyToEvent }); + const { container } = await getComponent({ replyToEvent }); addTextToComposer(container, "/tableflip"); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); @@ -516,7 +519,7 @@ describe("", () => { ); }); - it("shows chat effects on message sending", () => { + it("shows chat effects on message sending", async () => { mocked(doMaybeLocalRoomAction).mockImplementation( (roomId: string, fn: (actualRoomId: string) => Promise, _client?: MatrixClient) => { return fn(roomId); @@ -524,7 +527,7 @@ describe("", () => { ); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const { container } = getComponent(); + const { container } = await getComponent(); addTextToComposer(container, "🎉"); fireEvent.keyDown(container.querySelector(".mx_SendMessageComposer")!, { key: "Enter" }); @@ -538,7 +541,7 @@ describe("", () => { expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: `effects.confetti` }); }); - it("not to send chat effects on message sending for threads", () => { + it("not to send chat effects on message sending for threads", async () => { mocked(doMaybeLocalRoomAction).mockImplementation( (roomId: string, fn: (actualRoomId: string) => Promise, _client?: MatrixClient) => { return fn(roomId); @@ -546,7 +549,7 @@ describe("", () => { ); mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - const { container } = getComponent({ + const { container } = await getComponent({ relation: { rel_type: "m.thread", event_id: "$yolo", @@ -615,7 +618,8 @@ describe("", () => { , ); - + // Wait for the composer to be rendered + await waitFor(() => expect(screen.getByRole("textbox")).toBeInTheDocument()); const composer = container.querySelector(".mx_BasicMessageComposer_input")!; // Does not trigger on keydown as that'll cause false negatives for global shortcuts