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:
Will Hunt
2026-05-12 11:30:30 +00:00
committed by GitHub
parent 39607799de
commit aeaeb55cda
26 changed files with 768 additions and 490 deletions
@@ -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"],
@@ -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>