Merge branch 'develop' into gsouquet/threads-forceenablelabsflag

This commit is contained in:
Germain 2023-01-11 11:51:57 +00:00
commit d4f247d1fe
97 changed files with 3280 additions and 1325 deletions

View file

@ -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);
}

View file

@ -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"
>

View 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");
});
});
});

View file

@ -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>"`;

View file

@ -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

View file

@ -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(() => {

View file

@ -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,
};
}

View file

@ -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();
});
});

View file

@ -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();
});

View file

@ -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"

View file

@ -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());
});
});

View file

@ -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"] }] } };