mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
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
@ -175,7 +175,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean {
|
||||
return prevDate.getFullYear() === nextDate.getFullYear();
|
||||
}
|
||||
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ const groupedStateEvents = [
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
export function shouldFormContinuation(
|
||||
prevEvent: MatrixEvent,
|
||||
prevEvent: MatrixEvent | null,
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
threadsEnabled: boolean,
|
||||
@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
// here.
|
||||
return !this.props.canBackPaginate;
|
||||
}
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate);
|
||||
}
|
||||
|
||||
// Get a list of read receipts that should be shown next to this event
|
||||
|
@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
|
||||
}
|
||||
const baseEventId = this.props.mxEvent.getId();
|
||||
allEvents.forEach((e, i) => {
|
||||
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
|
||||
if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) {
|
||||
nodes.push(
|
||||
<li key={e.getTs() + "~"}>
|
||||
<DateSeparator roomId={e.getRoomId()} ts={e.getTs()} />
|
||||
|
@ -84,7 +84,7 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||
// is this a continuation of the previous message?
|
||||
const continuation =
|
||||
prevEv &&
|
||||
!wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) &&
|
||||
!wantsDateSeparator(prevEv.getDate() || undefined, mxEv.getDate() || undefined) &&
|
||||
shouldFormContinuation(
|
||||
prevEv,
|
||||
mxEv,
|
||||
@ -96,7 +96,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||
let lastInSection = true;
|
||||
const nextEv = timeline[j + 1];
|
||||
if (nextEv) {
|
||||
const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate());
|
||||
const willWantDateSeparator = wantsDateSeparator(
|
||||
mxEv.getDate() || undefined,
|
||||
nextEv.getDate() || undefined,
|
||||
);
|
||||
lastInSection =
|
||||
willWantDateSeparator ||
|
||||
mxEv.getSender() !== nextEv.getSender() ||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021, 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.
|
||||
@ -66,7 +66,7 @@ export default class HTMLExporter extends Exporter {
|
||||
}
|
||||
|
||||
protected async getRoomAvatar(): Promise<ReactNode> {
|
||||
let blob: Blob;
|
||||
let blob: Blob | undefined = undefined;
|
||||
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
|
||||
const avatarPath = "room.png";
|
||||
if (avatarUrl) {
|
||||
@ -85,7 +85,7 @@ export default class HTMLExporter extends Exporter {
|
||||
height={32}
|
||||
name={this.room.name}
|
||||
title={this.room.name}
|
||||
url={blob ? avatarPath : null}
|
||||
url={blob ? avatarPath : ""}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
);
|
||||
@ -96,9 +96,9 @@ export default class HTMLExporter extends Exporter {
|
||||
const roomAvatar = await this.getRoomAvatar();
|
||||
const exportDate = formatFullDateNoDayNoTime(new Date());
|
||||
const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator;
|
||||
const exporter = this.client.getUserId();
|
||||
const exporterName = this.room?.getMember(exporter)?.rawDisplayName;
|
||||
const creatorName = (creator ? this.room.getMember(creator)?.rawDisplayName : creator) || creator;
|
||||
const exporter = this.client.getUserId()!;
|
||||
const exporterName = this.room.getMember(exporter)?.rawDisplayName;
|
||||
const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || "";
|
||||
const createdText = _t("%(creatorName)s created this room.", {
|
||||
creatorName,
|
||||
@ -217,20 +217,19 @@ export default class HTMLExporter extends Exporter {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
protected getAvatarURL(event: MatrixEvent): string {
|
||||
protected getAvatarURL(event: MatrixEvent): string | undefined {
|
||||
const member = event.sender;
|
||||
return (
|
||||
member.getMxcAvatarUrl() && mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(30, 30, "crop")
|
||||
);
|
||||
const avatarUrl = member?.getMxcAvatarUrl();
|
||||
return avatarUrl ? mediaFromMxc(avatarUrl).getThumbnailOfSourceHttp(30, 30, "crop") : undefined;
|
||||
}
|
||||
|
||||
protected async saveAvatarIfNeeded(event: MatrixEvent): Promise<void> {
|
||||
const member = event.sender;
|
||||
const member = event.sender!;
|
||||
if (!this.avatars.has(member.userId)) {
|
||||
try {
|
||||
const avatarUrl = this.getAvatarURL(event);
|
||||
this.avatars.set(member.userId, true);
|
||||
const image = await fetch(avatarUrl);
|
||||
const image = await fetch(avatarUrl!);
|
||||
const blob = await image.blob();
|
||||
this.addFile(`users/${member.userId.replace(/:/g, "-")}.png`, blob);
|
||||
} catch (err) {
|
||||
@ -239,19 +238,19 @@ export default class HTMLExporter extends Exporter {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getDateSeparator(event: MatrixEvent): Promise<string> {
|
||||
protected getDateSeparator(event: MatrixEvent): string {
|
||||
const ts = event.getTs();
|
||||
const dateSeparator = (
|
||||
<li key={ts}>
|
||||
<DateSeparator forExport={true} key={ts} roomId={event.getRoomId()} ts={ts} />
|
||||
<DateSeparator forExport={true} key={ts} roomId={event.getRoomId()!} ts={ts} />
|
||||
</li>
|
||||
);
|
||||
return renderToStaticMarkup(dateSeparator);
|
||||
}
|
||||
|
||||
protected async needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent): Promise<boolean> {
|
||||
if (prevEvent == null) return true;
|
||||
return wantsDateSeparator(prevEvent.getDate(), event.getDate());
|
||||
protected needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent | null): boolean {
|
||||
if (!prevEvent) return true;
|
||||
return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined);
|
||||
}
|
||||
|
||||
public getEventTile(mxEv: MatrixEvent, continuation: boolean): JSX.Element {
|
||||
@ -264,9 +263,7 @@ export default class HTMLExporter extends Exporter {
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
forExport={true}
|
||||
readReceipts={null}
|
||||
alwaysShowTimestamps={true}
|
||||
readReceiptMap={null}
|
||||
showUrlPreview={false}
|
||||
checkUnmounting={() => false}
|
||||
isTwelveHour={false}
|
||||
@ -275,7 +272,6 @@ export default class HTMLExporter extends Exporter {
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
lastSuccessful={false}
|
||||
isSelectedEvent={false}
|
||||
getRelationsForEvent={null}
|
||||
showReactions={false}
|
||||
layout={Layout.Group}
|
||||
showReadReceipts={false}
|
||||
@ -286,7 +282,8 @@ export default class HTMLExporter extends Exporter {
|
||||
}
|
||||
|
||||
protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string): Promise<string> {
|
||||
const hasAvatar = !!this.getAvatarURL(mxEv);
|
||||
const avatarUrl = this.getAvatarURL(mxEv);
|
||||
const hasAvatar = !!avatarUrl;
|
||||
if (hasAvatar) await this.saveAvatarIfNeeded(mxEv);
|
||||
const EventTile = this.getEventTile(mxEv, continuation);
|
||||
let eventTileMarkup: string;
|
||||
@ -312,8 +309,8 @@ export default class HTMLExporter extends Exporter {
|
||||
eventTileMarkup = eventTileMarkup.replace(/<span class="mx_MFileBody_info_icon".*?>.*?<\/span>/, "");
|
||||
if (hasAvatar) {
|
||||
eventTileMarkup = eventTileMarkup.replace(
|
||||
encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&"),
|
||||
`users/${mxEv.sender.userId.replace(/:/g, "-")}.png`,
|
||||
encodeURI(avatarUrl).replace(/&/g, "&"),
|
||||
`users/${mxEv.sender!.userId.replace(/:/g, "-")}.png`,
|
||||
);
|
||||
}
|
||||
return eventTileMarkup;
|
||||
|
@ -58,8 +58,8 @@ const getExportCSS = async (usedClasses: Set<string>): Promise<string> => {
|
||||
|
||||
// If the light theme isn't loaded we will have to fetch & parse it manually
|
||||
if (!stylesheets.some(isLightTheme)) {
|
||||
const href = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href$="theme-light.css"]').href;
|
||||
stylesheets.push(await getRulesFromCssFile(href));
|
||||
const href = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href$="theme-light.css"]')?.href;
|
||||
if (href) stylesheets.push(await getRulesFromCssFile(href));
|
||||
}
|
||||
|
||||
let css = "";
|
||||
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 React from "react";
|
||||
import { render, RenderResult } from "@testing-library/react";
|
||||
import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { flushPromises, mkMessage, stubClient } from "../../../test-utils";
|
||||
import MessageEditHistoryDialog from "../../../../src/components/views/dialogs/MessageEditHistoryDialog";
|
||||
|
||||
describe("<MessageEditHistory />", () => {
|
||||
const roomId = "!aroom:example.com";
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||
event = mkMessage({
|
||||
event: true,
|
||||
user: "@user:example.com",
|
||||
room: "!room:example.com",
|
||||
msg: "My Great Message",
|
||||
});
|
||||
});
|
||||
|
||||
async function renderComponent(): Promise<RenderResult> {
|
||||
const result = render(<MessageEditHistoryDialog mxEvent={event} onFinished={jest.fn()} />);
|
||||
await flushPromises();
|
||||
return result;
|
||||
}
|
||||
|
||||
function mockEdits(...edits: { msg: string; ts: number | undefined }[]) {
|
||||
client.relations.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
events: edits.map(
|
||||
(e) =>
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
room_id: roomId,
|
||||
origin_server_ts: e.ts,
|
||||
content: {
|
||||
body: e.msg,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it("should match the snapshot", async () => {
|
||||
mockEdits({ msg: "My Great Massage", ts: 1234 });
|
||||
|
||||
const { container } = await renderComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should support events with ", async () => {
|
||||
mockEdits(
|
||||
{ msg: "My Great Massage", ts: undefined },
|
||||
{ msg: "My Great Massage?", ts: undefined },
|
||||
{ msg: "My Great Missage", ts: undefined },
|
||||
);
|
||||
|
||||
const { container } = await renderComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,322 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MessageEditHistory /> should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header mx_Dialog_headerWithCancel"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Message edits
|
||||
</h2>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
>
|
||||
<ul
|
||||
class="mx_MessageEditHistoryDialog_edits"
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
aria-label="Thu, Jan 1 1970"
|
||||
class="mx_DateSeparator"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
>
|
||||
Thu, Jan 1 1970
|
||||
</h2>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
00:00
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body"
|
||||
dir="auto"
|
||||
>
|
||||
My Great Massage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header mx_Dialog_headerWithCancel"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Message edits
|
||||
</h2>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomView_messageListWrapper"
|
||||
>
|
||||
<ol
|
||||
aria-live="polite"
|
||||
class="mx_RoomView_MessageList"
|
||||
>
|
||||
<ul
|
||||
class="mx_MessageEditHistoryDialog_edits"
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
aria-label=", NaN NaN"
|
||||
class="mx_DateSeparator"
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
>
|
||||
, NaN NaN
|
||||
</h2>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
NaN:NaN
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
My Great Massage
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
NaN:NaN
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body markdown-body"
|
||||
dir="auto"
|
||||
>
|
||||
<span>
|
||||
My Great M
|
||||
<span
|
||||
class="mx_EditHistoryMessage_deletion"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
ssage
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="mx_EventTile"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_line"
|
||||
>
|
||||
<span
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
NaN:NaN
|
||||
</span>
|
||||
<div
|
||||
class="mx_EventTile_content"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_body"
|
||||
dir="auto"
|
||||
>
|
||||
My Great Missage
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MessageActionBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
@ -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.
|
||||
@ -17,7 +17,7 @@ limitations under the License.
|
||||
import * as React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, type RenderResult } from "@testing-library/react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { stubClient } from "../../../test-utils";
|
||||
@ -26,6 +26,8 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
|
||||
const ROOM_ID = "!qPewotXpIctQySfjSy:localhost";
|
||||
|
||||
type Props = React.ComponentPropsWithoutRef<typeof SearchResultTile>;
|
||||
|
||||
describe("SearchResultTile", () => {
|
||||
beforeAll(() => {
|
||||
stubClient();
|
||||
@ -35,50 +37,72 @@ describe("SearchResultTile", () => {
|
||||
jest.spyOn(cli, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
function renderComponent(props: Partial<Props>): RenderResult {
|
||||
return render(<SearchResultTile timeline={[]} ourEventsIndexes={[1]} {...props} />);
|
||||
}
|
||||
|
||||
it("Sets up appropriate callEventGrouper for m.call. events", () => {
|
||||
const { container } = render(
|
||||
<SearchResultTile
|
||||
timeline={[
|
||||
new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
sender: "@user1:server",
|
||||
room_id: ROOM_ID,
|
||||
origin_server_ts: 1432735824652,
|
||||
content: { call_id: "call.1" },
|
||||
event_id: "$1:server",
|
||||
}),
|
||||
new MatrixEvent({
|
||||
content: {
|
||||
body: "This is an example text message",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<b>This is an example text message</b>",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
event_id: "$144429830826TWwbB:localhost",
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: ROOM_ID,
|
||||
sender: "@example:example.org",
|
||||
type: "m.room.message",
|
||||
unsigned: {
|
||||
age: 1234,
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: EventType.CallAnswer,
|
||||
sender: "@user2:server",
|
||||
room_id: ROOM_ID,
|
||||
origin_server_ts: 1432735824654,
|
||||
content: { call_id: "call.1" },
|
||||
event_id: "$2:server",
|
||||
}),
|
||||
]}
|
||||
ourEventsIndexes={[1]}
|
||||
/>,
|
||||
);
|
||||
const { container } = renderComponent({
|
||||
timeline: [
|
||||
new MatrixEvent({
|
||||
type: EventType.CallInvite,
|
||||
sender: "@user1:server",
|
||||
room_id: ROOM_ID,
|
||||
origin_server_ts: 1432735824652,
|
||||
content: { call_id: "call.1" },
|
||||
event_id: "$1:server",
|
||||
}),
|
||||
new MatrixEvent({
|
||||
content: {
|
||||
body: "This is an example text message",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<b>This is an example text message</b>",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
event_id: "$144429830826TWwbB:localhost",
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: ROOM_ID,
|
||||
sender: "@example:example.org",
|
||||
type: "m.room.message",
|
||||
unsigned: {
|
||||
age: 1234,
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: EventType.CallAnswer,
|
||||
sender: "@user2:server",
|
||||
room_id: ROOM_ID,
|
||||
origin_server_ts: 1432735824654,
|
||||
content: { call_id: "call.1" },
|
||||
event_id: "$2:server",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const tiles = container.querySelectorAll<HTMLElement>(".mx_EventTile");
|
||||
expect(tiles.length).toEqual(2);
|
||||
expect(tiles[0].dataset.eventId).toBe("$1:server");
|
||||
expect(tiles[1].dataset.eventId).toBe("$144429830826TWwbB:localhost");
|
||||
expect(tiles[0]!.dataset.eventId).toBe("$1:server");
|
||||
expect(tiles[1]!.dataset.eventId).toBe("$144429830826TWwbB:localhost");
|
||||
});
|
||||
|
||||
it("supports events with missing timestamps", () => {
|
||||
const { container } = renderComponent({
|
||||
timeline: [...Array(20)].map(
|
||||
(_, i) =>
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@user1:server",
|
||||
room_id: ROOM_ID,
|
||||
content: { body: `Message #${i}` },
|
||||
event_id: `$${i}:server`,
|
||||
origin_server_ts: undefined,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const separators = container.querySelectorAll(".mx_DateSeparator");
|
||||
// One separator is always rendered at the top, we don't want any
|
||||
// between messages.
|
||||
expect(separators.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -208,6 +208,10 @@ export function createTestClient(): MatrixClient {
|
||||
setPassword: jest.fn().mockRejectedValue({}),
|
||||
groupCallEventHandler: { groupCalls: new Map<string, GroupCall>() },
|
||||
redactEvent: jest.fn(),
|
||||
|
||||
createMessagesRequest: jest.fn().mockResolvedValue({
|
||||
chunk: [],
|
||||
}),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
client.reEmitter = new ReEmitter(client);
|
||||
|
@ -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…
Reference in New Issue
Block a user