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
This commit is contained in:
parent
4a23630e06
commit
a0c35d088a
13 changed files with 469 additions and 45 deletions
|
@ -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<React.ComponentProps<typeof MessageComposer>>;
|
||||
|
||||
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<React.ComponentProps<typeof MessageComposer>> = {},
|
||||
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 = {
|
||||
|
|
|
@ -34,7 +34,7 @@ const mockProps: React.ComponentProps<typeof MessageComposerButtons> = {
|
|||
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(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={false}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
false,
|
||||
);
|
||||
|
@ -63,11 +63,11 @@ describe("MessageComposerButtons", () => {
|
|||
it("Renders other buttons in menu in wide mode", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={true}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
false,
|
||||
);
|
||||
|
@ -88,11 +88,11 @@ describe("MessageComposerButtons", () => {
|
|||
it("Renders only some buttons in narrow mode", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={false}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
@ -106,11 +106,11 @@ describe("MessageComposerButtons", () => {
|
|||
it("Renders other buttons in menu (except voice messages) in narrow mode", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={true}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
@ -131,11 +131,11 @@ describe("MessageComposerButtons", () => {
|
|||
it('should render when asked to', () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={true}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
@ -155,11 +155,11 @@ describe("MessageComposerButtons", () => {
|
|||
it('should not render when asked not to', () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={true}
|
||||
showLocationButton={true}
|
||||
showPollsButton={false} // !! the change from the alternate test
|
||||
showStickersButton={true}
|
||||
{...mockProps}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
@ -176,6 +176,35 @@ describe("MessageComposerButtons", () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with showVoiceBroadcastButton = true", () => {
|
||||
it("should render the »Voice broadcast« button", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
{...mockProps}
|
||||
isMenuOpen={true}
|
||||
showLocationButton={true}
|
||||
showPollsButton={true}
|
||||
showStickersButton={true}
|
||||
showVoiceBroadcastButton={true}
|
||||
/>,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(buttonLabels(buttons)).toEqual([
|
||||
"Emoji",
|
||||
"Attachment",
|
||||
"More options",
|
||||
[
|
||||
"Sticker",
|
||||
"Voice Message",
|
||||
"Voice broadcast",
|
||||
"Poll",
|
||||
"Location",
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper {
|
||||
|
|
|
@ -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('<SendMessageComposer/>', () => {
|
|||
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(),
|
||||
|
|
33
test/test-utils/composer.ts
Normal file
33
test/test-utils/composer.ts
Normal file
|
@ -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();
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue