Phase 2 : Refactor TextualBody to MVVM and remove legacy component (#33165)
* Refactor TextualBody to MVVM and remove legacy component * Update snapshot + fix eslint warning * update css to fix playwright tests failure * return i18n into the MVVM * Update snapshots * Update tests to reflect the css changes * Update snapshot * Update css to correct letter-spacing * Update css to fix playwright issues. * Preserve inline emote sender rendering in TextualBodyView * Update snapshot to reflect html change * Update back to span instead of button, the default button css fails tests * Extract TextualBodyFactory from MBodyFactory * Update snapshot * Update HTML snapshot to pass tests * Update Snapshots * Added several tests for coverage * Remove double checks, merge function already checks. * Remove unessecery comment * revert to button * Update snapshots because of the revert * added Math.min() to simplify ternary expressions. * Update playwright screenshots for accessibility * Update playwright screenshots * Update css to fix playwright fail * Update screenshot + snapshots * Add comments to props
This commit is contained in:
+1
-1
@@ -44,7 +44,7 @@ exports[`ReplyChain should call setQuoteExpanded if chain is longer than 2 lines
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
|
||||
@@ -38,6 +38,11 @@ jest.mock("../../../../../src/components/views/messages/MBodyFactory", () => ({
|
||||
renderMBody: () => <div data-testid="file-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/TextualBodyFactory", () => ({
|
||||
__esModule: true,
|
||||
TextualBodyFactory: () => <div data-testid="textual-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/MImageReplyBody", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="image-reply-body" />,
|
||||
@@ -53,11 +58,6 @@ jest.mock("../../../../../src/components/views/messages/MStickerBody", () => ({
|
||||
default: () => <div data-testid="sticker-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/TextualBody.tsx", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="textual-body" />,
|
||||
}));
|
||||
|
||||
describe("MessageEvent", () => {
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
@@ -21,13 +21,18 @@ import {
|
||||
} from "../../../../test-utils";
|
||||
import * as languageHandler from "../../../../../src/languageHandler";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import TextualBody from "../../../../../src/components/views/messages/TextualBody";
|
||||
import { TextualBodyFactory as TextualBody } from "../../../../../src/components/views/messages/TextualBodyFactory";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { type MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import { getRoomContext } from "../../../../test-utils/room";
|
||||
|
||||
jest.mock("../../../../../src/hooks/useMediaVisible", () => ({
|
||||
__esModule: true,
|
||||
useMediaVisible: () => [true, jest.fn()],
|
||||
}));
|
||||
|
||||
const room1Id = "!room1:example.com";
|
||||
const room2Id = "!room2:example.com";
|
||||
const room2Name = "Room 2";
|
||||
@@ -104,7 +109,6 @@ describe("<TextualBody />", () => {
|
||||
defaultMatrixClient.pushProcessor = new PushProcessor(defaultMatrixClient);
|
||||
|
||||
defaultRoom = mkStubRoom(room1Id, "test room", defaultMatrixClient);
|
||||
defaultProps.permalinkCreator = new RoomPermalinkCreator(defaultRoom);
|
||||
otherRoom = mkStubRoom(room2Id, room2Name, defaultMatrixClient);
|
||||
|
||||
mocked(defaultRoom).findEventById.mockImplementation((eventId: string) => {
|
||||
@@ -117,10 +121,14 @@ describe("<TextualBody />", () => {
|
||||
const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) => {
|
||||
const mergedProps = { ...defaultProps, ...props };
|
||||
const room = matrixClient.getRoom(mergedProps.mxEvent.getRoomId()) ?? defaultRoom;
|
||||
const finalProps = {
|
||||
...mergedProps,
|
||||
permalinkCreator: mergedProps.permalinkCreator ?? new RoomPermalinkCreator(room),
|
||||
};
|
||||
return (renderingFn ?? render)(
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<RoomContext.Provider value={getRoomContext(room, {})}>
|
||||
<TextualBody {...mergedProps} />
|
||||
<TextualBody {...finalProps} />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
@@ -163,9 +171,7 @@ describe("<TextualBody />", () => {
|
||||
|
||||
const { container } = getComponent({ mxEvent: ev, replacingEventId: ev.getId() });
|
||||
|
||||
const annotated = container.querySelector(".mx_MEmoteBody > .mx_EventTile_annotatedInline");
|
||||
expect(annotated).not.toBeNull();
|
||||
expect(annotated?.tagName).toBe("DIV");
|
||||
expect(container).toHaveTextContent("* sender winks(edited)");
|
||||
});
|
||||
|
||||
it("renders m.notice correctly", () => {
|
||||
|
||||
+3
-3
@@ -130,7 +130,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for an
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills appear for event permalinks without a custom label 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
@@ -177,7 +177,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for eve
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills appear for room links with vias 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
@@ -225,7 +225,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for roo
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills do not appear for event permalinks with a custom label 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
|
||||
+4
-4
@@ -198,7 +198,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_r_"
|
||||
>
|
||||
<div
|
||||
@@ -279,7 +279,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_13_"
|
||||
>
|
||||
<div
|
||||
@@ -426,7 +426,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_10l_"
|
||||
>
|
||||
<div
|
||||
@@ -507,7 +507,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_10t_"
|
||||
>
|
||||
<div
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ exports[`<PinnedEventTile /> should render pinned event 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_0_"
|
||||
>
|
||||
<div
|
||||
@@ -143,7 +143,7 @@ exports[`<PinnedEventTile /> should render pinned event with thread info 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
id="_r_9_"
|
||||
>
|
||||
<div
|
||||
|
||||
+3
-3
@@ -102,7 +102,7 @@ exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
@@ -196,7 +196,7 @@ exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
id="2"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
@@ -293,7 +293,7 @@ exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
id="3"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
|
||||
+3
-3
@@ -245,7 +245,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
@@ -339,7 +339,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
|
||||
id="2"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
@@ -436,7 +436,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = `
|
||||
id="3"
|
||||
>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,436 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
LINKIFIED_DATA_ATTRIBUTE,
|
||||
TextualBodyViewBodyWrapperKind,
|
||||
TextualBodyViewKind,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { MsgType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { flushPromises, mkEvent } from "../../test-utils";
|
||||
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import { TextualBodyViewModel } from "../../../src/viewmodels/room/timeline/event-tile/body/TextualBodyViewModel";
|
||||
import Modal from "../../../src/Modal";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { IntegrationManagers } from "../../../src/integrations/IntegrationManagers";
|
||||
import * as permalinkUtils from "../../../src/utils/permalinks/Permalinks";
|
||||
import QuestionDialog from "../../../src/components/views/dialogs/QuestionDialog";
|
||||
import MessageEditHistoryDialog from "../../../src/components/views/dialogs/MessageEditHistoryDialog";
|
||||
|
||||
describe("TextualBodyViewModel", () => {
|
||||
const createEvent = (
|
||||
content: Record<string, unknown>,
|
||||
overrides?: Partial<{ room: string; user: string }>,
|
||||
): MatrixEvent =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
room: overrides?.room ?? "!room:example.com",
|
||||
user: overrides?.user ?? "@alice:example.com",
|
||||
content,
|
||||
});
|
||||
|
||||
const createVm = (
|
||||
overrides?: Partial<ConstructorParameters<typeof TextualBodyViewModel>[0]>,
|
||||
): TextualBodyViewModel =>
|
||||
new TextualBodyViewModel({
|
||||
mxEvent: createEvent({
|
||||
body: "Hello world",
|
||||
msgtype: MsgType.Text,
|
||||
}),
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
window.location.hash = "";
|
||||
});
|
||||
|
||||
it("computes the initial snapshot from props", () => {
|
||||
const event = createEvent({
|
||||
body: "Caption",
|
||||
msgtype: MsgType.Image,
|
||||
});
|
||||
jest.spyOn(event, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
jest.spyOn(event, "messageVisibility").mockReturnValue({
|
||||
visible: false,
|
||||
reason: "copyright",
|
||||
} as ReturnType<MatrixEvent["messageVisibility"]>);
|
||||
|
||||
const vm = createVm({
|
||||
id: "event-id",
|
||||
mxEvent: event,
|
||||
highlightLink: "https://example.com",
|
||||
replacingEventId: "$replacement",
|
||||
isSeeingThroughMessageHiddenForModeration: true,
|
||||
});
|
||||
const snapshot = vm.getSnapshot();
|
||||
|
||||
expect(snapshot.id).toBe("event-id");
|
||||
expect(snapshot.kind).toBe(TextualBodyViewKind.CAPTION);
|
||||
expect(snapshot.bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.LINK);
|
||||
expect(snapshot.bodyLinkHref).toBe("https://example.com");
|
||||
expect(snapshot.showEditedMarker).toBe(true);
|
||||
expect(snapshot.editedMarkerText).toContain("edited");
|
||||
expect(snapshot.showPendingModerationMarker).toBe(true);
|
||||
expect(snapshot.pendingModerationText).toContain("copyright");
|
||||
});
|
||||
|
||||
it("updates message-derived fields when the event changes", () => {
|
||||
const vm = createVm();
|
||||
const emoteEvent = createEvent(
|
||||
{
|
||||
body: "waves",
|
||||
msgtype: MsgType.Emote,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": "https://scalar.example/starter",
|
||||
},
|
||||
},
|
||||
{ user: "@bob:example.com" },
|
||||
);
|
||||
emoteEvent.sender = { name: "Bob" } as MatrixEvent["sender"];
|
||||
|
||||
vm.setEvent(emoteEvent);
|
||||
|
||||
expect(vm.getSnapshot().kind).toBe(TextualBodyViewKind.EMOTE);
|
||||
expect(vm.getSnapshot().bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.ACTION);
|
||||
expect(vm.getSnapshot().emoteSenderName).toBe("Bob");
|
||||
});
|
||||
|
||||
it("updates wrapper state when the highlight link changes", () => {
|
||||
const starterLinkEvent = createEvent({
|
||||
body: "Open the integration",
|
||||
msgtype: MsgType.Text,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": "https://scalar.example/starter",
|
||||
},
|
||||
});
|
||||
const vm = createVm({ mxEvent: starterLinkEvent });
|
||||
|
||||
expect(vm.getSnapshot().bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.ACTION);
|
||||
|
||||
vm.setHighlightLink("https://element.io");
|
||||
expect(vm.getSnapshot().bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.LINK);
|
||||
expect(vm.getSnapshot().bodyLinkHref).toBe("https://element.io");
|
||||
|
||||
vm.setHighlightLink(undefined);
|
||||
expect(vm.getSnapshot().bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.ACTION);
|
||||
});
|
||||
|
||||
it("uses the notice kind and no action wrapper for non-string starter links", () => {
|
||||
const noticeEvent = createEvent({
|
||||
body: "Notice",
|
||||
msgtype: MsgType.Notice,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": 42,
|
||||
},
|
||||
});
|
||||
const vm = createVm({ mxEvent: noticeEvent });
|
||||
|
||||
expect(vm.getSnapshot().kind).toBe(TextualBodyViewKind.NOTICE);
|
||||
expect(vm.getSnapshot().bodyWrapper).toBe(TextualBodyViewBodyWrapperKind.NONE);
|
||||
expect(vm.getSnapshot().bodyActionAriaLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the moderation marker from the dedicated setter", () => {
|
||||
const hiddenEvent = createEvent({
|
||||
body: "hidden",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
jest.spyOn(hiddenEvent, "messageVisibility").mockReturnValue({
|
||||
visible: false,
|
||||
reason: "spam",
|
||||
} as ReturnType<MatrixEvent["messageVisibility"]>);
|
||||
|
||||
const vm = createVm({ mxEvent: hiddenEvent });
|
||||
|
||||
vm.setIsSeeingThroughMessageHiddenForModeration(true);
|
||||
|
||||
expect(vm.getSnapshot().showPendingModerationMarker).toBe(true);
|
||||
expect(vm.getSnapshot().pendingModerationText).toContain("spam");
|
||||
});
|
||||
|
||||
it("updates id and edited marker from dedicated setters", () => {
|
||||
const event = createEvent({
|
||||
body: "edited",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
jest.spyOn(event, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
const vm = createVm({ mxEvent: event });
|
||||
|
||||
vm.setId("updated-id");
|
||||
vm.setReplacingEventId("$edit");
|
||||
|
||||
expect(vm.getSnapshot().id).toBe("updated-id");
|
||||
expect(vm.getSnapshot().showEditedMarker).toBe(true);
|
||||
expect(vm.getSnapshot().editedMarkerText).toContain("edited");
|
||||
|
||||
vm.setReplacingEventId(undefined);
|
||||
|
||||
expect(vm.getSnapshot().showEditedMarker).toBe(false);
|
||||
expect(vm.getSnapshot().editedMarkerTooltip).toBeUndefined();
|
||||
});
|
||||
|
||||
it("renders the generic pending moderation text when there is no reason", () => {
|
||||
const hiddenEvent = createEvent({
|
||||
body: "hidden",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
jest.spyOn(hiddenEvent, "messageVisibility").mockReturnValue({
|
||||
visible: false,
|
||||
reason: null,
|
||||
} as ReturnType<MatrixEvent["messageVisibility"]>);
|
||||
const vm = createVm({ mxEvent: hiddenEvent });
|
||||
|
||||
vm.setIsSeeingThroughMessageHiddenForModeration(true);
|
||||
|
||||
expect(vm.getSnapshot().showPendingModerationMarker).toBe(true);
|
||||
expect(vm.getSnapshot().pendingModerationText).toMatch(/^\(.+\)$/);
|
||||
expect(vm.getSnapshot().pendingModerationText).not.toContain("undefined");
|
||||
});
|
||||
|
||||
it("throws when pending moderation is requested for a visible message", () => {
|
||||
const visibleEvent = createEvent({
|
||||
body: "visible",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
jest.spyOn(visibleEvent, "messageVisibility").mockReturnValue({
|
||||
visible: true,
|
||||
} as ReturnType<MatrixEvent["messageVisibility"]>);
|
||||
|
||||
expect(() =>
|
||||
createVm({
|
||||
mxEvent: visibleEvent,
|
||||
isSeeingThroughMessageHiddenForModeration: true,
|
||||
}),
|
||||
).toThrow("TextualBodyViewModel should only render pending moderation for hidden messages");
|
||||
});
|
||||
|
||||
it("ignores linkified root clicks", () => {
|
||||
const vm = createVm();
|
||||
const preventDefault = jest.fn();
|
||||
const transformSpy = jest.spyOn(permalinkUtils, "tryTransformPermalinkToLocalHref");
|
||||
|
||||
vm.onRootClick({
|
||||
preventDefault,
|
||||
target: {
|
||||
dataset: {
|
||||
[LINKIFIED_DATA_ATTRIBUTE]: "true",
|
||||
},
|
||||
href: "https://example.org",
|
||||
nodeName: "A",
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(transformSpy).not.toHaveBeenCalled();
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rewrites permalink clicks to local hashes", () => {
|
||||
const vm = createVm();
|
||||
const preventDefault = jest.fn();
|
||||
const anchor = { href: "https://element.example/#/room/!room:example.org", nodeName: "A" };
|
||||
jest.spyOn(permalinkUtils, "tryTransformPermalinkToLocalHref").mockReturnValue("#/room/!room:example.org");
|
||||
|
||||
vm.onRootClick({
|
||||
preventDefault,
|
||||
target: {
|
||||
nodeName: "SPAN",
|
||||
closest: jest.fn().mockReturnValue(anchor),
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(window.location.hash).toBe("#/room/!room:example.org");
|
||||
});
|
||||
|
||||
it("leaves external root clicks alone when no local permalink is found", () => {
|
||||
const vm = createVm();
|
||||
const preventDefault = jest.fn();
|
||||
const href = "https://example.org";
|
||||
jest.spyOn(permalinkUtils, "tryTransformPermalinkToLocalHref").mockReturnValue(href);
|
||||
|
||||
vm.onRootClick({
|
||||
preventDefault,
|
||||
target: {
|
||||
href,
|
||||
nodeName: "A",
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
expect(window.location.hash).toBe("");
|
||||
});
|
||||
|
||||
it("does nothing for body actions without a starter link", () => {
|
||||
const vm = createVm();
|
||||
const preventDefault = jest.fn();
|
||||
const hasManagerSpy = jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager");
|
||||
|
||||
vm.onBodyActionClick({ preventDefault } as any);
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(hasManagerSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the no-manager dialog for starter links when integrations are unavailable", () => {
|
||||
const vm = createVm({
|
||||
mxEvent: createEvent({
|
||||
body: "Open the integration",
|
||||
msgtype: MsgType.Text,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": "https://scalar.example/starter",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const preventDefault = jest.fn();
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
jest.spyOn(managers, "hasManager").mockReturnValue(false);
|
||||
const openNoManagerDialogSpy = jest.spyOn(managers, "openNoManagerDialog").mockImplementation(() => {});
|
||||
|
||||
vm.onBodyActionClick({ preventDefault } as any);
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(openNoManagerDialogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the scalar starter link after confirmation", async () => {
|
||||
const vm = createVm({
|
||||
mxEvent: createEvent({
|
||||
body: "Open the integration",
|
||||
msgtype: MsgType.Text,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": "https://scalar.example/starter",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const preventDefault = jest.fn();
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
const connect = jest.fn().mockResolvedValue(undefined);
|
||||
const getStarterLink = jest.fn().mockReturnValue("https://scalar.example/complete");
|
||||
const scalarClient = { connect, getStarterLink };
|
||||
const integrationManager = {
|
||||
getScalarClient: jest.fn().mockReturnValue(scalarClient),
|
||||
uiUrl: "https://scalar.example/ui",
|
||||
};
|
||||
const popup = { opener: "initial" };
|
||||
|
||||
jest.spyOn(managers, "hasManager").mockReturnValue(true);
|
||||
jest.spyOn(managers, "getPrimaryManager").mockReturnValue(integrationManager as any);
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
close: jest.fn(),
|
||||
finished: Promise.resolve([true]),
|
||||
} as any);
|
||||
const openSpy = jest.spyOn(window, "open").mockImplementation(() => popup as any);
|
||||
|
||||
vm.onBodyActionClick({ preventDefault } as any);
|
||||
await flushPromises();
|
||||
|
||||
expect(connect).toHaveBeenCalled();
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(
|
||||
QuestionDialog,
|
||||
expect.objectContaining({
|
||||
button: expect.any(String),
|
||||
title: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
"https://scalar.example/complete",
|
||||
"_blank",
|
||||
expect.stringContaining("width="),
|
||||
);
|
||||
expect(popup.opener).toBeNull();
|
||||
});
|
||||
|
||||
it("does not open the scalar starter link when the dialog is cancelled", async () => {
|
||||
const vm = createVm({
|
||||
mxEvent: createEvent({
|
||||
body: "Open the integration",
|
||||
msgtype: MsgType.Text,
|
||||
data: {
|
||||
"org.matrix.neb.starter_link": "https://scalar.example/starter",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const managers = IntegrationManagers.sharedInstance();
|
||||
const scalarClient = {
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
getStarterLink: jest.fn().mockReturnValue("https://scalar.example/complete"),
|
||||
};
|
||||
|
||||
jest.spyOn(managers, "hasManager").mockReturnValue(true);
|
||||
jest.spyOn(managers, "getPrimaryManager").mockReturnValue({
|
||||
getScalarClient: jest.fn().mockReturnValue(scalarClient),
|
||||
uiUrl: "https://scalar.example/ui",
|
||||
} as any);
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
close: jest.fn(),
|
||||
finished: Promise.resolve([false]),
|
||||
} as any);
|
||||
const openSpy = jest.spyOn(window, "open").mockImplementation(() => ({ opener: "initial" }) as any);
|
||||
|
||||
vm.onBodyActionClick({ preventDefault: jest.fn() } as any);
|
||||
await flushPromises();
|
||||
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the edit history dialog from the edited marker", () => {
|
||||
const event = createEvent({
|
||||
body: "edited",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
const vm = createVm({ mxEvent: event });
|
||||
const createDialogSpy = jest
|
||||
.spyOn(Modal, "createDialog")
|
||||
.mockImplementation(() => ({ close: jest.fn() }) as any);
|
||||
|
||||
vm.onEditedMarkerClick();
|
||||
|
||||
expect(createDialogSpy).toHaveBeenCalledWith(MessageEditHistoryDialog, { mxEvent: event });
|
||||
});
|
||||
|
||||
it("dispatches composer insert for the emote sender using the current rendering type", () => {
|
||||
const event = createEvent({
|
||||
body: "waves",
|
||||
msgtype: MsgType.Emote,
|
||||
});
|
||||
const vm = createVm({ mxEvent: event });
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
|
||||
vm.setTimelineRenderingType(TimelineRenderingType.Thread);
|
||||
vm.onEmoteSenderClick();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ComposerInsert,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
userId: event.getSender(),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not emit for unchanged setter values", () => {
|
||||
const mxEvent = createEvent({
|
||||
body: "Hello world",
|
||||
msgtype: MsgType.Text,
|
||||
});
|
||||
const vm = createVm({ mxEvent });
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setId(undefined);
|
||||
vm.setEvent(mxEvent);
|
||||
vm.setHighlightLink(undefined);
|
||||
vm.setReplacingEventId(undefined);
|
||||
vm.setIsSeeingThroughMessageHiddenForModeration(undefined);
|
||||
vm.setTimelineRenderingType(TimelineRenderingType.Room);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user