Merge branch 'develop' into gsouquet/threads-forceenablelabsflag
This commit is contained in:
commit
d4f247d1fe
97 changed files with 3280 additions and 1325 deletions
|
@ -26,7 +26,7 @@ import {
|
|||
getBeaconInfoIdentifier,
|
||||
EventType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { ExtensibleEvent, MessageEvent, M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk";
|
||||
import { M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk";
|
||||
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
|
||||
import { mocked } from "jest-mock";
|
||||
import { act } from "@testing-library/react";
|
||||
|
@ -44,6 +44,7 @@ import { ReadPinsEventId } from "../../../../src/components/views/right_panel/ty
|
|||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||
import { createMessageEventContent } from "../../../test-utils/events";
|
||||
|
||||
jest.mock("../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn(),
|
||||
|
@ -64,7 +65,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does show copy link button when supplied a link", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const props = {
|
||||
link: "https://google.com/",
|
||||
};
|
||||
|
@ -75,7 +76,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show copy link button when not supplied a link", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const menu = createMenuWithContent(eventContent);
|
||||
const copyLinkButton = menu.find('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toHaveLength(0);
|
||||
|
@ -91,8 +92,8 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show pin option when user does not have rights to pin", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const event = new MatrixEvent(eventContent.serialize());
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
const room = makeDefaultRoom();
|
||||
// mock permission to disallow adding pinned messages to room
|
||||
|
@ -116,8 +117,12 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show pin option when pinning feature is disabled", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
const room = makeDefaultRoom();
|
||||
// mock permission to allow adding pinned messages to room
|
||||
|
@ -131,8 +136,12 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("shows pin option when pinning feature is enabled", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
const room = makeDefaultRoom();
|
||||
// mock permission to allow adding pinned messages to room
|
||||
|
@ -145,8 +154,12 @@ describe("MessageContextMenu", () => {
|
|||
|
||||
it("pins event on pin option click", () => {
|
||||
const onFinished = jest.fn();
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = makeDefaultRoom();
|
||||
|
@ -188,8 +201,12 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("unpins event on pin option click when event is pinned", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = makeDefaultRoom();
|
||||
|
@ -231,7 +248,7 @@ describe("MessageContextMenu", () => {
|
|||
|
||||
describe("message forwarding", () => {
|
||||
it("allows forwarding a room message", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const menu = createMenuWithContent(eventContent);
|
||||
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(1);
|
||||
});
|
||||
|
@ -335,7 +352,7 @@ describe("MessageContextMenu", () => {
|
|||
|
||||
describe("open as map link", () => {
|
||||
it("does not allow opening a plain message in open street maps", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const menu = createMenuWithContent(eventContent);
|
||||
expect(menu.find('a[aria-label="Open in OpenStreetMap"]')).toHaveLength(0);
|
||||
});
|
||||
|
@ -380,7 +397,7 @@ describe("MessageContextMenu", () => {
|
|||
describe("right click", () => {
|
||||
it("copy button does work as expected", () => {
|
||||
const text = "hello";
|
||||
const eventContent = MessageEvent.from(text);
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue(text);
|
||||
|
||||
const menu = createRightClickMenuWithContent(eventContent);
|
||||
|
@ -391,7 +408,7 @@ describe("MessageContextMenu", () => {
|
|||
|
||||
it("copy button is not shown when there is nothing to copy", () => {
|
||||
const text = "hello";
|
||||
const eventContent = MessageEvent.from(text);
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue("");
|
||||
|
||||
const menu = createRightClickMenuWithContent(eventContent);
|
||||
|
@ -400,7 +417,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("shows edit button when we can edit", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(true);
|
||||
|
||||
const menu = createRightClickMenuWithContent(eventContent);
|
||||
|
@ -409,7 +426,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show edit button when we cannot edit", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(false);
|
||||
|
||||
const menu = createRightClickMenuWithContent(eventContent);
|
||||
|
@ -418,7 +435,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("shows reply button when we can reply", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
|
@ -429,11 +446,11 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show reply button when we cannot reply", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
const unsentMessage = new MatrixEvent(eventContent.serialize());
|
||||
const unsentMessage = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
// queued messages are not actionable
|
||||
unsentMessage.setStatus(EventStatus.QUEUED);
|
||||
|
||||
|
@ -443,7 +460,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("shows react button when we can react", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: true,
|
||||
};
|
||||
|
@ -454,7 +471,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show react button when we cannot react", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: false,
|
||||
};
|
||||
|
@ -465,8 +482,8 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("shows view in room button when the event is a thread root", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
mxEvent.getThread = () => ({ rootEvent: mxEvent } as Thread);
|
||||
const props = {
|
||||
rightClick: true,
|
||||
|
@ -481,7 +498,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("does not show view in room button when the event is not a thread root", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
|
||||
const menu = createRightClickMenuWithContent(eventContent);
|
||||
const reactButton = menu.find('div[aria-label="View in room"]');
|
||||
|
@ -489,8 +506,8 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
|
||||
it("creates a new thread on reply in thread click", () => {
|
||||
const eventContent = MessageEvent.from("hello");
|
||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
const context = {
|
||||
|
@ -513,7 +530,7 @@ describe("MessageContextMenu", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function createRightClickMenuWithContent(eventContent: ExtensibleEvent, context?: Partial<IRoomState>): ReactWrapper {
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): ReactWrapper {
|
||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
|
@ -522,11 +539,13 @@ function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState
|
|||
}
|
||||
|
||||
function createMenuWithContent(
|
||||
eventContent: ExtensibleEvent,
|
||||
eventContent: object,
|
||||
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
|
||||
context?: Partial<IRoomState>,
|
||||
): ReactWrapper {
|
||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
||||
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||
// test is for the Message context menu, it's a fairly safe assumption.
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
return createMenu(mxEvent, props, context);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,27 +21,7 @@ exports[`RoomGeneralContextMenu renders an empty context menu for archived rooms
|
|||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-label="Mark as read"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_item"
|
||||
role="menuitemcheckbox"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Mark as read
|
||||
</span>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_IconizedContextMenu_unchecked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
|
@ -88,27 +68,7 @@ exports[`RoomGeneralContextMenu renders the default context menu 1`] = `
|
|||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-label="Mark as read"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_item"
|
||||
role="menuitemcheckbox"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconMarkAsRead"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Mark as read
|
||||
</span>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_IconizedContextMenu_unchecked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
|
|
54
test/components/views/elements/Field-test.tsx
Normal file
54
test/components/views/elements/Field-test.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import Field from "../../../../src/components/views/elements/Field";
|
||||
|
||||
describe("Field", () => {
|
||||
describe("Placeholder", () => {
|
||||
it("Should display a placeholder", async () => {
|
||||
// When
|
||||
const { rerender } = render(<Field value="" placeholder="my placeholder" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my placeholder");
|
||||
|
||||
// When
|
||||
rerender(<Field value="" placeholder="" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "");
|
||||
});
|
||||
|
||||
it("Should display label as placeholder", async () => {
|
||||
// When
|
||||
render(<Field value="" label="my label" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my label");
|
||||
});
|
||||
|
||||
it("Should not display a placeholder", async () => {
|
||||
// When
|
||||
render(<Field value="" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PollCreateDialog renders a blank poll 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_1"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_1"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value=""><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" aria-disabled="true" disabled="" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
exports[`PollCreateDialog renders a blank poll 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_1"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_1"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value=""><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" aria-disabled="true" disabled="" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
|
||||
exports[`PollCreateDialog renders a question and some options 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_4"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_4"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="How many turnips is the optimal number?"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="As many as my neighbour"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="The question is meaningless"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_2" maxlength="340" label="Option 3" placeholder="Write an option" type="text" value="Mu"><label for="pollcreate_option_2">Option 3</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
exports[`PollCreateDialog renders a question and some options 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_4"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_4"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="How many turnips is the optimal number?"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="As many as my neighbour"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="The question is meaningless"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_2" maxlength="340" label="Option 3" placeholder="Write an option" type="text" value="Mu"><label for="pollcreate_option_2">Option 3</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
|
||||
exports[`PollCreateDialog renders info from a previous event 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Edit poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_5"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_5"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="Poll Q"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="Answer 1"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="Answer 2"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Done</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
exports[`PollCreateDialog renders info from a previous event 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Edit poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_5"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_5"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="Poll Q"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="Answer 1"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="Answer 2"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Done</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||
|
|
|
@ -15,15 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||
import React from "react";
|
||||
|
||||
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { mkEvent, stubClient } from "../../../test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("RoomHeaderButtons-test.tsx", function () {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
|
@ -35,6 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
|||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
@ -48,12 +52,12 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
|||
return render(<RoomHeaderButtons room={room} excludedRightPanelPhaseButtons={[]} />);
|
||||
}
|
||||
|
||||
function getThreadButton(container) {
|
||||
function getThreadButton(container: HTMLElement) {
|
||||
return container.querySelector(".mx_RightPanel_threadsButton");
|
||||
}
|
||||
|
||||
function isIndicatorOfType(container, type: "red" | "gray") {
|
||||
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator").className.includes(type);
|
||||
function isIndicatorOfType(container: HTMLElement, type: "red" | "gray" | "bold") {
|
||||
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")!.className.includes(type);
|
||||
}
|
||||
|
||||
it("shows the thread button", () => {
|
||||
|
@ -76,7 +80,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
|||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
});
|
||||
|
||||
it("room wide notification does not change the thread button", () => {
|
||||
it("thread notification does change the thread button", () => {
|
||||
const { container } = getComponent(room);
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
||||
|
@ -91,6 +95,85 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
|||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
});
|
||||
|
||||
it("thread activity does change the thread button", async () => {
|
||||
const { container } = getComponent(room);
|
||||
|
||||
// Thread activity should appear on the icon.
|
||||
const { rootEvent, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: client.getUserId()!,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
});
|
||||
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||
|
||||
// Sending the last event should clear the notification.
|
||||
let event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: client.getUserId()!,
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Test",
|
||||
"m.relates_to": {
|
||||
event_id: rootEvent.getId(),
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
await expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
|
||||
// Mark it as unread again.
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Test",
|
||||
"m.relates_to": {
|
||||
event_id: rootEvent.getId(),
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||
|
||||
// Sending a read receipt on an earlier event shouldn't do anything.
|
||||
let receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: room.roomId,
|
||||
content: {
|
||||
[events.at(-1)!.getId()!]: {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||
|
||||
// Sending a receipt on the latest event should clear the notification.
|
||||
receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: room.roomId,
|
||||
content: {
|
||||
[event.getId()!]: {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not explode without a room", () => {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
||||
expect(() => getComponent()).not.toThrow();
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -141,9 +141,10 @@ describe("EventTile", () => {
|
|||
mxEvent = rootEvent;
|
||||
});
|
||||
|
||||
it("shows an unread notification bage", () => {
|
||||
it("shows an unread notification badge", () => {
|
||||
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
||||
|
||||
// By default, the thread will assume it is read.
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
|
|
|
@ -15,15 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { EventType, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils";
|
||||
import MessageComposer, {
|
||||
MessageComposer as MessageComposerClass,
|
||||
} from "../../../../src/components/views/rooms/MessageComposer";
|
||||
import {
|
||||
createTestClient,
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
mkEvent,
|
||||
mkStubRoom,
|
||||
mockPlatformPeg,
|
||||
stubClient,
|
||||
} from "../../../test-utils";
|
||||
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||
|
@ -31,42 +37,108 @@ import { IRoomState } from "../../../../src/components/structures/RoomView";
|
|||
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 { addTextToComposerEnzyme } from "../../../test-utils/composer";
|
||||
import { addTextToComposerRTL } from "../../../test-utils/composer";
|
||||
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
|
||||
import { SendWysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
import Modal from "../../../../src/Modal";
|
||||
|
||||
jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({
|
||||
SendWysiwygComposer: jest.fn().mockImplementation(() => <div data-testid="wysiwyg-composer" />),
|
||||
}));
|
||||
|
||||
const openStickerPicker = async (): Promise<void> => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
await userEvent.click(screen.getByLabelText("Sticker"));
|
||||
});
|
||||
};
|
||||
|
||||
const startVoiceMessage = async (): Promise<void> => {
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
await userEvent.click(screen.getByLabelText("Voice Message"));
|
||||
});
|
||||
};
|
||||
|
||||
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
|
||||
const recording = new VoiceBroadcastRecording(
|
||||
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
|
||||
MatrixClientPeg.get(),
|
||||
state,
|
||||
);
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||
};
|
||||
|
||||
const waitForModal = async (): Promise<void> => {
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
};
|
||||
|
||||
const shouldClearModal = async (): Promise<void> => {
|
||||
afterEach(async () => {
|
||||
Modal.closeCurrentModal("force");
|
||||
await waitForModal();
|
||||
});
|
||||
};
|
||||
|
||||
const expectVoiceMessageRecordingTriggered = (): void => {
|
||||
// Checking for the voice message dialog text, if no mic can be found.
|
||||
// By this we know at least that starting a voice message was triggered.
|
||||
expect(screen.getByText("No microphone found")).toBeInTheDocument();
|
||||
};
|
||||
|
||||
describe("MessageComposer", () => {
|
||||
stubClient();
|
||||
const cli = createTestClient();
|
||||
|
||||
filterConsole("Starting load of AsyncWrapper for modal");
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
|
||||
|
||||
// restore settings
|
||||
act(() => {
|
||||
[
|
||||
"MessageComposerInput.showStickersButton",
|
||||
"MessageComposerInput.showPollsButton",
|
||||
Features.VoiceBroadcast,
|
||||
"feature_wysiwyg_composer",
|
||||
].forEach((setting: string): void => {
|
||||
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a Room", () => {
|
||||
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
||||
|
||||
it("Renders a SendMessageComposer and MessageComposerButtons by default", () => {
|
||||
const wrapper = wrapAndRender({ room });
|
||||
|
||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(1);
|
||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(1);
|
||||
wrapAndRender({ room });
|
||||
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => {
|
||||
const wrapper = wrapAndRender({ room }, false);
|
||||
|
||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
|
||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
|
||||
expect(wrapper.find(".mx_MessageComposer_noperm_error")).toHaveLength(1);
|
||||
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", () => {
|
||||
const wrapper = wrapAndRender(
|
||||
wrapAndRender(
|
||||
{ room },
|
||||
true,
|
||||
false,
|
||||
|
@ -81,13 +153,12 @@ describe("MessageComposer", () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
|
||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
|
||||
expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1);
|
||||
expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("This room has been replaced and is no longer active.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when receiving a »reply_to_event«", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
let roomContext: IRoomState;
|
||||
let resizeNotifier: ResizeNotifier;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -95,18 +166,17 @@ describe("MessageComposer", () => {
|
|||
resizeNotifier = {
|
||||
notifyTimelineHeightChanged: jest.fn(),
|
||||
} as unknown as ResizeNotifier;
|
||||
wrapper = wrapAndRender({
|
||||
roomContext = wrapAndRender({
|
||||
room,
|
||||
resizeNotifier,
|
||||
});
|
||||
}).roomContext;
|
||||
});
|
||||
|
||||
it("should call notifyTimelineHeightChanged() for the same context", () => {
|
||||
dis.dispatch({
|
||||
action: "reply_to_event",
|
||||
context: (wrapper.instance as unknown as MessageComposerClass).context,
|
||||
context: roomContext.timelineRenderingType,
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
jest.advanceTimersByTime(150);
|
||||
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled();
|
||||
|
@ -117,7 +187,6 @@ describe("MessageComposer", () => {
|
|||
action: "reply_to_event",
|
||||
context: "test",
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
jest.advanceTimersByTime(150);
|
||||
expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled();
|
||||
|
@ -128,28 +197,33 @@ describe("MessageComposer", () => {
|
|||
[
|
||||
{
|
||||
setting: "MessageComposerInput.showStickersButton",
|
||||
prop: "showStickersButton",
|
||||
buttonLabel: "Sticker",
|
||||
},
|
||||
{
|
||||
setting: "MessageComposerInput.showPollsButton",
|
||||
prop: "showPollsButton",
|
||||
buttonLabel: "Poll",
|
||||
},
|
||||
{
|
||||
setting: Features.VoiceBroadcast,
|
||||
prop: "showVoiceBroadcastButton",
|
||||
buttonLabel: "Voice broadcast",
|
||||
},
|
||||
].forEach(({ setting, prop }) => {
|
||||
].forEach(({ setting, buttonLabel }) => {
|
||||
[true, false].forEach((value: boolean) => {
|
||||
describe(`when ${setting} = ${value}`, () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
|
||||
wrapper = wrapAndRender({ room });
|
||||
wrapAndRender({ room });
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
});
|
||||
});
|
||||
|
||||
it(`should pass the prop ${prop} = ${value}`, () => {
|
||||
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(value);
|
||||
it(`should${value || "not"} display the button`, () => {
|
||||
if (value) {
|
||||
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
describe(`and setting ${setting} to ${!value}`, () => {
|
||||
|
@ -164,11 +238,14 @@ describe("MessageComposer", () => {
|
|||
},
|
||||
true,
|
||||
);
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it(`should pass the prop ${prop} = ${!value}`, () => {
|
||||
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(!value);
|
||||
it(`should${!value || "not"} display the button`, () => {
|
||||
if (!value) {
|
||||
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -176,26 +253,22 @@ describe("MessageComposer", () => {
|
|||
});
|
||||
|
||||
it("should not render the send button", () => {
|
||||
const wrapper = wrapAndRender({ room });
|
||||
expect(wrapper.find("SendButton")).toHaveLength(0);
|
||||
wrapAndRender({ room });
|
||||
expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when a message has been entered", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = wrapAndRender({ room });
|
||||
addTextToComposerEnzyme(wrapper, "Hello");
|
||||
wrapper.update();
|
||||
beforeEach(async () => {
|
||||
const renderResult = wrapAndRender({ room }).renderResult;
|
||||
await addTextToComposerRTL(renderResult, "Hello");
|
||||
});
|
||||
|
||||
it("should render the send button", () => {
|
||||
expect(wrapper.find("SendButton")).toHaveLength(1);
|
||||
expect(screen.getByLabelText("Send message")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UIStore interactions", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
let resizeCallback: Function;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -205,74 +278,74 @@ describe("MessageComposer", () => {
|
|||
});
|
||||
|
||||
describe("when a non-resize event occurred in UIStore", () => {
|
||||
let stateBefore: any;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = wrapAndRender({ room }).children();
|
||||
stateBefore = { ...wrapper.instance().state };
|
||||
beforeEach(async () => {
|
||||
wrapAndRender({ room });
|
||||
await openStickerPicker();
|
||||
resizeCallback("test", {});
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it("should not change the state", () => {
|
||||
expect(wrapper.instance().state).toEqual(stateBefore);
|
||||
it("should still display the sticker picker", () => {
|
||||
expect(screen.getByText("You don't currently have any stickerpacks enabled")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a resize to narrow event occurred in UIStore", () => {
|
||||
beforeEach(() => {
|
||||
wrapper = wrapAndRender({ room }, true, true).children();
|
||||
|
||||
wrapper.setState({
|
||||
isMenuOpen: true,
|
||||
isStickerPickerOpen: true,
|
||||
});
|
||||
beforeEach(async () => {
|
||||
wrapAndRender({ room }, true, true);
|
||||
await openStickerPicker();
|
||||
resizeCallback(UI_EVENTS.Resize, {});
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it("isMenuOpen should be true", () => {
|
||||
expect(wrapper.state("isMenuOpen")).toBe(true);
|
||||
it("should close the menu", () => {
|
||||
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("isStickerPickerOpen should be false", () => {
|
||||
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
|
||||
it("should not show the attachment button", () => {
|
||||
expect(screen.queryByLabelText("Attachment")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close the sticker picker", () => {
|
||||
expect(
|
||||
screen.queryByText("You don't currently have any stickerpacks enabled"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a resize to non-narrow event occurred in UIStore", () => {
|
||||
beforeEach(() => {
|
||||
wrapper = wrapAndRender({ room }, true, false).children();
|
||||
wrapper.setState({
|
||||
isMenuOpen: true,
|
||||
isStickerPickerOpen: true,
|
||||
});
|
||||
beforeEach(async () => {
|
||||
wrapAndRender({ room }, true, false);
|
||||
await openStickerPicker();
|
||||
resizeCallback(UI_EVENTS.Resize, {});
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it("isMenuOpen should be false", () => {
|
||||
expect(wrapper.state("isMenuOpen")).toBe(false);
|
||||
it("should close the menu", () => {
|
||||
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("isStickerPickerOpen should be false", () => {
|
||||
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
|
||||
it("should show the attachment button", () => {
|
||||
expect(screen.getByLabelText("Attachment")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close the sticker picker", () => {
|
||||
expect(
|
||||
screen.queryByText("You don't currently have any stickerpacks enabled"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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…");
|
||||
wrapAndRender({ room });
|
||||
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => {
|
||||
const wrapper = wrapAndRender({
|
||||
wrapAndRender({
|
||||
room,
|
||||
e2eStatus: E2EStatus.Normal,
|
||||
});
|
||||
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send an encrypted message…");
|
||||
expect(screen.getByLabelText("Send an encrypted message…")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -282,8 +355,8 @@ describe("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);
|
||||
wrapAndRender(props);
|
||||
expect(screen.getByLabelText(expected)).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -296,7 +369,7 @@ describe("MessageComposer", () => {
|
|||
beforeEach(() => {
|
||||
replyToEvent = mkEvent({
|
||||
event: true,
|
||||
type: MsgType.Text,
|
||||
type: EventType.RoomMessage,
|
||||
user: cli.getUserId(),
|
||||
content: {},
|
||||
});
|
||||
|
@ -337,25 +410,72 @@ describe("MessageComposer", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking start a voice message", () => {
|
||||
beforeEach(async () => {
|
||||
wrapAndRender({ room });
|
||||
await startVoiceMessage();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
shouldClearModal();
|
||||
|
||||
it("should try to start a voice message", () => {
|
||||
expectVoiceMessageRecordingTriggered();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when recording a voice broadcast and trying to start a voice message", () => {
|
||||
beforeEach(async () => {
|
||||
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
|
||||
wrapAndRender({ room });
|
||||
await startVoiceMessage();
|
||||
await waitForModal();
|
||||
});
|
||||
|
||||
shouldClearModal();
|
||||
|
||||
it("should not start a voice message and display the info dialog", async () => {
|
||||
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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 startVoiceMessage();
|
||||
await waitForModal();
|
||||
});
|
||||
|
||||
shouldClearModal();
|
||||
|
||||
it("should try to start a voice message and should not display the info dialog", async () => {
|
||||
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
|
||||
expectVoiceMessageRecordingTriggered();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a LocalRoom", () => {
|
||||
const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
|
||||
|
||||
it("should pass the sticker picker disabled prop", () => {
|
||||
const wrapper = wrapAndRender({ room: localRoom });
|
||||
expect(wrapper.find(MessageComposerButtons).props().showStickersButton).toBe(false);
|
||||
it("should not show the stickers button", async () => {
|
||||
wrapAndRender({ room: localRoom });
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
});
|
||||
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render SendWysiwygComposer", () => {
|
||||
it("should render SendWysiwygComposer when enabled", () => {
|
||||
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
||||
|
||||
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
|
||||
const wrapper = wrapAndRender({ room });
|
||||
|
||||
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, false);
|
||||
expect(wrapper.find(SendWysiwygComposer)).toBeTruthy();
|
||||
wrapAndRender({ room });
|
||||
expect(screen.getByTestId("wysiwyg-composer")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -364,7 +484,7 @@ function wrapAndRender(
|
|||
canSendMessages = true,
|
||||
narrow = false,
|
||||
tombstone?: MatrixEvent,
|
||||
): ReactWrapper {
|
||||
) {
|
||||
const mockClient = MatrixClientPeg.get();
|
||||
const roomId = "myroomid";
|
||||
const room: any = props.room || {
|
||||
|
@ -376,7 +496,7 @@ function wrapAndRender(
|
|||
},
|
||||
};
|
||||
|
||||
const roomState = {
|
||||
const roomContext = {
|
||||
room,
|
||||
canSendMessages,
|
||||
tombstone,
|
||||
|
@ -389,11 +509,14 @@ function wrapAndRender(
|
|||
permalinkCreator: new RoomPermalinkCreator(room),
|
||||
};
|
||||
|
||||
return mount(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider value={roomState}>
|
||||
<MessageComposer {...defaultProps} {...props} />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
return {
|
||||
renderResult: render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider value={roomContext}>
|
||||
<MessageComposer {...defaultProps} {...props} />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
),
|
||||
roomContext,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,13 +17,15 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import "jest-mock";
|
||||
import { screen, act, render } from "@testing-library/react";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventStatus } from "matrix-js-sdk/src/models/event-status";
|
||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||
|
||||
import { mkThread } from "../../../../test-utils/threads";
|
||||
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
||||
import { mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
||||
import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
||||
|
||||
|
@ -34,28 +36,57 @@ jest.mock("../../../../../src/RoomNotifs", () => ({
|
|||
}));
|
||||
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let THREAD_ID;
|
||||
let THREAD_ID: string;
|
||||
|
||||
describe("UnreadNotificationBadge", () => {
|
||||
let mockClient: MatrixClient;
|
||||
stubClient();
|
||||
const client = MatrixClientPeg.get();
|
||||
let room: Room;
|
||||
|
||||
function getComponent(threadId?: string) {
|
||||
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const receipt = new MatrixEvent({
|
||||
type: "m.receipt",
|
||||
room_id: room.roomId,
|
||||
content: {
|
||||
"$event0:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 1, thread_id: "$otherthread:localhost" },
|
||||
},
|
||||
},
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
[client.getUserId()!]: { ts: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
room.addReceipt(receipt);
|
||||
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
const { rootEvent } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: client.getUserId()!,
|
||||
participantUserIds: [client.getUserId()!],
|
||||
});
|
||||
THREAD_ID = rootEvent.getId()!;
|
||||
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
|
@ -125,4 +156,34 @@ describe("UnreadNotificationBadge", () => {
|
|||
const { container } = render(getComponent());
|
||||
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
||||
});
|
||||
|
||||
it("activity renders unread notification badge", () => {
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
// Add another event on the thread which is not sent by us.
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@alice:server.org",
|
||||
room: room.roomId,
|
||||
content: {
|
||||
"msgtype": MsgType.Text,
|
||||
"body": "Hello from Bob",
|
||||
"m.relates_to": {
|
||||
event_id: THREAD_ID,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
ts: 5,
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
});
|
||||
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,273 +0,0 @@
|
|||
/*
|
||||
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 React from "react";
|
||||
import ReactTestUtils from "react-dom/test-utils";
|
||||
import ReactDOM from "react-dom";
|
||||
import { PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import * as TestUtils from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import dis from "../../../../src/dispatcher/dispatcher";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
|
||||
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
||||
import RoomList from "../../../../src/components/views/rooms/RoomList";
|
||||
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
|
||||
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
||||
|
||||
function generateRoomId() {
|
||||
return "!" + Math.random().toString().slice(2, 10) + ":domain";
|
||||
}
|
||||
|
||||
describe("RoomList", () => {
|
||||
function createRoom(opts) {
|
||||
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
|
||||
// The room list now uses getPendingEvents(), so we need a detached ordering.
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
if (opts) {
|
||||
Object.assign(room, opts);
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
let parentDiv = null;
|
||||
let root = null;
|
||||
const myUserId = "@me:domain";
|
||||
|
||||
const movingRoomId = "!someroomid";
|
||||
let movingRoom: Room | undefined;
|
||||
let otherRoom: Room | undefined;
|
||||
|
||||
let myMember: RoomMember | undefined;
|
||||
let myOtherMember: RoomMember | undefined;
|
||||
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(myUserId),
|
||||
getRooms: jest.fn(),
|
||||
getVisibleRooms: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
onKeyDown: jest.fn(),
|
||||
onFocus: jest.fn(),
|
||||
onBlur: jest.fn(),
|
||||
onResize: jest.fn(),
|
||||
resizeNotifier: {} as unknown as ResizeNotifier,
|
||||
isMinimized: false,
|
||||
activeSpace: "",
|
||||
};
|
||||
|
||||
beforeEach(async function (done) {
|
||||
RoomListStoreClass.TEST_MODE = true;
|
||||
jest.clearAllMocks();
|
||||
|
||||
client.credentials = { userId: myUserId };
|
||||
|
||||
DMRoomMap.makeShared();
|
||||
|
||||
parentDiv = document.createElement("div");
|
||||
document.body.appendChild(parentDiv);
|
||||
|
||||
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
||||
root = ReactDOM.render(<WrappedRoomList {...defaultProps} />, parentDiv);
|
||||
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
||||
|
||||
movingRoom = createRoom({ name: "Moving room" });
|
||||
expect(movingRoom.roomId).not.toBe(null);
|
||||
|
||||
// Mock joined member
|
||||
myMember = new RoomMember(movingRoomId, myUserId);
|
||||
myMember.membership = "join";
|
||||
movingRoom.updateMyMembership("join");
|
||||
movingRoom.getMember = (userId) =>
|
||||
({
|
||||
[client.credentials.userId]: myMember,
|
||||
}[userId]);
|
||||
|
||||
otherRoom = createRoom({ name: "Other room" });
|
||||
myOtherMember = new RoomMember(otherRoom.roomId, myUserId);
|
||||
myOtherMember.membership = "join";
|
||||
otherRoom.updateMyMembership("join");
|
||||
otherRoom.getMember = (userId) =>
|
||||
({
|
||||
[client.credentials.userId]: myOtherMember,
|
||||
}[userId]);
|
||||
|
||||
// Mock the matrix client
|
||||
const mockRooms = [
|
||||
movingRoom,
|
||||
otherRoom,
|
||||
createRoom({ tags: { "m.favourite": { order: 0.1 } }, name: "Some other room" }),
|
||||
createRoom({ tags: { "m.favourite": { order: 0.2 } }, name: "Some other room 2" }),
|
||||
createRoom({ tags: { "m.lowpriority": {} }, name: "Some unimportant room" }),
|
||||
createRoom({ tags: { "custom.tag": {} }, name: "Some room customly tagged" }),
|
||||
];
|
||||
client.getRooms.mockReturnValue(mockRooms);
|
||||
client.getVisibleRooms.mockReturnValue(mockRooms);
|
||||
|
||||
const roomMap = {};
|
||||
client.getRooms().forEach((r) => {
|
||||
roomMap[r.roomId] = r;
|
||||
});
|
||||
|
||||
client.getRoom.mockImplementation((roomId) => roomMap[roomId]);
|
||||
|
||||
// Now that everything has been set up, prepare and update the store
|
||||
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(async (done) => {
|
||||
if (parentDiv) {
|
||||
ReactDOM.unmountComponentAtNode(parentDiv);
|
||||
parentDiv.remove();
|
||||
parentDiv = null;
|
||||
}
|
||||
|
||||
await RoomListLayoutStore.instance.resetLayouts();
|
||||
await (RoomListStore.instance as RoomListStoreClass).resetStore();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
function expectRoomInSubList(room, subListTest) {
|
||||
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSublist);
|
||||
const containingSubList = subLists.find(subListTest);
|
||||
|
||||
let expectedRoomTile;
|
||||
try {
|
||||
const roomTiles = ReactTestUtils.scryRenderedComponentsWithType(containingSubList, RoomTile);
|
||||
console.info({ roomTiles: roomTiles.length });
|
||||
expectedRoomTile = roomTiles.find((tile) => tile.props.room === room);
|
||||
} catch (err) {
|
||||
// truncate the error message because it's spammy
|
||||
err.message =
|
||||
"Error finding RoomTile for " +
|
||||
room.roomId +
|
||||
" in " +
|
||||
subListTest +
|
||||
": " +
|
||||
err.message.split("componentType")[0] +
|
||||
"...";
|
||||
throw err;
|
||||
}
|
||||
|
||||
expect(expectedRoomTile).toBeTruthy();
|
||||
expect(expectedRoomTile.props.room).toBe(room);
|
||||
}
|
||||
|
||||
function expectCorrectMove(oldTagId, newTagId) {
|
||||
const getTagSubListTest = (tagId) => {
|
||||
return (s) => s.props.tagId === tagId;
|
||||
};
|
||||
|
||||
// Default to finding the destination sublist with newTag
|
||||
const destSubListTest = getTagSubListTest(newTagId);
|
||||
const srcSubListTest = getTagSubListTest(oldTagId);
|
||||
|
||||
// Set up the room that will be moved such that it has the correct state for a room in
|
||||
// the section for oldTagId
|
||||
if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) {
|
||||
movingRoom.tags = { [oldTagId]: {} };
|
||||
} else if (oldTagId === DefaultTagID.DM) {
|
||||
// Mock inverse m.direct
|
||||
// @ts-ignore forcing private property
|
||||
DMRoomMap.shared().roomToUser = {
|
||||
[movingRoom.roomId]: "@someotheruser:domain",
|
||||
};
|
||||
}
|
||||
|
||||
dis.dispatch({ action: "MatrixActions.sync", prevState: null, state: "PREPARED", matrixClient: client });
|
||||
|
||||
expectRoomInSubList(movingRoom, srcSubListTest);
|
||||
|
||||
dis.dispatch({
|
||||
action: "RoomListActions.tagRoom.pending",
|
||||
request: {
|
||||
oldTagId,
|
||||
newTagId,
|
||||
room: movingRoom,
|
||||
},
|
||||
});
|
||||
|
||||
expectRoomInSubList(movingRoom, destSubListTest);
|
||||
}
|
||||
|
||||
function itDoesCorrectOptimisticUpdatesForDraggedRoomTiles() {
|
||||
// TODO: Re-enable dragging tests when we support dragging again.
|
||||
describe.skip("does correct optimistic update when dragging from", () => {
|
||||
it("rooms to people", () => {
|
||||
expectCorrectMove(undefined, DefaultTagID.DM);
|
||||
});
|
||||
|
||||
it("rooms to favourites", () => {
|
||||
expectCorrectMove(undefined, "m.favourite");
|
||||
});
|
||||
|
||||
it("rooms to low priority", () => {
|
||||
expectCorrectMove(undefined, "m.lowpriority");
|
||||
});
|
||||
|
||||
// XXX: Known to fail - the view does not update immediately to reflect the change.
|
||||
// Whe running the app live, it updates when some other event occurs (likely the
|
||||
// m.direct arriving) that these tests do not fire.
|
||||
xit("people to rooms", () => {
|
||||
expectCorrectMove(DefaultTagID.DM, undefined);
|
||||
});
|
||||
|
||||
it("people to favourites", () => {
|
||||
expectCorrectMove(DefaultTagID.DM, "m.favourite");
|
||||
});
|
||||
|
||||
it("people to lowpriority", () => {
|
||||
expectCorrectMove(DefaultTagID.DM, "m.lowpriority");
|
||||
});
|
||||
|
||||
it("low priority to rooms", () => {
|
||||
expectCorrectMove("m.lowpriority", undefined);
|
||||
});
|
||||
|
||||
it("low priority to people", () => {
|
||||
expectCorrectMove("m.lowpriority", DefaultTagID.DM);
|
||||
});
|
||||
|
||||
it("low priority to low priority", () => {
|
||||
expectCorrectMove("m.lowpriority", "m.lowpriority");
|
||||
});
|
||||
|
||||
it("favourites to rooms", () => {
|
||||
expectCorrectMove("m.favourite", undefined);
|
||||
});
|
||||
|
||||
it("favourites to people", () => {
|
||||
expectCorrectMove("m.favourite", DefaultTagID.DM);
|
||||
});
|
||||
|
||||
it("favourites to low priority", () => {
|
||||
expectCorrectMove("m.favourite", "m.lowpriority");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
exports[`RoomTile should render the room 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="!1:example.org Unread messages."
|
||||
aria-label="!1:example.org"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_RoomTile"
|
||||
role="treeitem"
|
||||
|
@ -37,7 +37,7 @@ exports[`RoomTile should render the room 1`] = `
|
|||
class="mx_RoomTile_titleContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomTile_title mx_RoomTile_titleHasUnreadEvents"
|
||||
class="mx_RoomTile_title"
|
||||
tabindex="-1"
|
||||
title="!1:example.org"
|
||||
>
|
||||
|
@ -51,15 +51,7 @@ exports[`RoomTile should render the room 1`] = `
|
|||
<div
|
||||
aria-hidden="true"
|
||||
class="mx_RoomTile_badgeContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_dot"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
|
|
|
@ -27,6 +27,8 @@ import { SubSelection } from "../../../../../../src/components/views/rooms/wysiw
|
|||
describe("LinkModal", () => {
|
||||
const formattingFunctions = {
|
||||
link: jest.fn(),
|
||||
removeLinks: jest.fn(),
|
||||
getLink: jest.fn().mockReturnValue("my initial content"),
|
||||
} as unknown as FormattingFunctions;
|
||||
const defaultValue: SubSelection = {
|
||||
focusNode: null,
|
||||
|
@ -35,13 +37,14 @@ describe("LinkModal", () => {
|
|||
anchorOffset: 4,
|
||||
};
|
||||
|
||||
const customRender = (isTextEnabled: boolean, onClose: () => void) => {
|
||||
const customRender = (isTextEnabled: boolean, onClose: () => void, isEditing = false) => {
|
||||
return render(
|
||||
<LinkModal
|
||||
composer={formattingFunctions}
|
||||
isTextEnabled={isTextEnabled}
|
||||
onClose={onClose}
|
||||
composerContext={{ selection: defaultValue }}
|
||||
isEditing={isEditing}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
@ -75,13 +78,13 @@ describe("LinkModal", () => {
|
|||
// When
|
||||
jest.useFakeTimers();
|
||||
screen.getByText("Save").click();
|
||||
jest.runAllTimers();
|
||||
|
||||
// Then
|
||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||
await waitFor(() => expect(onClose).toBeCalledTimes(1));
|
||||
|
||||
// When
|
||||
jest.runAllTimers();
|
||||
await waitFor(() => {
|
||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||
expect(onClose).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(formattingFunctions.link).toHaveBeenCalledWith("l", undefined);
|
||||
|
@ -118,15 +121,41 @@ describe("LinkModal", () => {
|
|||
// When
|
||||
jest.useFakeTimers();
|
||||
screen.getByText("Save").click();
|
||||
jest.runAllTimers();
|
||||
|
||||
// Then
|
||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||
await waitFor(() => expect(onClose).toBeCalledTimes(1));
|
||||
|
||||
// When
|
||||
jest.runAllTimers();
|
||||
await waitFor(() => {
|
||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||
expect(onClose).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(formattingFunctions.link).toHaveBeenCalledWith("l", "t");
|
||||
});
|
||||
|
||||
it("Should remove the link", async () => {
|
||||
// When
|
||||
const onClose = jest.fn();
|
||||
customRender(true, onClose, true);
|
||||
await userEvent.click(screen.getByText("Remove"));
|
||||
|
||||
// Then
|
||||
expect(formattingFunctions.removeLinks).toHaveBeenCalledTimes(1);
|
||||
expect(onClose).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("Should display the link in editing", async () => {
|
||||
// When
|
||||
customRender(true, jest.fn(), true);
|
||||
|
||||
// Then
|
||||
expect(screen.getByLabelText("Link")).toContainHTML("my initial content");
|
||||
expect(screen.getByText("Save")).toBeDisabled();
|
||||
|
||||
// When
|
||||
await userEvent.type(screen.getByLabelText("Link"), "l");
|
||||
|
||||
// Then
|
||||
await waitFor(() => expect(screen.getByText("Save")).toBeEnabled());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutD
|
|||
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
||||
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation";
|
||||
|
||||
mockPlatformPeg();
|
||||
|
||||
|
@ -87,6 +88,7 @@ describe("<SessionManagerTab />", () => {
|
|||
generateClientSecret: jest.fn(),
|
||||
setDeviceDetails: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
deleteAccountData: jest.fn(),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
|
||||
getPushers: jest.fn(),
|
||||
setPusher: jest.fn(),
|
||||
|
@ -182,6 +184,9 @@ describe("<SessionManagerTab />", () => {
|
|||
],
|
||||
});
|
||||
|
||||
// @ts-ignore mock
|
||||
mockClient.store = { accountData: {} };
|
||||
|
||||
mockClient.getAccountData.mockReset().mockImplementation((eventType) => {
|
||||
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
||||
return new MatrixEvent({
|
||||
|
@ -667,6 +672,47 @@ describe("<SessionManagerTab />", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("removes account data events for devices after sign out", async () => {
|
||||
const mobileDeviceClientInfo = new MatrixEvent({
|
||||
type: getClientInformationEventType(alicesMobileDevice.device_id),
|
||||
content: {
|
||||
name: "test",
|
||||
},
|
||||
});
|
||||
// @ts-ignore setup mock
|
||||
mockClient.store = {
|
||||
// @ts-ignore setup mock
|
||||
accountData: {
|
||||
[mobileDeviceClientInfo.getType()]: mobileDeviceClientInfo,
|
||||
},
|
||||
};
|
||||
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({
|
||||
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
// refreshed devices after sign out
|
||||
devices: [alicesDevice],
|
||||
});
|
||||
|
||||
const { getByTestId, getByLabelText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
expect(mockClient.deleteAccountData).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(getByTestId("current-session-menu"));
|
||||
fireEvent.click(getByLabelText("Sign out of all other sessions (2)"));
|
||||
await confirmSignout(getByTestId);
|
||||
|
||||
// only called once for signed out device with account data event
|
||||
expect(mockClient.deleteAccountData).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.deleteAccountData).toHaveBeenCalledWith(mobileDeviceClientInfo.getType());
|
||||
});
|
||||
|
||||
describe("other devices", () => {
|
||||
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue