diff --git a/test/utils/exportUtils/HTMLExport-test.ts b/test/utils/exportUtils/HTMLExport-test.ts index 89a0ddedf50..607dfbdbdc7 100644 --- a/test/utils/exportUtils/HTMLExport-test.ts +++ b/test/utils/exportUtils/HTMLExport-test.ts @@ -14,18 +14,57 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, IRoomEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, IRoomEvent, MatrixClient, MatrixEvent, MsgType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import fetchMock from "fetch-mock-jest"; -import { mkStubRoom, REPEATABLE_DATE, stubClient } 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; + 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(); @@ -33,13 +72,39 @@ describe("HTMLExport", () => { client = stubClient() as jest.Mocked; DMRoomMap.makeShared(); + + room = new Room("!myroom:example.org", client, "@me:example.org"); + client.getRoom.mockReturnValue(room); }); - function getMessageFile(exporter: HTMLExporter): Blob { + 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; - const file = files.find((f) => f.name == "messages.html")!; - return file.blob; + 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", () => { @@ -59,10 +124,8 @@ describe("HTMLExport", () => { }); it("should export", async () => { - const room = new Room("!myroom:example.org", client, "@me:example.org"); - const events = [...Array(50)].map((_, i) => ({ - event_id: "$1", + event_id: `${i}`, type: EventType.RoomMessage, sender: `@user${i}:example.com`, origin_server_ts: 5_000 + i * 1000, @@ -71,9 +134,7 @@ describe("HTMLExport", () => { body: `Message #${i}`, }, })); - - client.getRoom.mockReturnValue(room); - client.createMessagesRequest.mockResolvedValue({ chunk: events }); + mockMessages(...events); const exporter = new HTMLExporter( room, @@ -91,4 +152,167 @@ describe("HTMLExport", () => { 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/); + } + }); }); diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap index af50c8d604d..c47170d3eda 100644 --- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap +++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap @@ -65,7 +65,7 @@ exports[`HTMLExport should export 1`] = `

-
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0
  • +
  • @user49:example.com
    Message #49
  • @user48:example.com
    Message #48
  • @user47:example.com
    Message #47
  • @user46:example.com
    Message #46
  • @user45:example.com
    Message #45
  • @user44:example.com
    Message #44
  • @user43:example.com
    Message #43
  • @user42:example.com
    Message #42
  • @user41:example.com
    Message #41
  • @user40:example.com
    Message #40
  • @user39:example.com
    Message #39
  • @user38:example.com
    Message #38
  • @user37:example.com
    Message #37
  • @user36:example.com
    Message #36
  • @user35:example.com
    Message #35
  • @user34:example.com
    Message #34
  • @user33:example.com
    Message #33
  • @user32:example.com
    Message #32
  • @user31:example.com
    Message #31
  • @user30:example.com
    Message #30
  • @user29:example.com
    Message #29
  • @user28:example.com
    Message #28
  • @user27:example.com
    Message #27
  • @user26:example.com
    Message #26
  • @user25:example.com
    Message #25
  • @user24:example.com
    Message #24
  • @user23:example.com
    Message #23
  • @user22:example.com
    Message #22
  • @user21:example.com
    Message #21
  • @user20:example.com
    Message #20
  • @user19:example.com
    Message #19
  • @user18:example.com
    Message #18
  • @user17:example.com
    Message #17
  • @user16:example.com
    Message #16
  • @user15:example.com
    Message #15
  • @user14:example.com
    Message #14
  • @user13:example.com
    Message #13
  • @user12:example.com
    Message #12
  • @user11:example.com
    Message #11
  • @user10:example.com
    Message #10
  • @user9:example.com
    Message #9
  • @user8:example.com
    Message #8
  • @user7:example.com
    Message #7
  • @user6:example.com
    Message #6
  • @user5:example.com
    Message #5
  • @user4:example.com
    Message #4
  • @user3:example.com
    Message #3
  • @user2:example.com
    Message #2
  • @user1:example.com
    Message #1
  • @user0:example.com
    Message #0