element-web/test/utils/export-test.tsx

342 lines
11 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "@testing-library/react";
import {
IContent,
MatrixClient,
MatrixEvent,
Room,
RoomMember,
RelationType,
EventType,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import { IExportOptions, ExportType, ExportFormat } from "../../src/utils/exportUtils/exportUtils";
import PlainTextExporter from "../../src/utils/exportUtils/PlainTextExport";
import HTMLExporter from "../../src/utils/exportUtils/HtmlExport";
import * as TestUtilsMatrix from "../test-utils";
import { stubClient } from "../test-utils";
let client: MatrixClient;
const MY_USER_ID = "@me:here";
function generateRoomId() {
return "!" + Math.random().toString().slice(2, 10) + ":domain";
}
interface ITestContent extends IContent {
expectedText: string;
}
describe("export", function () {
const setProgressText = jest.fn();
let mockExportOptions: IExportOptions;
let mockRoom: Room;
let ts0: number;
let events: MatrixEvent[];
beforeEach(() => {
stubClient();
client = MatrixClientPeg.safeGet();
client.getUserId = () => {
return MY_USER_ID;
};
mockExportOptions = {
numberOfMessages: 5,
maxSize: 100 * 1024 * 1024,
attachmentsIncluded: false,
};
function createRoom() {
const room = new Room(generateRoomId(), client, client.getUserId()!);
return room;
}
mockRoom = createRoom();
ts0 = Date.now();
events = mkEvents();
jest.spyOn(client, "getRoom").mockReturnValue(mockRoom);
});
function mkRedactedEvent(i = 0) {
return new MatrixEvent({
type: "m.room.message",
sender: MY_USER_ID,
content: {},
unsigned: {
age: 72,
transaction_id: "m1212121212.23",
redacted_because: {
content: {},
origin_server_ts: ts0 + i * 1000,
redacts: "$9999999999999999999999999999999999999999998",
sender: "@me:here",
type: EventType.RoomRedaction,
unsigned: {
age: 94,
transaction_id: "m1111111111.1",
},
event_id: "$9999999999999999999999999999999999999999998",
room_id: mockRoom.roomId,
},
},
event_id: "$9999999999999999999999999999999999999999999",
room_id: mockRoom.roomId,
});
}
function mkFileEvent() {
return new MatrixEvent({
content: {
body: "index.html",
info: {
mimetype: "text/html",
size: 31613,
},
msgtype: "m.file",
url: "mxc://test.org",
},
origin_server_ts: 1628872988364,
sender: MY_USER_ID,
type: "m.room.message",
unsigned: {
age: 266,
transaction_id: "m99999999.2",
},
event_id: "$99999999999999999999",
room_id: mockRoom.roomId,
});
}
function mkImageEvent() {
return new MatrixEvent({
content: {
body: "image.png",
info: {
mimetype: "image/png",
size: 31613,
},
msgtype: "m.image",
url: "mxc://test.org",
},
origin_server_ts: 1628872988364,
sender: MY_USER_ID,
type: "m.room.message",
unsigned: {
age: 266,
transaction_id: "m99999999.2",
},
event_id: "$99999999999999999999",
room_id: mockRoom.roomId,
});
}
function mkEvents() {
const matrixEvents: MatrixEvent[] = [];
let i: number;
// plain text
for (i = 0; i < 10; i++) {
matrixEvents.push(
TestUtilsMatrix.mkMessage({
event: true,
room: "!room:id",
user: "@user:id",
ts: ts0 + i * 1000,
}),
);
}
// reply events
for (i = 0; i < 10; i++) {
const eventId = "$" + Math.random() + "-" + Math.random();
matrixEvents.push(
TestUtilsMatrix.mkEvent({
content: {
"body": "> <@me:here> Hi\n\nTest",
"format": "org.matrix.custom.html",
"m.relates_to": {
"rel_type": RelationType.Reference,
"event_id": eventId,
"m.in_reply_to": {
event_id: eventId,
},
},
"msgtype": "m.text",
},
user: "@me:here",
type: "m.room.message",
room: mockRoom.roomId,
event: true,
}),
);
}
// membership events
for (i = 0; i < 10; i++) {
matrixEvents.push(
TestUtilsMatrix.mkMembership({
event: true,
room: "!room:id",
user: "@user:id",
target: {
userId: "@user:id",
name: "Bob",
getAvatarUrl: () => {
return "avatar.jpeg";
},
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
} as unknown as RoomMember,
ts: ts0 + i * 1000,
mship: KnownMembership.Join,
prevMship: KnownMembership.Join,
name: "A user",
}),
);
}
// emote
matrixEvents.push(
TestUtilsMatrix.mkEvent({
content: {
body: "waves",
msgtype: "m.emote",
},
user: "@me:here",
type: "m.room.message",
room: mockRoom.roomId,
event: true,
}),
);
// redacted events
for (i = 0; i < 10; i++) {
matrixEvents.push(mkRedactedEvent(i));
}
return matrixEvents;
}
function renderToString(elem: JSX.Element): string {
return render(elem).container.outerHTML;
}
it("checks if the export format is valid", function () {
function isValidFormat(format: string): boolean {
const options: string[] = Object.values(ExportFormat);
return options.includes(format);
}
expect(isValidFormat("Html")).toBeTruthy();
expect(isValidFormat("Json")).toBeTruthy();
expect(isValidFormat("PlainText")).toBeTruthy();
expect(isValidFormat("Pdf")).toBeFalsy();
});
it("checks if the icons' html corresponds to export regex", function () {
const exporter = new HTMLExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
const fileRegex = /<span class="mx_MFileBody_info_icon">.*?<\/span>/;
expect(fileRegex.test(renderToString(exporter.getEventTile(mkFileEvent(), true)))).toBeTruthy();
});
it("should export images if attachments are enabled", () => {
const exporter = new HTMLExporter(
mockRoom,
ExportType.Beginning,
{
numberOfMessages: 5,
maxSize: 100 * 1024 * 1024,
attachmentsIncluded: true,
},
setProgressText,
);
const imageRegex = /<img.+ src="mxc:\/\/test.org" alt="image\.png"\/?>/;
expect(imageRegex.test(renderToString(exporter.getEventTile(mkImageEvent(), true)))).toBeTruthy();
});
const invalidExportOptions: [string, IExportOptions][] = [
[
"numberOfMessages exceeds max",
{
numberOfMessages: 10 ** 9,
maxSize: 1024 * 1024 * 1024,
attachmentsIncluded: false,
},
],
[
"maxSize exceeds 8GB",
{
numberOfMessages: -1,
maxSize: 8001 * 1024 * 1024,
attachmentsIncluded: false,
},
],
[
"maxSize is less than 1mb",
{
numberOfMessages: 0,
maxSize: 0,
attachmentsIncluded: false,
},
],
];
it.each(invalidExportOptions)("%s", (_d, options) => {
expect(() => new PlainTextExporter(mockRoom, ExportType.Beginning, options, setProgressText)).toThrow(
"Invalid export options",
);
});
it("tests the file extension splitter", function () {
const exporter = new PlainTextExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
const fileNameWithExtensions: Record<string, [string, string]> = {
"": ["", ""],
"name": ["name", ""],
"name.txt": ["name", ".txt"],
".htpasswd": ["", ".htpasswd"],
"name.with.many.dots.myext": ["name.with.many.dots", ".myext"],
};
for (const fileName in fileNameWithExtensions) {
expect(exporter.splitFileName(fileName)).toStrictEqual(fileNameWithExtensions[fileName]);
}
});
it("checks if the reply regex executes correctly", function () {
const eventContents: ITestContent[] = [
{
msgtype: "m.text",
body: "> <@me:here> Source\n\nReply",
expectedText: '<@me:here "Source"> Reply',
},
{
msgtype: "m.text",
// if the reply format is invalid, then return the body
body: "Invalid reply format",
expectedText: "Invalid reply format",
},
{
msgtype: "m.text",
body: "> <@me:here> The source is more than 32 characters\n\nReply",
expectedText: '<@me:here "The source is more than 32 chara..."> Reply',
},
{
msgtype: "m.text",
body: "> <@me:here> This\nsource\nhas\nnew\nlines\n\nReply",
expectedText: '<@me:here "This"> Reply',
},
];
const exporter = new PlainTextExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
for (const content of eventContents) {
expect(exporter.textForReplyEvent(content)).toBe(content.expectedText);
}
});
it("checks if the render to string doesn't throw any error for different types of events", function () {
const exporter = new HTMLExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
for (const event of events) {
expect(renderToString(exporter.getEventTile(event, false))).toBeTruthy();
}
});
});