Rework Upload internals to use MVVM (#33307)
* Initial reword of upload to MVVM. * Update tests * More incremental improvements * Refactor tests to use helper method for composer uploads. * Add drag and drop tests * lint * Add commentary * fixup test * More precise selector * Retarget uploads * lint * fixup * one more type * update snap * Fixup composerUploadFiles * fix import * lint * Copy and paste fixes too * Add tests for pasting * Add tests for pasting files. * Remove redundant fn * rm comment * tidy up * Test cleanup * More clean up * another fix * Use condensed version * Cleanup tests * more cleaning * last bity * s/throw Error/throw new Error/
This commit is contained in:
@@ -659,8 +659,8 @@ export function mkMessage({
|
||||
|
||||
export function mkStubRoom(
|
||||
roomId: string | null | undefined = null,
|
||||
name: string | undefined,
|
||||
client: MatrixClient | undefined,
|
||||
name?: string | undefined,
|
||||
client?: MatrixClient | undefined,
|
||||
state?: RoomState | undefined,
|
||||
): Room {
|
||||
const stubTimeline = {
|
||||
|
||||
@@ -6,35 +6,62 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { render, fireEvent } from "jest-matrix-react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { useMockedViewModel } from "@element-hq/web-shared-components";
|
||||
|
||||
import FileDropTarget from "../../../../src/components/structures/FileDropTarget.tsx";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import {
|
||||
RoomUploadContext,
|
||||
type RoomUploadViewActions,
|
||||
type RoomUploadViewModel,
|
||||
type RoomUploadViewSnapshot,
|
||||
} from "../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
function FileDropTargetWrapped({
|
||||
element,
|
||||
snapshot,
|
||||
actions,
|
||||
}: {
|
||||
element: HTMLDivElement;
|
||||
snapshot: RoomUploadViewSnapshot;
|
||||
actions: Partial<RoomUploadViewActions>;
|
||||
}) {
|
||||
const mockVm = useMockedViewModel<RoomUploadViewSnapshot, RoomUploadViewActions>(
|
||||
snapshot,
|
||||
actions as RoomUploadViewActions,
|
||||
);
|
||||
return (
|
||||
<RoomUploadContext.Provider value={mockVm as RoomUploadViewModel}>
|
||||
<FileDropTarget parent={element} />
|
||||
</RoomUploadContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("FileDropTarget", () => {
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
const client = stubClient();
|
||||
room = new Room("!roomId:example.com", client, client.getUserId()!);
|
||||
room.currentState.maySendMessage = jest.fn().mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should render nothing when idle", () => {
|
||||
const element = document.createElement("div");
|
||||
const onFileDrop = jest.fn();
|
||||
|
||||
const { asFragment } = render(<FileDropTarget room={room} onFileDrop={onFileDrop} parent={element} />);
|
||||
const { asFragment } = render(
|
||||
<FileDropTargetWrapped
|
||||
element={element}
|
||||
snapshot={{ mayUpload: true }}
|
||||
actions={{ initiateViaDataTransfer: onFileDrop }}
|
||||
/>,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render drop file prompt on mouse over with file if permissions allow", () => {
|
||||
const element = document.createElement("div");
|
||||
const onFileDrop = jest.fn();
|
||||
mocked(room.currentState.maySendMessage).mockReturnValue(true);
|
||||
|
||||
const { asFragment } = render(<FileDropTarget room={room} onFileDrop={onFileDrop} parent={element} />);
|
||||
const { asFragment } = render(
|
||||
<FileDropTargetWrapped
|
||||
element={element}
|
||||
snapshot={{ mayUpload: true }}
|
||||
actions={{ initiateViaDataTransfer: onFileDrop }}
|
||||
/>,
|
||||
);
|
||||
fireEvent.dragEnter(element, {
|
||||
dataTransfer: {
|
||||
types: ["Files"],
|
||||
@@ -46,9 +73,13 @@ describe("FileDropTarget", () => {
|
||||
it("should not render drop file prompt on mouse over with file if permissions do not allow", () => {
|
||||
const element = document.createElement("div");
|
||||
const onFileDrop = jest.fn();
|
||||
mocked(room.currentState.maySendMessage).mockReturnValue(false);
|
||||
|
||||
const { asFragment } = render(<FileDropTarget room={room} onFileDrop={onFileDrop} parent={element} />);
|
||||
const { asFragment } = render(
|
||||
<FileDropTargetWrapped
|
||||
element={element}
|
||||
snapshot={{ mayUpload: false }}
|
||||
actions={{ initiateViaDataTransfer: onFileDrop }}
|
||||
/>,
|
||||
);
|
||||
fireEvent.dragEnter(element, {
|
||||
dataTransfer: {
|
||||
types: ["Files"],
|
||||
|
||||
+60
-35
@@ -270,6 +270,12 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -638,16 +644,17 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1010,16 +1017,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1320,6 +1328,12 @@ exports[`RoomView should hide the composer when hideComposer=true 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1621,15 +1635,16 @@ exports[`RoomView should hide the header when hideHeader=true 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2096,15 +2111,16 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2571,15 +2587,16 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2794,6 +2811,12 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3288,15 +3311,16 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3844,15 +3868,16 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
data-testid="room-upload-context-input"
|
||||
multiple=""
|
||||
style="display: none;"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
<div>
|
||||
|
||||
@@ -36,6 +36,10 @@ import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
import {
|
||||
RoomUploadContext,
|
||||
type RoomUploadViewModel,
|
||||
} from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
const openStickerPicker = async (): Promise<void> => {
|
||||
await userEvent.click(screen.getByLabelText("More options"));
|
||||
@@ -469,7 +473,9 @@ function wrapAndRender(
|
||||
const getRawComponent = (props = {}, context = roomContext, client = mockClient) => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider {...context}>
|
||||
<MessageComposer {...defaultProps} {...props} />
|
||||
<RoomUploadContext.Provider value={{} as RoomUploadViewModel}>
|
||||
<MessageComposer {...defaultProps} {...props} />
|
||||
</RoomUploadContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,8 @@ import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-u
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
import { type RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
describe("MessageComposerButtons", () => {
|
||||
// @ts-ignore - we're deliberately not implementing the whole interface here, but
|
||||
@@ -54,7 +55,9 @@ describe("MessageComposerButtons", () => {
|
||||
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext}>{component}</ScopedRoomContextProvider>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext}>
|
||||
<RoomUploadContextProvider>{component}</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
|
||||
import { addTextToComposer } from "../../../../test-utils/composer";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext.ts";
|
||||
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
jest.mock("../../../../../src/utils/local-room", () => ({
|
||||
doMaybeLocalRoomAction: jest.fn(),
|
||||
@@ -187,8 +188,10 @@ describe("<SendMessageComposer/>", () => {
|
||||
};
|
||||
const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider {...roomContext}>
|
||||
<SendMessageComposer {...defaultProps} {...props} />
|
||||
<ScopedRoomContextProvider room={mockRoom} {...roomContext}>
|
||||
<RoomUploadContextProvider>
|
||||
<SendMessageComposer {...defaultProps} {...props} />
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
@@ -435,7 +438,11 @@ describe("<SendMessageComposer/>", () => {
|
||||
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<SendMessageComposer room={room} toggleStickerPickerOpen={jest.fn()} />
|
||||
<ScopedRoomContextProvider {...({ room } as unknown as RoomContextType)}>
|
||||
<RoomUploadContextProvider>
|
||||
<SendMessageComposer room={room} toggleStickerPickerOpen={jest.fn()} />
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
|
||||
+10
-3
@@ -27,6 +27,7 @@ import { type ActionPayload } from "../../../../../../src/dispatcher/payloads";
|
||||
import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton";
|
||||
import { createMocks } from "./utils";
|
||||
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { RoomUploadContextProvider } from "../../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
beforeAll(initOnce, 10000);
|
||||
|
||||
@@ -46,7 +47,9 @@ describe("EditWysiwygComposer", () => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider {...roomContext}>
|
||||
<EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} />
|
||||
<RoomUploadContextProvider>
|
||||
<EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} />
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
@@ -62,7 +65,9 @@ describe("EditWysiwygComposer", () => {
|
||||
rerender(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext} room={undefined}>
|
||||
<EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} />
|
||||
<RoomUploadContextProvider>
|
||||
<EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} />
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
@@ -273,7 +278,9 @@ describe("EditWysiwygComposer", () => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext}>
|
||||
<EditWysiwygComposer editorStateTransfer={editorStateTransfer} />
|
||||
<RoomUploadContextProvider>
|
||||
<EditWysiwygComposer editorStateTransfer={editorStateTransfer} />
|
||||
</RoomUploadContextProvider>
|
||||
<Emoji menuPosition={{ chevronFace: ChevronFace.Top }} />
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
|
||||
+12
-9
@@ -25,6 +25,7 @@ import { setSelection } from "../../../../../../src/components/views/rooms/wysiw
|
||||
import { createMocks } from "./utils";
|
||||
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { E2EStatus } from "../../../../../../src/utils/ShieldUtils.ts";
|
||||
import { RoomUploadContextProvider } from "../../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({
|
||||
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
|
||||
@@ -77,15 +78,17 @@ describe("SendWysiwygComposer", () => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext}>
|
||||
<SendWysiwygComposer
|
||||
onChange={onChange}
|
||||
onSend={onSend}
|
||||
disabled={disabled}
|
||||
isRichTextEnabled={isRichTextEnabled}
|
||||
menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })}
|
||||
placeholder={placeholder}
|
||||
e2eStatus={e2eStatus}
|
||||
/>
|
||||
<RoomUploadContextProvider>
|
||||
<SendWysiwygComposer
|
||||
onChange={onChange}
|
||||
onSend={onSend}
|
||||
disabled={disabled}
|
||||
isRichTextEnabled={isRichTextEnabled}
|
||||
menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })}
|
||||
placeholder={placeholder}
|
||||
e2eStatus={e2eStatus}
|
||||
/>
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
+24
-19
@@ -33,6 +33,7 @@ import type AutocompleteProvider from "../../../../../../../src/autocomplete/Aut
|
||||
import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks";
|
||||
import { type PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor";
|
||||
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { RoomUploadContextProvider } from "../../../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
|
||||
beforeAll(initOnce, 10000);
|
||||
|
||||
@@ -42,12 +43,14 @@ describe("WysiwygComposer", () => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ScopedRoomContextProvider {...defaultRoomContext}>
|
||||
<WysiwygComposer
|
||||
onChange={onChange}
|
||||
onSend={onSend}
|
||||
disabled={disabled}
|
||||
initialContent={initialContent}
|
||||
/>
|
||||
<RoomUploadContextProvider>
|
||||
<WysiwygComposer
|
||||
onChange={onChange}
|
||||
onSend={onSend}
|
||||
disabled={disabled}
|
||||
initialContent={initialContent}
|
||||
/>
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
@@ -561,19 +564,21 @@ describe("WysiwygComposer", () => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider {...roomContext}>
|
||||
<ComposerContext.Provider
|
||||
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
|
||||
>
|
||||
<WysiwygComposer
|
||||
onChange={jest.fn()}
|
||||
onSend={jest.fn()}
|
||||
initialContent={
|
||||
roomContext.room && _editorStateTransfer
|
||||
? parseEditorStateTransfer(_editorStateTransfer, roomContext.room, client)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</ComposerContext.Provider>
|
||||
<RoomUploadContextProvider>
|
||||
<ComposerContext.Provider
|
||||
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
|
||||
>
|
||||
<WysiwygComposer
|
||||
onChange={jest.fn()}
|
||||
onSend={jest.fn()}
|
||||
initialContent={
|
||||
roomContext.room && _editorStateTransfer
|
||||
? parseEditorStateTransfer(_editorStateTransfer, roomContext.room, client)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</ComposerContext.Provider>
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
+29
-125
@@ -5,29 +5,21 @@ Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
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 { type IEventRelation, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { TimelineRenderingType } from "../../../../../../../src/contexts/RoomContext";
|
||||
import { mkStubRoom, stubClient } from "../../../../../../test-utils";
|
||||
import ContentMessages from "../../../../../../../src/ContentMessages";
|
||||
import { type IRoomState } from "../../../../../../../src/components/structures/RoomView";
|
||||
import {
|
||||
handleClipboardEvent,
|
||||
isEventToHandleAsClipboardEvent,
|
||||
} from "../../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/utils";
|
||||
import type { RoomUploadViewModel } from "../../../../../../../src/viewmodels/room/RoomUploadViewModel";
|
||||
import type { MockedObject } from "jest-mock";
|
||||
|
||||
const mockClient = stubClient();
|
||||
const mockRoom = mkStubRoom("mock room", "mock room", mockClient);
|
||||
const mockRoomState = {
|
||||
room: mockRoom,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
replyToEvent: {} as unknown as MatrixEvent,
|
||||
} as unknown as IRoomState;
|
||||
const mockUploadVM = {
|
||||
initiateViaDataTransfer: jest.fn().mockResolvedValue(undefined),
|
||||
initiateViaInputFiles: jest.fn().mockResolvedValue(undefined),
|
||||
} as Partial<RoomUploadViewModel> as MockedObject<RoomUploadViewModel>;
|
||||
|
||||
const sendContentListToRoomSpy = jest.spyOn(ContentMessages.sharedInstance(), "sendContentListToRoom");
|
||||
const sendContentToRoomSpy = jest.spyOn(ContentMessages.sharedInstance(), "sendContentToRoom");
|
||||
const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
describe("handleClipboardEvent", () => {
|
||||
@@ -45,29 +37,16 @@ describe("handleClipboardEvent", () => {
|
||||
|
||||
it("returns false if it is not a paste event", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "copy" });
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
expect(output).toBe(false);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if clipboard data is null", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "paste", clipboardData: null });
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if room is undefined", () => {
|
||||
const originalEvent = createMockClipboardEvent({ type: "paste" });
|
||||
const { room, ...roomStateWithoutRoom } = mockRoomState;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
roomStateWithoutRoom,
|
||||
mockClient,
|
||||
);
|
||||
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
expect(output).toBe(false);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false if room clipboardData files and types are empty", () => {
|
||||
@@ -75,8 +54,9 @@ describe("handleClipboardEvent", () => {
|
||||
type: "paste",
|
||||
clipboardData: { files: [], types: [] },
|
||||
});
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
expect(output).toBe(false);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles event and calls sendContentListToRoom when data files are present", () => {
|
||||
@@ -84,65 +64,23 @@ describe("handleClipboardEvent", () => {
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient);
|
||||
const mockReplyToEvent = {} as unknown as MatrixEvent;
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
originalEvent.clipboardData?.files,
|
||||
mockRoom.roomId,
|
||||
undefined, // this is the event relation, an optional arg
|
||||
mockReplyToEvent,
|
||||
mockClient,
|
||||
mockRoomState.timelineRenderingType,
|
||||
);
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls sendContentListToRoom with eventRelation when present", () => {
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const mockReplyToEvent = {} as unknown as MatrixEvent;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
originalEvent.clipboardData?.files,
|
||||
mockRoom.roomId,
|
||||
mockEventRelation, // this is the event relation, an optional arg
|
||||
mockReplyToEvent,
|
||||
mockClient,
|
||||
mockRoomState.timelineRenderingType,
|
||||
);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).toHaveBeenCalledWith(originalEvent.clipboardData);
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls the error handler when sentContentListToRoom errors", async () => {
|
||||
const mockErrorMessage = "something went wrong";
|
||||
sendContentListToRoomSpy.mockRejectedValueOnce(new Error(mockErrorMessage));
|
||||
mockUploadVM.initiateViaDataTransfer.mockRejectedValueOnce(new Error(mockErrorMessage));
|
||||
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
clipboardData: { files: ["something here"], types: [] },
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockUploadVM.initiateViaDataTransfer).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
});
|
||||
@@ -158,15 +96,7 @@ describe("handleClipboardEvent", () => {
|
||||
getData: jest.fn().mockReturnValue("<div>invalid html"),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
expect(logSpy).toHaveBeenCalledWith("Failed to handle pasted content as Safari inserted content");
|
||||
expect(output).toBe(false);
|
||||
});
|
||||
@@ -180,10 +110,10 @@ describe("handleClipboardEvent", () => {
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockRoomState, mockClient, mockEventRelation);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
|
||||
expect(fetchMock).toHaveFetchedTimes(1, "blob:");
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls error handler when fetch fails", async () => {
|
||||
@@ -197,14 +127,7 @@ describe("handleClipboardEvent", () => {
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
@@ -212,7 +135,7 @@ describe("handleClipboardEvent", () => {
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
|
||||
it("calls sendContentToRoom when parsing is successful", async () => {
|
||||
it("calls initiateViaInputFiles when parsing is successful", async () => {
|
||||
fetchMock.get("test/file", {
|
||||
blob: () => {
|
||||
return Promise.resolve({ type: "image/jpeg" } as Blob);
|
||||
@@ -227,23 +150,11 @@ describe("handleClipboardEvent", () => {
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendContentToRoomSpy).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
mockRoom.roomId,
|
||||
mockEventRelation,
|
||||
mockClient,
|
||||
mockRoomState.replyToEvent,
|
||||
);
|
||||
expect(mockUploadVM.initiateViaInputFiles).toHaveBeenCalledTimes(1);
|
||||
expect(mockUploadVM.initiateViaInputFiles).toHaveBeenCalledWith([expect.any(File)]);
|
||||
});
|
||||
expect(output).toBe(true);
|
||||
});
|
||||
@@ -254,8 +165,8 @@ describe("handleClipboardEvent", () => {
|
||||
return Promise.resolve({ type: "image/jpeg" } as Blob);
|
||||
},
|
||||
});
|
||||
const mockErrorMessage = "sendContentToRoom failed";
|
||||
sendContentToRoomSpy.mockRejectedValueOnce(mockErrorMessage);
|
||||
const mockErrorMessage = "initiateViaInputFiles failed";
|
||||
mockUploadVM.initiateViaInputFiles.mockRejectedValueOnce(mockErrorMessage);
|
||||
|
||||
const originalEvent = createMockClipboardEvent({
|
||||
type: "paste",
|
||||
@@ -265,14 +176,7 @@ describe("handleClipboardEvent", () => {
|
||||
getData: jest.fn().mockReturnValue(`<img src="blob:" />`),
|
||||
},
|
||||
});
|
||||
const mockEventRelation = {} as unknown as IEventRelation;
|
||||
const output = handleClipboardEvent(
|
||||
originalEvent,
|
||||
originalEvent.clipboardData,
|
||||
mockRoomState,
|
||||
mockClient,
|
||||
mockEventRelation,
|
||||
);
|
||||
const output = handleClipboardEvent(originalEvent, originalEvent.clipboardData, mockUploadVM);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logSpy).toHaveBeenCalledWith(mockErrorMessage);
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (c) 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 { type IEventRelation, type MatrixClient, type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { MockedObject } from "jest-mock";
|
||||
import { RoomUploadViewModel } from "../../../src/viewmodels/room/RoomUploadViewModel";
|
||||
import { mkEvent, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import type { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||
import ContentMessages from "../../../src/ContentMessages";
|
||||
const sendContentListToRoomSpy = jest.spyOn(ContentMessages.sharedInstance(), "sendContentListToRoom");
|
||||
|
||||
describe("RoomUploadViewModel", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let room: MockedObject<Room>;
|
||||
let dis: MockedObject<MatrixDispatcher>;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
client = stubClient() as MockedObject<MatrixClient>;
|
||||
room = mkStubRoom("!room", undefined, undefined) as MockedObject<Room>;
|
||||
dis = {
|
||||
dispatch: jest.fn(),
|
||||
} as Partial<MatrixDispatcher> as MockedObject<MatrixDispatcher>;
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([true, false])("handles state of mayUpload when room.maySendMessage = %s", (maySendMessage) => {
|
||||
room.maySendMessage.mockReturnValue(maySendMessage);
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Room,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
expect(vm.getSnapshot().mayUpload).toEqual(maySendMessage);
|
||||
room.maySendMessage.mockReturnValue(!maySendMessage);
|
||||
room.emit(RoomEvent.CurrentStateUpdated, room, null as any, null as any);
|
||||
expect(vm.getSnapshot().mayUpload).toEqual(!maySendMessage);
|
||||
});
|
||||
|
||||
describe("uploads via input", () => {
|
||||
it("redirected if guest", async () => {
|
||||
client.isGuest.mockReturnValue(true);
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Room,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
await vm.initiateViaInputFiles([] as unknown as FileList);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
|
||||
});
|
||||
it("skips empty files", async () => {
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Room,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
await vm.initiateViaInputFiles([] as unknown as FileList);
|
||||
expect(dis.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
it("uploads with correct context", async () => {
|
||||
sendContentListToRoomSpy.mockResolvedValue(undefined);
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Thread,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
const replyEvent = mkEvent({ event: true, type: "anything", user: "anyone", content: {} });
|
||||
vm.setReplyToEvent(replyEvent);
|
||||
const threadRelation: IEventRelation = { key: "foo" };
|
||||
vm.setThreadRelation(threadRelation);
|
||||
const fileList = [
|
||||
{
|
||||
name: "fake.png",
|
||||
size: 1024,
|
||||
type: "image/png",
|
||||
},
|
||||
] as unknown as FileList;
|
||||
await vm.initiateViaInputFiles(fileList);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
fileList,
|
||||
room.roomId,
|
||||
threadRelation,
|
||||
replyEvent,
|
||||
client,
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uploads via data transfer", () => {
|
||||
it("redirected if guest", async () => {
|
||||
client.isGuest.mockReturnValue(true);
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Room,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
await vm.initiateViaDataTransfer({} as DataTransfer);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
|
||||
});
|
||||
it("skips empty files", async () => {
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Room,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
await vm.initiateViaDataTransfer({ files: [] as unknown as FileList } as DataTransfer);
|
||||
expect(dis.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
it("uploads with correct context", async () => {
|
||||
sendContentListToRoomSpy.mockResolvedValue(undefined);
|
||||
const vm = new RoomUploadViewModel(
|
||||
room,
|
||||
client,
|
||||
TimelineRenderingType.Thread,
|
||||
dis,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
);
|
||||
const replyEvent = mkEvent({ event: true, type: "anything", user: "anyone", content: {} });
|
||||
vm.setReplyToEvent(replyEvent);
|
||||
const threadRelation: IEventRelation = { key: "foo" };
|
||||
vm.setThreadRelation(threadRelation);
|
||||
const files = [
|
||||
{
|
||||
name: "fake.png",
|
||||
size: 1024,
|
||||
type: "image/png",
|
||||
},
|
||||
] as unknown as FileList;
|
||||
await vm.initiateViaDataTransfer({ files } as DataTransfer);
|
||||
expect(sendContentListToRoomSpy).toHaveBeenCalledWith(
|
||||
files,
|
||||
room.roomId,
|
||||
threadRelation,
|
||||
replyEvent,
|
||||
client,
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user