Files
ThreadNet-Web/apps/web/src/components/views/rooms/MessageComposerUrlPreview.test.tsx
T
SiriusandGitHub 84653bb416 Sending URL Preview Bundles (MSC 4095) (#34150)
* Fetches link previews for all links in message (instead of just the first one)

* message url component to display preview for multiple urls

* Revert visible behaviour back to only showing one URL preview

While fetching all link previews in the text message.

* Moved URL preview VM to MessageComposer

* Added com.beeper.linkpreviews to messages sent with the markdown compositor

* MSC 4452 implemented for the older (markdown) composer

* claude told me to use logger instead of console

* Previews generated with snapshot instead of vm

* moved attachPreviews to a separate file

* added attach URL previews to rich text editor

* don't let attachUrlPreviews block clearning the composer

* fixed linter errors

* moved url preview behind labs feature gate

* passed linters

* moved lab feature checking to where attach preview is used

* claude wrote some unit tests for url previewing

* fixed linter errors

* added feature to labs.md

* fixed linter errors

* fixed oxfmt error

* set previews to none if all previews failed

* resolved PR reviews besides the ones that requires a larger code change

* moved url preview VM creation to message composer

* removed delay when clearing composer url preview on message send

* minor changes to stop using globals everywhere

* moved debouncing responsibility to urlpreviewVM

* minor lint fixes

* added comment

* remove composer content from the state of urlpreview, moved it to the vm

* urlpreviewwrapper depends on the vm only

* added comments

* edited the comment

* updated snapshots

* claude fixed tests

* enable feature for tests that requires MSC4095

* updated snapshot
2026-07-13 15:31:18 +00:00

160 lines
5.7 KiB
TypeScript

/*
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 React from "react";
import { render, waitFor } from "test-utils-rtl";
import { test, describe, beforeEach, expect, vi, afterEach } from "vitest";
import { MessageComposerUrlPreviewWrapper } from "./MessageComposerUrlPreview";
import {
getMockClientWithEventEmitter,
getRoomContext,
mkRoom,
mockClientMethodsUser,
} from "../../../../test/test-utils";
import type { I18nApi } from "@element-hq/element-web-module-api";
import type { ModuleApi } from "../../../modules/Api";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { CustomComponentsApi } from "../../../modules/customComponentApi";
import {
DEBOUNCE_REQUEST_TIMEOUT_MS,
MessageComposerUrlPreviewViewModel,
type MessageComposerUrlPreviewViewModelProps,
} from "../../../viewmodels/composer/MessageComposerUrlPreviewViewModel";
// @vitest-environment happy-dom
const BASIC_PREVIEW_OGDATA = {
"og:title": "This is an example!",
"og:description": "This is a description",
"og:type": "document",
"og:url": "https://example.org",
"og:site_name": "Example.org",
};
function getUrlPreviewVm(client: MatrixClient, content?: string): MessageComposerUrlPreviewViewModel {
const props: MessageComposerUrlPreviewViewModelProps = {
client,
visible: true,
showTooltips: false,
urlPreviewBundle: false,
};
if (content !== undefined) {
props.content = content;
}
const vm = new MessageComposerUrlPreviewViewModel(props);
if (content !== undefined) {
// Mirror how MessageComposer drives the view model so previews are actually computed.
void vm.updateWithText({ content, debounced: false });
}
return vm;
}
describe("MessageComposerUrlPreview", () => {
let client: MatrixClient;
let originalMxModuleApi: ModuleApi;
beforeEach(() => {
originalMxModuleApi = window.mxModuleApi;
window.mxModuleApi = {
i18n: {} as I18nApi,
} as ModuleApi;
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
getUrlPreview: vi.fn().mockResolvedValue(BASIC_PREVIEW_OGDATA),
});
});
afterEach(() => {
window.mxModuleApi = originalMxModuleApi;
});
function wrapComponent(component: Parameters<typeof render>[0]): ReturnType<typeof render> {
return render(component, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider
roomId="!foo:bar"
{...getRoomContext(mkRoom(client, "!foo:bar"), { showUrlPreview: true })}
>
{children}
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
),
});
}
test("to be empty without a link to preview", () => {
const { container } = wrapComponent(
<MessageComposerUrlPreviewWrapper urlPreviewVm={getUrlPreviewVm(client, "Test a string")} />,
);
expect(container).toMatchInlineSnapshot(`<div />`);
});
test("to contain a link when there is a URL", async () => {
const { getByText } = wrapComponent(
<MessageComposerUrlPreviewWrapper urlPreviewVm={getUrlPreviewVm(client, "https://example.org")} />,
);
await waitFor(
() => {
expect(getByText("Example.org")).toBeDefined();
},
{ timeout: DEBOUNCE_REQUEST_TIMEOUT_MS },
);
});
test("to allow overriding with a module component", async () => {
const modApi = {
customComponents: new CustomComponentsApi(),
} as ModuleApi;
modApi.customComponents.registerComposerPreview(
() => true,
() => <strong>Fake preview</strong>,
);
const { getByText } = wrapComponent(
<MessageComposerUrlPreviewWrapper
moduleApi={modApi}
urlPreviewVm={getUrlPreviewVm(client, "https://example.org")}
/>,
);
await waitFor(
() => {
expect(getByText("Fake preview")).toBeDefined();
},
{ timeout: DEBOUNCE_REQUEST_TIMEOUT_MS },
);
});
test("to reset module component override when filter function does not match", async () => {
const modApi = {
customComponents: new CustomComponentsApi(),
} as ModuleApi;
modApi.customComponents.registerComposerPreview(
(text) => text === "show-fake-preview",
() => <strong>Fake preview</strong>,
);
const { container, getByText, queryByText, rerender } = wrapComponent(
<MessageComposerUrlPreviewWrapper
urlPreviewVm={getUrlPreviewVm(client, "show-fake-preview")}
moduleApi={modApi}
/>,
);
await waitFor(() => {
expect(getByText("Fake preview")).toBeDefined();
});
rerender(
<MessageComposerUrlPreviewWrapper
urlPreviewVm={getUrlPreviewVm(client, "no-longer-matching")}
moduleApi={modApi}
/>,
);
await waitFor(() => {
expect(queryByText("Fake preview")).toBeNull();
});
expect(container).toMatchInlineSnapshot(`<div />`);
});
});