Skip to content

Commit fd7a76f

Browse files
committed
Tests
1 parent 72b2e28 commit fd7a76f

7 files changed

Lines changed: 110 additions & 17 deletions

File tree

apps/web/src/components/structures/RoomView.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,14 +1293,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
12931293
}
12941294

12951295
case Action.ComposerInsert: {
1296-
if (payload.composerType) break;
1296+
const composerInsertPayload = payload as ComposerInsertPayload;
1297+
if (composerInsertPayload.composerType) break;
12971298

1298-
let timelineRenderingType: TimelineRenderingType = payload.timelineRenderingType;
1299+
// If the dispatchee didn't request a timeline rendering type, use the current one.
1300+
let timelineRenderingType: TimelineRenderingType =
1301+
composerInsertPayload.timelineRenderingType ?? this.state.timelineRenderingType;
12991302
// ThreadView handles Action.ComposerInsert itself due to it having its own editState
13001303
if (timelineRenderingType === TimelineRenderingType.Thread) break;
13011304
if (
13021305
this.state.timelineRenderingType === TimelineRenderingType.Search &&
1303-
payload.timelineRenderingType === TimelineRenderingType.Search
1306+
composerInsertPayload.timelineRenderingType === TimelineRenderingType.Search
13041307
) {
13051308
// we don't have the composer rendered in this state, so bring it back first
13061309
await this.onCancelSearchClick();
@@ -1309,7 +1312,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
13091312

13101313
// re-dispatch to the correct composer
13111314
defaultDispatcher.dispatch<ComposerInsertPayload>({
1312-
...(payload as ComposerInsertPayload),
1315+
...composerInsertPayload,
13131316
timelineRenderingType,
13141317
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
13151318
});

apps/web/src/dispatcher/payloads/ComposerInsertPayload.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,32 @@ export enum ComposerType {
1515
Edit = "edit",
1616
}
1717

18-
interface IBaseComposerInsertPayload extends ActionPayload {
18+
/**
19+
* Explicit composer insert to target
20+
*/
21+
interface IBaseComposerInsertPayloadExplicit extends ActionPayload {
1922
action: Action.ComposerInsert;
2023
timelineRenderingType: TimelineRenderingType;
21-
composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer
24+
composerType: ComposerType;
2225
}
2326

24-
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
25-
userId: string;
27+
/**
28+
* Explicit composer insert to current target.
29+
*/
30+
interface IBaseComposerInsertPayloadImplicit extends ActionPayload {
31+
action: Action.ComposerInsert;
32+
composerType?: undefined; // undefined if this should be re-dispatched to the correct composer
33+
timelineRenderingType?: TimelineRenderingType; // undefined if this should just use the current in-focus type.
2634
}
2735

28-
interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload {
36+
type IBaseComposerInsertPayload = IBaseComposerInsertPayloadExplicit | IBaseComposerInsertPayloadImplicit;
37+
38+
type IComposerInsertMentionPayload = IBaseComposerInsertPayload & {
39+
userId: string;
40+
};
41+
42+
type IComposerInsertPlaintextPayload = IBaseComposerInsertPayload & {
2943
text: string;
30-
}
44+
};
3145

3246
export type ComposerInsertPayload = IComposerInsertMentionPayload | IComposerInsertPlaintextPayload;

apps/web/src/modules/Api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { StoresApi } from "./StoresApi.ts";
3333
import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
3434
import { WidgetApi } from "./WidgetApi.ts";
3535
import { CustomisationsApi } from "./customisationsApi.ts";
36+
import { ComposerApi } from "./ComposerApi.ts";
37+
import defaultDispatcher from "../dispatcher/dispatcher.ts";
3638

3739
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
3840
let used = false;
@@ -94,6 +96,7 @@ export class ModuleApi implements Api {
9496
public readonly rootNode = document.getElementById("matrixchat")!;
9597
public readonly client = new ClientApi();
9698
public readonly stores = new StoresApi();
99+
public readonly composer = new ComposerApi(defaultDispatcher);
97100

98101
public createRoot(element: Element): Root {
99102
return createRoot(element);

apps/web/src/modules/ComposerApi.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ Please see LICENSE files in the repository root for full details.
77

88
import { type ComposerApi as ModuleComposerApi } from "@element-hq/element-web-module-api";
99

10-
import defaultDispatcher from "../dispatcher/dispatcher";
10+
import type { MatrixDispatcher } from "../dispatcher/dispatcher";
1111
import { Action } from "../dispatcher/actions";
1212
import type { ComposerInsertPayload } from "../dispatcher/payloads/ComposerInsertPayload";
13-
import { TimelineRenderingType } from "../contexts/RoomContext";
1413

1514
export class ComposerApi implements ModuleComposerApi {
15+
public constructor(private readonly dispatcher: MatrixDispatcher) {}
16+
1617
public insertTextIntoComposer(text: string): void {
17-
defaultDispatcher.dispatch({
18+
this.dispatcher.dispatch({
1819
action: Action.ComposerInsert,
1920
text,
20-
timelineRenderingType: TimelineRenderingType.Room,
2121
} satisfies ComposerInsertPayload);
2222
}
2323
}

apps/web/test/unit-tests/components/structures/RoomView-test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog.ts
7171
import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents";
7272
import { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
7373
import { ModuleApi } from "../../../../src/modules/Api";
74+
import { ComposerInsertPayload, ComposerType } from "../../../../src/dispatcher/payloads/ComposerInsertPayload.ts";
7475

7576
// Used by group calls
7677
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
@@ -1075,6 +1076,54 @@ describe("RoomView", () => {
10751076
expect(onRoomViewUpdateMock).toHaveBeenCalledWith(true);
10761077
});
10771078

1079+
describe("handles Action.ComposerInsert", () => {
1080+
it("redispatches an empty composerType, timelineRenderingType with the current state", async () => {
1081+
jest.spyOn(defaultDispatcher, "dispatch");
1082+
await mountRoomView();
1083+
const promise = untilDispatch((payload) => {
1084+
try {
1085+
expect(payload).toEqual({
1086+
action: Action.ComposerInsert,
1087+
text: "Hello world",
1088+
timelineRenderingType: TimelineRenderingType.Room,
1089+
composerType: ComposerType.Send,
1090+
});
1091+
} catch {
1092+
return false;
1093+
}
1094+
return true;
1095+
}, defaultDispatcher);
1096+
defaultDispatcher.dispatch({
1097+
action: Action.ComposerInsert,
1098+
text: "Hello world",
1099+
} satisfies ComposerInsertPayload);
1100+
await promise;
1101+
});
1102+
it("redispatches an empty composerType with the current state", async () => {
1103+
jest.spyOn(defaultDispatcher, "dispatch");
1104+
await mountRoomView();
1105+
const promise = untilDispatch((payload) => {
1106+
try {
1107+
expect(payload).toEqual({
1108+
action: Action.ComposerInsert,
1109+
text: "Hello world",
1110+
timelineRenderingType: TimelineRenderingType.Room,
1111+
composerType: ComposerType.Send,
1112+
});
1113+
} catch {
1114+
return false;
1115+
}
1116+
return true;
1117+
}, defaultDispatcher);
1118+
defaultDispatcher.dispatch({
1119+
action: Action.ComposerInsert,
1120+
text: "Hello world",
1121+
timelineRenderingType: TimelineRenderingType.Room,
1122+
} satisfies ComposerInsertPayload);
1123+
await promise;
1124+
});
1125+
});
1126+
10781127
describe("when there is a RoomView", () => {
10791128
const widget1Id = "widget1";
10801129
const widget2Id = "widget2";

apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
247247
class="_actions_n7ud0_61"
248248
>
249249
<button
250-
class="_button_1nw83_8 _primaryAction_1xryk_20 _has-icon_1nw83_60"
250+
class="_button_13vu4_8 _primaryAction_1xryk_20 _has-icon_13vu4_60"
251251
data-kind="primary"
252-
data-size="md"
252+
data-size="sm"
253253
role="button"
254254
tabindex="0"
255255
>
@@ -1062,7 +1062,7 @@ exports[`RoomView invites renders an invite room 1`] = `
10621062
Decline
10631063
</div>
10641064
<button
1065-
class="_button_1nw83_8 _destructive_1nw83_110"
1065+
class="_button_13vu4_8 _destructive_13vu4_110"
10661066
data-kind="tertiary"
10671067
data-size="lg"
10681068
role="button"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { Action } from "../../../src/dispatcher/actions";
9+
import type { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
10+
import { ComposerApi } from "../../../src/modules/ComposerApi";
11+
12+
describe("ComposerApi", () => {
13+
it("should be able to insert text via insertTextIntoComposer()", () => {
14+
const dispatcher = {
15+
dispatch: jest.fn(),
16+
} as unknown as MatrixDispatcher;
17+
const api = new ComposerApi(dispatcher);
18+
api.insertTextIntoComposer("Hello world");
19+
expect(dispatcher.dispatch).toHaveBeenCalledWith({
20+
action: Action.ComposerInsert,
21+
text: "Hello world",
22+
});
23+
});
24+
});

0 commit comments

Comments
 (0)