From a0c35d088a37d1c9103bc7d250045b38350715dc Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 16 Sep 2022 11:10:33 +0200 Subject: [PATCH] Add Voice Broadcast labs setting and composer button (#9279) * Add Voice Broadcast labs setting and composer button * Implement strict typing * Extend MessageComposer-test * Extend tests * Revert some strict type fixex * Convert FEATURES to enum; change case * Use fake timers in MessageComposer-test --- res/css/views/rooms/_MessageComposer.pcss | 4 + res/img/element-icons/live.svg | 58 ++++ src/Modal.tsx | 2 +- .../views/location/LocationButton.tsx | 2 +- .../views/rooms/MessageComposer.tsx | 31 +- .../views/rooms/MessageComposerButtons.tsx | 38 ++- src/components/views/rooms/ReplyPreview.tsx | 4 +- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 11 + .../views/rooms/MessageComposer-test.tsx | 271 +++++++++++++++++- .../rooms/MessageComposerButtons-test.tsx | 43 ++- .../views/rooms/SendMessageComposer-test.tsx | 15 +- test/test-utils/composer.ts | 33 +++ 13 files changed, 469 insertions(+), 45 deletions(-) create mode 100644 res/img/element-icons/live.svg create mode 100644 test/test-utils/composer.ts diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 75f1487156..4284eac56d 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -245,6 +245,10 @@ limitations under the License. mask-image: url('$(res)/img/voip/mic-on-mask.svg'); } +.mx_MessageComposer_voiceBroadcast::before { + mask-image: url('$(res)/img/element-icons/live.svg'); +} + .mx_MessageComposer_emoji::before { mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); } diff --git a/res/img/element-icons/live.svg b/res/img/element-icons/live.svg new file mode 100644 index 0000000000..40a7a66677 --- /dev/null +++ b/res/img/element-icons/live.svg @@ -0,0 +1,58 @@ + + + + + + + + + + diff --git a/src/Modal.tsx b/src/Modal.tsx index af9ec22b69..465f3cdad2 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -103,7 +103,7 @@ export class ModalManager { } public createDialog( - Element: React.ComponentType, + Element: React.ComponentType, ...rest: ParametersWithoutFirst ) { return this.createDialogAsync(Promise.resolve(Element), ...rest); diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index b119268588..7efd350b6b 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -28,7 +28,7 @@ import LocationShareMenu from './LocationShareMenu'; interface IProps { roomId: string; sender: RoomMember; - menuPosition: AboveLeftOf; + menuPosition?: AboveLeftOf; relation?: IEventRelation; } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 239969cb82..fb8dbc827e 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -52,6 +52,7 @@ import MessageComposerButtons from './MessageComposerButtons'; import { ButtonEvent } from '../elements/AccessibleButton'; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom'; +import { Features } from '../../../settings/Settings'; let instanceCount = 0; @@ -89,10 +90,11 @@ interface IState { isStickerPickerOpen: boolean; showStickersButton: boolean; showPollsButton: boolean; + showVoiceBroadcastButton: boolean; } export default class MessageComposer extends React.Component { - private dispatcherRef: string; + private dispatcherRef?: string; private messageComposerInput = createRef(); private voiceRecordingButton = createRef(); private ref: React.RefObject = createRef(); @@ -114,17 +116,19 @@ export default class MessageComposer extends React.Component { this.state = { isComposerEmpty: true, haveRecording: false, - recordingTimeLeftSeconds: null, // when set to a number, shows a toast + recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast isMenuOpen: false, isStickerPickerOpen: false, showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"), showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"), + showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast), }; this.instanceId = instanceCount++; SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); + SettingsStore.monitorSetting(Features.VoiceBroadcast, null); } private get voiceRecording(): Optional { @@ -153,7 +157,7 @@ export default class MessageComposer extends React.Component { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); this.waitForOwnMember(); - UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current); + UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!); UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize); this.updateRecordingState(); // grab any cached recordings } @@ -199,13 +203,19 @@ export default class MessageComposer extends React.Component { } break; } + case Features.VoiceBroadcast: { + if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) { + this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue }); + } + break; + } } } } }; private waitForOwnMember() { - // if we have the member already, do that + // If we have the member already, do that const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); if (me) { this.setState({ me }); @@ -242,6 +252,7 @@ export default class MessageComposer extends React.Component { } const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')]; + dis.dispatch({ action: Action.ViewRoom, highlighted: true, @@ -426,8 +437,8 @@ export default class MessageComposer extends React.Component { } let recordingTooltip; - const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); - if (secondsLeft) { + if (this.state.recordingTimeLeftSeconds) { + const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); recordingTooltip = { showPollsButton={this.state.showPollsButton} showStickersButton={this.showStickersButton} toggleButtonMenu={this.toggleButtonMenu} + showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} + onStartVoiceBroadcastClick={() => { + // Sends a voice message. To be replaced by voice broadcast during development. + this.voiceRecordingButton.current?.onRecordStartEndClick(); + if (this.context.narrow) { + this.toggleButtonMenu(); + } + }} /> } { showSendButton && ( void; relation?: IEventRelation; setStickerPickerOpen: (isStickerPickerOpen: boolean) => void; @@ -53,6 +53,8 @@ interface IProps { showPollsButton: boolean; showStickersButton: boolean; toggleButtonMenu: () => void; + showVoiceBroadcastButton: boolean; + onStartVoiceBroadcastClick: () => void; } type OverflowMenuCloser = () => void; @@ -76,7 +78,8 @@ const MessageComposerButtons: React.FC = (props: IProps) => { uploadButton(), // props passed via UploadButtonContext showStickersButton(props), voiceRecordingButton(props, narrow), - props.showPollsButton && pollButton(room, props.relation), + startVoiceBroadcastButton(props), + props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, roomId, matrixClient), ]; } else { @@ -87,7 +90,8 @@ const MessageComposerButtons: React.FC = (props: IProps) => { moreButtons = [ showStickersButton(props), voiceRecordingButton(props, narrow), - props.showPollsButton && pollButton(room, props.relation), + startVoiceBroadcastButton(props), + props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, roomId, matrixClient), ]; } @@ -265,7 +269,7 @@ const UploadButton = () => { />; }; -function showStickersButton(props: IProps): ReactElement { +function showStickersButton(props: IProps): ReactElement | null { return ( props.showStickersButton ? = (props: IProps): ReactElement | null => { + return ( + props.showVoiceBroadcastButton + ? + : null + ); +}; + +function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null { // XXX: recording UI does not work well in narrow mode, so hide for now return ( narrow @@ -312,7 +330,7 @@ class PollButton extends React.PureComponent { this.context?.(); // close overflow menu const canSend = this.props.room.currentState.maySendEvent( M_POLL_START.name, - MatrixClientPeg.get().getUserId(), + MatrixClientPeg.get().getUserId()!, ); if (!canSend) { Modal.createDialog( @@ -362,14 +380,16 @@ function showLocationButton( room: Room, roomId: string, matrixClient: MatrixClient, -): ReactElement { +): ReactElement | null { + const sender = room.getMember(matrixClient.getUserId()!); + return ( - props.showLocationButton + props.showLocationButton && sender ? : null diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index 611c58f852..3bf39577d4 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -34,13 +34,13 @@ function cancelQuoting(context: TimelineRenderingType) { interface IProps { permalinkCreator: RoomPermalinkCreator; - replyToEvent: MatrixEvent; + replyToEvent?: MatrixEvent; } export default class ReplyPreview extends React.Component { public static contextType = RoomContext; - public render(): JSX.Element { + public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; return
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 214db0000c..e22e6b7499 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -911,6 +911,7 @@ "Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)", "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", + "Voice broadcast (under active development)": "Voice broadcast (under active development)", "Use new session manager (under active development)": "Use new session manager (under active development)", "Font size": "Font size", "Use custom size": "Use custom size", @@ -1813,6 +1814,7 @@ "Emoji": "Emoji", "Hide stickers": "Hide stickers", "Sticker": "Sticker", + "Voice broadcast": "Voice broadcast", "Voice Message": "Voice Message", "You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.", "Poll": "Poll", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1675968257..cb661b2169 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -101,6 +101,10 @@ export enum LabGroup { Developer, } +export enum Features { + VoiceBroadcast = "feature_voice_broadcast", +} + export const labGroupNames: Record = { [LabGroup.Messaging]: _td("Messaging"), [LabGroup.Profile]: _td("Profile"), @@ -435,6 +439,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Favourite Messages (under active development)"), default: false, }, + [Features.VoiceBroadcast]: { + isFeature: true, + labsGroup: LabGroup.Messaging, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Voice broadcast (under active development)"), + default: false, + }, "feature_new_device_manager": { isFeature: true, labsGroup: LabGroup.Experimental, diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index 2aa07fbeef..aa4245cebc 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -17,8 +17,8 @@ limitations under the License. import * as React from "react"; // eslint-disable-next-line deprecate/import import { mount, ReactWrapper } from "enzyme"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils"; import MessageComposer from "../../../../src/components/views/rooms/MessageComposer"; @@ -30,6 +30,15 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { LocalRoom } from "../../../../src/models/LocalRoom"; import MessageComposerButtons from "../../../../src/components/views/rooms/MessageComposerButtons"; +import { Features } from "../../../../src/settings/Settings"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import dis from "../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../src/dispatcher/actions"; +import { SendMessageComposer } from "../../../../src/components/views/rooms/SendMessageComposer"; +import { E2EStatus } from "../../../../src/utils/ShieldUtils"; +import { addTextToComposer } from "../../../test-utils/composer"; +import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore"; describe("MessageComposer", () => { stubClient(); @@ -54,7 +63,7 @@ describe("MessageComposer", () => { }); it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => { - const wrapper = wrapAndRender({ room }, true, mkEvent({ + const wrapper = wrapAndRender({ room }, true, false, mkEvent({ event: true, type: "m.room.tombstone", room: room.roomId, @@ -68,10 +77,258 @@ describe("MessageComposer", () => { expect(wrapper.find("MessageComposerButtons")).toHaveLength(0); expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1); }); + + describe("when receiving a »reply_to_event«", () => { + let wrapper: ReactWrapper; + let resizeNotifier: ResizeNotifier; + + beforeEach(() => { + jest.useFakeTimers(); + resizeNotifier = { + notifyTimelineHeightChanged: jest.fn(), + } as unknown as ResizeNotifier; + wrapper = wrapAndRender({ + room, + resizeNotifier, + }); + }); + + it("should call notifyTimelineHeightChanged() for the same context", () => { + dis.dispatch({ + action: "reply_to_event", + context: (wrapper.instance as unknown as MessageComposer).context, + }); + wrapper.update(); + + jest.advanceTimersByTime(150); + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled(); + }); + + it("should not call notifyTimelineHeightChanged() for a different context", () => { + dis.dispatch({ + action: "reply_to_event", + context: "test", + }); + wrapper.update(); + + jest.advanceTimersByTime(150); + expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled(); + }); + }); + + // test button display depending on settings + [ + { + setting: "MessageComposerInput.showStickersButton", + prop: "showStickersButton", + }, + { + setting: "MessageComposerInput.showPollsButton", + prop: "showPollsButton", + }, + { + setting: Features.VoiceBroadcast, + prop: "showVoiceBroadcastButton", + }, + ].forEach(({ setting, prop }) => { + [true, false].forEach((value: boolean) => { + describe(`when ${setting} = ${value}`, () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); + wrapper = wrapAndRender({ room }); + }); + + it(`should pass the prop ${prop} = ${value}`, () => { + expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(value); + }); + + describe(`and setting ${setting} to ${!value}`, () => { + beforeEach(async () => { + // simulate settings update + await SettingsStore.setValue(setting, null, SettingLevel.DEVICE, !value); + dis.dispatch({ + action: Action.SettingUpdated, + settingName: setting, + newValue: !value, + }, true); + wrapper.update(); + }); + + it(`should pass the prop ${prop} = ${!value}`, () => { + expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(!value); + }); + }); + }); + }); + }); + + it("should not render the send button", () => { + const wrapper = wrapAndRender({ room }); + expect(wrapper.find("SendButton")).toHaveLength(0); + }); + + describe("when a message has been entered", () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = wrapAndRender({ room }); + addTextToComposer(wrapper, "Hello"); + wrapper.update(); + }); + + it("should render the send button", () => { + expect(wrapper.find("SendButton")).toHaveLength(1); + }); + }); + + describe("UIStore interactions", () => { + let wrapper: ReactWrapper; + let resizeCallback: Function; + + beforeEach(() => { + jest.spyOn(UIStore.instance, "on").mockImplementation((_event: string, listener: Function): any => { + resizeCallback = listener; + }); + }); + + describe("when a non-resize event occurred in UIStore", () => { + let stateBefore: any; + + beforeEach(() => { + wrapper = wrapAndRender({ room }); + stateBefore = { ...wrapper.instance().state }; + resizeCallback("test", {}); + wrapper.update(); + }); + + it("should not change the state", () => { + expect(wrapper.instance().state).toEqual(stateBefore); + }); + }); + + describe("when a resize to narrow event occurred in UIStore", () => { + beforeEach(() => { + wrapper = wrapAndRender({ room }, true, true); + wrapper.setState({ + isMenuOpen: true, + isStickerPickerOpen: true, + }); + resizeCallback(UI_EVENTS.Resize, {}); + wrapper.update(); + }); + + it("isMenuOpen should be true", () => { + expect(wrapper.state("isMenuOpen")).toBe(true); + }); + + it("isStickerPickerOpen should be false", () => { + expect(wrapper.state("isStickerPickerOpen")).toBe(false); + }); + }); + + describe("when a resize to non-narrow event occurred in UIStore", () => { + beforeEach(() => { + wrapper = wrapAndRender({ room }, true, false); + wrapper.setState({ + isMenuOpen: true, + isStickerPickerOpen: true, + }); + resizeCallback(UI_EVENTS.Resize, {}); + wrapper.update(); + }); + + it("isMenuOpen should be false", () => { + expect(wrapper.state("isMenuOpen")).toBe(false); + }); + + it("isStickerPickerOpen should be false", () => { + expect(wrapper.state("isStickerPickerOpen")).toBe(false); + }); + }); + }); + + describe("when not replying to an event", () => { + it("should pass the expected placeholder to SendMessageComposer", () => { + const wrapper = wrapAndRender({ room }); + expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send a message…"); + }); + + it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => { + const wrapper = wrapAndRender({ + room, + e2eStatus: E2EStatus.Normal, + }); + expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send an encrypted message…"); + }); + }); + + describe("when replying to an event", () => { + let replyToEvent: MatrixEvent; + let props: Partial>; + + const checkPlaceholder = (expected: string) => { + it("should pass the expected placeholder to SendMessageComposer", () => { + const wrapper = wrapAndRender(props); + expect(wrapper.find(SendMessageComposer).props().placeholder).toBe(expected); + }); + }; + + const setEncrypted = () => { + beforeEach(() => { + props.e2eStatus = E2EStatus.Normal; + }); + }; + + beforeEach(() => { + replyToEvent = mkEvent({ + event: true, + type: MsgType.Text, + user: cli.getUserId(), + content: {}, + }); + + props = { + room, + replyToEvent, + }; + }); + + describe("without encryption", () => { + checkPlaceholder("Send a reply…"); + }); + + describe("with encryption", () => { + setEncrypted(); + checkPlaceholder("Send an encrypted reply…"); + }); + + describe("with a non-thread relation", () => { + beforeEach(() => { + props.relation = { rel_type: "test" }; + }); + + checkPlaceholder("Send a reply…"); + }); + + describe("that is a thread", () => { + beforeEach(() => { + props.relation = { rel_type: THREAD_RELATION_TYPE.name }; + }); + + checkPlaceholder("Reply to thread…"); + + describe("with encryption", () => { + setEncrypted(); + checkPlaceholder("Reply to encrypted thread…"); + }); + }); + }); }); describe("for a LocalRoom", () => { - const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()); + const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!); it("should pass the sticker picker disabled prop", () => { const wrapper = wrapAndRender({ room: localRoom }); @@ -83,6 +340,7 @@ describe("MessageComposer", () => { function wrapAndRender( props: Partial> = {}, canSendMessages = true, + narrow = false, tombstone?: MatrixEvent, ): ReactWrapper { const mockClient = MatrixClientPeg.get(); @@ -97,7 +355,10 @@ function wrapAndRender( }; const roomState = { - room, canSendMessages, tombstone, + room, + canSendMessages, + tombstone, + narrow, } as unknown as IRoomState; const defaultProps = { diff --git a/test/components/views/rooms/MessageComposerButtons-test.tsx b/test/components/views/rooms/MessageComposerButtons-test.tsx index 6666a03f1f..ade14f752e 100644 --- a/test/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/components/views/rooms/MessageComposerButtons-test.tsx @@ -34,7 +34,7 @@ const mockProps: React.ComponentProps = { addEmoji: () => false, haveRecording: false, isStickerPickerOpen: false, - menuPosition: null, + menuPosition: undefined, onRecordStartEndClick: () => {}, setStickerPickerOpen: () => {}, toggleButtonMenu: () => {}, @@ -44,11 +44,11 @@ describe("MessageComposerButtons", () => { it("Renders emoji and upload buttons in wide mode", () => { const buttons = wrapAndRender( , false, ); @@ -63,11 +63,11 @@ describe("MessageComposerButtons", () => { it("Renders other buttons in menu in wide mode", () => { const buttons = wrapAndRender( , false, ); @@ -88,11 +88,11 @@ describe("MessageComposerButtons", () => { it("Renders only some buttons in narrow mode", () => { const buttons = wrapAndRender( , true, ); @@ -106,11 +106,11 @@ describe("MessageComposerButtons", () => { it("Renders other buttons in menu (except voice messages) in narrow mode", () => { const buttons = wrapAndRender( , true, ); @@ -131,11 +131,11 @@ describe("MessageComposerButtons", () => { it('should render when asked to', () => { const buttons = wrapAndRender( , true, ); @@ -155,11 +155,11 @@ describe("MessageComposerButtons", () => { it('should not render when asked not to', () => { const buttons = wrapAndRender( , true, ); @@ -176,6 +176,35 @@ describe("MessageComposerButtons", () => { ]); }); }); + + describe("with showVoiceBroadcastButton = true", () => { + it("should render the »Voice broadcast« button", () => { + const buttons = wrapAndRender( + , + false, + ); + + expect(buttonLabels(buttons)).toEqual([ + "Emoji", + "Attachment", + "More options", + [ + "Sticker", + "Voice Message", + "Voice broadcast", + "Poll", + "Location", + ], + ]); + }); + }); }); function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper { diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index cb10032720..6f6f846c24 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -40,6 +40,7 @@ import { IRoomState } from "../../../../src/components/structures/RoomView"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { mockPlatformPeg } from "../../../test-utils/platform"; import { doMaybeLocalRoomAction } from "../../../../src/utils/local-room"; +import { addTextToComposer } from "../../../test-utils/composer"; jest.mock("../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), @@ -187,20 +188,6 @@ describe('', () => { spyDispatcher.mockReset(); }); - const addTextToComposer = (wrapper, text) => act(() => { - // couldn't get input event on contenteditable to work - // paste works without illegal private method access - const pasteEvent = { - clipboardData: { - types: [], - files: [], - getData: type => type === "text/plain" ? text : undefined, - }, - }; - wrapper.find('[role="textbox"]').simulate('paste', pasteEvent); - wrapper.update(); - }); - const defaultProps = { room: mockRoom, toggleStickerPickerOpen: jest.fn(), diff --git a/test/test-utils/composer.ts b/test/test-utils/composer.ts new file mode 100644 index 0000000000..abfb694d96 --- /dev/null +++ b/test/test-utils/composer.ts @@ -0,0 +1,33 @@ +/* +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. +*/ + +// eslint-disable-next-line deprecate/import +import { ReactWrapper } from "enzyme"; +import { act } from "react-dom/test-utils"; + +export const addTextToComposer = (wrapper: ReactWrapper, text: string) => act(() => { + // couldn't get input event on contenteditable to work + // paste works without illegal private method access + const pasteEvent = { + clipboardData: { + types: [], + files: [], + getData: type => type === "text/plain" ? text : undefined, + }, + }; + wrapper.find('[role="textbox"]').simulate('paste', pasteEvent); + wrapper.update(); +});