Fix "[object Promise]" appearing in HTML exports (#9975)
Fixes https://github.com/vector-im/element-web/issues/24272
This commit is contained in:
parent
3e2bf5640e
commit
4c1e4f5127
13 changed files with 895 additions and 84 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,26 +14,101 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, IRoomEvent, MatrixClient, MatrixEvent, MsgType, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
|
||||
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
|
||||
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import HTMLExporter from "../../../src/utils/exportUtils/HtmlExport";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { mediaFromMxc } from "../../../src/customisations/Media";
|
||||
|
||||
jest.mock("jszip");
|
||||
|
||||
const EVENT_MESSAGE: IRoomEvent = {
|
||||
event_id: "$1",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@bob:example.com",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Message",
|
||||
avatar_url: "mxc://example.org/avatar.bmp",
|
||||
},
|
||||
};
|
||||
|
||||
const EVENT_ATTACHMENT: IRoomEvent = {
|
||||
event_id: "$2",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
msgtype: MsgType.File,
|
||||
body: "hello.txt",
|
||||
filename: "hello.txt",
|
||||
url: "mxc://example.org/test-id",
|
||||
},
|
||||
};
|
||||
|
||||
describe("HTMLExport", () => {
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
|
||||
filterConsole(
|
||||
"Starting export",
|
||||
"events in", // Fetched # events in # seconds
|
||||
"events so far",
|
||||
"Export successful!",
|
||||
"does not have an m.room.create event",
|
||||
"Creating HTML",
|
||||
"Generating a ZIP",
|
||||
"Cleaning up",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
|
||||
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||
DMRoomMap.makeShared();
|
||||
|
||||
room = new Room("!myroom:example.org", client, "@me:example.org");
|
||||
client.getRoom.mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(SdkConfig.get).mockRestore();
|
||||
});
|
||||
function mockMessages(...events: IRoomEvent[]): void {
|
||||
client.createMessagesRequest.mockImplementation((_roomId, fromStr, limit = 30) => {
|
||||
const from = fromStr === null ? 0 : parseInt(fromStr);
|
||||
const chunk = events.slice(from, limit);
|
||||
return Promise.resolve({
|
||||
chunk,
|
||||
from: from.toString(),
|
||||
to: (from + limit).toString(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Retrieve a map of files within the zip. */
|
||||
function getFiles(exporter: HTMLExporter): { [filename: string]: Blob } {
|
||||
//@ts-ignore private access
|
||||
const files = exporter.files;
|
||||
return files.reduce((d, f) => ({ ...d, [f.name]: f.blob }), {});
|
||||
}
|
||||
|
||||
function getMessageFile(exporter: HTMLExporter): Blob {
|
||||
const files = getFiles(exporter);
|
||||
return files["messages.html"]!;
|
||||
}
|
||||
|
||||
/** set a mock fetch response for an MXC */
|
||||
function mockMxc(mxc: string, body: string) {
|
||||
const media = mediaFromMxc(mxc, client);
|
||||
fetchMock.get(media.srcHttp, body);
|
||||
}
|
||||
|
||||
it("should have an SDK-branded destination file name", () => {
|
||||
const roomName = "My / Test / Room: Welcome";
|
||||
const client = createTestClient();
|
||||
const stubOptions: IExportOptions = {
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 50000000,
|
||||
|
@ -43,10 +118,201 @@ describe("HTMLExport", () => {
|
|||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation(() => {
|
||||
return { brand: "BrandedChat/WithSlashes/ForFun" };
|
||||
});
|
||||
SdkConfig.put({ brand: "BrandedChat/WithSlashes/ForFun" });
|
||||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should export", async () => {
|
||||
const events = [...Array(50)].map<IRoomEvent>((_, i) => ({
|
||||
event_id: `${i}`,
|
||||
type: EventType.RoomMessage,
|
||||
sender: `@user${i}:example.com`,
|
||||
origin_server_ts: 5_000 + i * 1000,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: `Message #${i}`,
|
||||
},
|
||||
}));
|
||||
mockMessages(...events);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: events.length,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const file = getMessageFile(exporter);
|
||||
expect(await file.text()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should include the room's avatar", async () => {
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
|
||||
const mxc = "mxc://www.example.com/avatars/nice-room.jpeg";
|
||||
const avatar = "011011000110111101101100";
|
||||
jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(mxc);
|
||||
mockMxc(mxc, avatar);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const files = getFiles(exporter);
|
||||
expect(await files["room.png"]!.text()).toBe(avatar);
|
||||
});
|
||||
|
||||
it("should include the creation event", async () => {
|
||||
const creator = "@bob:example.com";
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomCreate,
|
||||
event_id: "$00001",
|
||||
room_id: room.roomId,
|
||||
sender: creator,
|
||||
origin_server_ts: 0,
|
||||
content: {},
|
||||
state_key: "",
|
||||
}),
|
||||
]);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
expect(await getMessageFile(exporter).text()).toContain(`${creator} created this room.`);
|
||||
});
|
||||
|
||||
it("should include the topic", async () => {
|
||||
const topic = ":^-) (-^:";
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomTopic,
|
||||
event_id: "$00001",
|
||||
room_id: room.roomId,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 0,
|
||||
content: { topic },
|
||||
state_key: "",
|
||||
}),
|
||||
]);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
expect(await getMessageFile(exporter).text()).toContain(`Topic: ${topic}`);
|
||||
});
|
||||
|
||||
it("should include avatars", async () => {
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
|
||||
jest.spyOn(RoomMember.prototype, "getMxcAvatarUrl").mockReturnValue("mxc://example.org/avatar.bmp");
|
||||
|
||||
const avatarContent = "this is a bitmap all the pixels are red :^-)";
|
||||
mockMxc("mxc://example.org/avatar.bmp", avatarContent);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the avatar is present
|
||||
const files = getFiles(exporter);
|
||||
const file = files["users/@bob-example.com.png"];
|
||||
expect(file).not.toBeUndefined();
|
||||
|
||||
// Ensure it has the expected content
|
||||
expect(await file.text()).toBe(avatarContent);
|
||||
});
|
||||
|
||||
it("should include attachments", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||
const attachmentBody = "Lorem ipsum dolor sit amet";
|
||||
|
||||
mockMxc("mxc://example.org/test-id", attachmentBody);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the attachment is present
|
||||
const files = getFiles(exporter);
|
||||
const file = files["files/hello-1-1-1970 at 12-00-00 AM.txt"];
|
||||
expect(file).not.toBeUndefined();
|
||||
|
||||
// Ensure that the attachment has the expected content
|
||||
const text = await file.text();
|
||||
expect(text).toBe(attachmentBody);
|
||||
});
|
||||
|
||||
it("should omit attachments", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the attachment is present
|
||||
const files = getFiles(exporter);
|
||||
for (const fileName of Object.keys(files)) {
|
||||
expect(fileName).not.toMatch(/^files\/hello/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because one or more lines are too long
26
test/utils/exportUtils/exportCSS-test.ts
Normal file
26
test/utils/exportUtils/exportCSS-test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import getExportCSS from "../../../src/utils/exportUtils/exportCSS";
|
||||
|
||||
describe("exportCSS", () => {
|
||||
describe("getExportCSS", () => {
|
||||
it("supports documents missing stylesheets", async () => {
|
||||
const css = await getExportCSS(new Set());
|
||||
expect(css).not.toContain("color-scheme: light");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue