/* * 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 { expect } from "@jest/globals"; import type { MockedObject } from "jest-mock"; import type { MatrixClient, IPreviewUrlResponse } from "matrix-js-sdk/src/matrix"; import { UrlPreviewGroupViewModel } from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel"; import type { UrlPreview } from "@element-hq/web-shared-components"; import { getMockClientWithEventEmitter, mkEvent } from "../../test-utils"; const IMAGE_MXC = "mxc://example.org/abc"; 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 getViewModel({ mediaVisible, visible } = { mediaVisible: true, visible: true }): { vm: UrlPreviewGroupViewModel; client: MockedObject; onImageClicked: jest.Mock; } { const client = getMockClientWithEventEmitter({ getUrlPreview: jest.fn(), mxcUrlToHttp: jest.fn(), }); const onImageClicked = jest.fn(); const vm = new UrlPreviewGroupViewModel({ client, mediaVisible, visible, onImageClicked, mxEvent: mkEvent({ event: true, user: "@foo:bar", type: "m.room.message", content: {}, id: "$id", }), }); return { vm, client, onImageClicked }; } describe("UrlPreviewGroupViewModel", () => { it("should return no previews by default", () => { expect(getViewModel().vm.getSnapshot()).toMatchSnapshot(); }); it("should preview a single valid URL", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); }); it("should preview nested URLs but ignore some element types", async () => { const { vm, client } = getViewModel(); vm.onTogglePreviewLimit(); client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); const msg = document.createElement("div"); msg.innerHTML = `
Test4
Test5
Test6
`; await vm.updateEventElement(msg); const { previews } = vm.getSnapshot(); expect(previews).toHaveLength(3); expect(previews).toMatchObject([ { link: "https://example.org/1", }, { link: "https://example.org/2", }, { link: "https://example.org/3", }, ]); }); it("should hide preview when invisible", async () => { const { vm, client } = getViewModel({ visible: false, mediaVisible: true }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); expect(client.getUrlPreview).not.toHaveBeenCalled(); }); it("should preview a URL with media", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ "og:title": "This is an example!", "og:type": "document", "og:url": "https://example.org", "og:image": IMAGE_MXC, "og:image:height": 128, "og:image:width": 128, "matrix:image:size": 10000, }); // eslint-disable-next-line no-restricted-properties client.mxcUrlToHttp.mockImplementation((url, width) => { expect(url).toEqual(IMAGE_MXC); if (width) { return "https://example.org/image/thumb"; } return "https://example.org/image/src"; }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); }); it.each>([ { "matrix:image:size": 8191 }, { "og:image:width": 95 }, { "og:image:height": 95 }, ])("should preview a URL with a site icon", async (extraResp) => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ "og:title": "This is an example!", "og:type": "document", "og:url": "https://example.org", "og:image": IMAGE_MXC, "og:image:height": 128, "og:image:width": 128, "matrix:image:size": 8193, ...extraResp, }); // eslint-disable-next-line no-restricted-properties client.mxcUrlToHttp.mockImplementation((url) => { expect(url).toEqual(IMAGE_MXC); return "https://example.org/image/src"; }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0].siteIcon).toBeTruthy(); }); it("should ignore media when mediaVisible is false", async () => { const { vm, client } = getViewModel({ mediaVisible: false, visible: true }); client.getUrlPreview.mockResolvedValueOnce({ "og:title": "This is an example!", "og:type": "document", "og:url": "https://example.org", "og:image": IMAGE_MXC, "og:image:height": 128, "og:image:width": 128, "matrix:image:size": 10000, }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); // eslint-disable-next-line no-restricted-properties expect(client.mxcUrlToHttp).not.toHaveBeenCalled(); }); it("should deduplicate multiple versions of the same URL", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); const msg = document.createElement("div"); msg.innerHTML = 'TestTestTest'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); expect(client.getUrlPreview).toHaveBeenCalledTimes(1); }); it("should ignore failed previews", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockRejectedValue(new Error("Forced test failure")); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot()).toMatchSnapshot(); }); it("should handle image clicks", async () => { const { vm, client, onImageClicked } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ "og:title": "This is an example!", "og:type": "document", "og:url": "https://example.org", "og:image": IMAGE_MXC, "og:image:height": 128, "og:image:width": 128, "matrix:image:size": 10000, }); // eslint-disable-next-line no-restricted-properties client.mxcUrlToHttp.mockImplementation((url, width) => { expect(url).toEqual(IMAGE_MXC); if (width) { return "https://example.org/image/thumb"; } return "https://example.org/image/src"; }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); const { previews } = vm.getSnapshot(); vm.onImageClick(previews[0]); expect(onImageClicked).toHaveBeenCalled(); }); it("should handle being hidden and shown by the user", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); await vm.onHideClick(); expect(vm.getSnapshot()).toMatchSnapshot(); await vm.onShowClick(); expect(vm.getSnapshot()).toMatchSnapshot(); }); describe("calculates author", () => { it("should use the profile:username if provided", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "profile:username": "my username" }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0].author).toEqual("my username"); }); it("should use author if the og:type is an article", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "og:type": "article", "article:author": "my name", }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0].author).toEqual("my name"); }); it("should NOT use author if the author is a URL", async () => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "og:type": "article", "article:author": "https://junk.example.org/foo", }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0].author).toBeUndefined(); }); }); it.each([ { text: "", href: "", hasPreview: false }, { text: "test", href: "noprotocol.example.org", hasPreview: false }, { text: "matrix link", href: "https://matrix.to", hasPreview: false }, { text: "email", href: "mailto:example.org", hasPreview: false }, { text: "", href: "https://example.org", hasPreview: true }, ])("handles different kinds of links %s", async (item) => { const { vm, client } = getViewModel(); client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); const msg = document.createElement("div"); msg.innerHTML = `${item.text}`; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews).toHaveLength(item.hasPreview ? 1 : 0); }); // og:url, og:type are ignored. const baseOg = { "og:url": "https://example.org", "og:type": "document", }; it.each([ { ...baseOg, "og:title": "Basic title" }, { ...baseOg, "og:site_name": "Site name", "og:title": "" }, { ...baseOg, "og:description": "A description", "og:title": "" }, { ...baseOg, "og:title": "Cool blog", "og:site_name": "Cool site" }, { ...baseOg, "og:title": "Media test", // API *may* return a string, so check we parse correctly. "og:image:height": "500" as unknown as number, "og:image:width": 500, "matrix:image:size": 10000, "og:image": IMAGE_MXC, }, ])("handles different kinds of opengraph responses %s", async (og) => { const { vm, client } = getViewModel(); // eslint-disable-next-line no-restricted-properties client.mxcUrlToHttp.mockImplementation((url, width) => { expect(url).toEqual(IMAGE_MXC); if (width) { return "https://example.org/image/thumb"; } return "https://example.org/image/src"; }); client.getUrlPreview.mockResolvedValueOnce(og); const msg = document.createElement("div"); msg.innerHTML = `test`; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0]).toMatchSnapshot(); }); it.each(["og:video", "og:video:type", "og:audio"])("detects playable links via %s", async (property) => { const { vm, client } = getViewModel(); // eslint-disable-next-line no-restricted-properties client.mxcUrlToHttp.mockImplementation((url, width) => { expect(url).toEqual(IMAGE_MXC); if (width) { return "https://example.org/image/thumb"; } return "https://example.org/image/src"; }); client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "og:image": IMAGE_MXC, [property]: "anything", }); const msg = document.createElement("div"); msg.innerHTML = `test`; await vm.updateEventElement(msg); expect(vm.getSnapshot().previews[0].image?.playable).toEqual(true); }); });