@@ -14,32 +14,97 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17- import { EventType , IRoomEvent , MatrixClient , Room } from "matrix-js-sdk/src/matrix" ;
17+ import { EventType , IRoomEvent , MatrixClient , MatrixEvent , MsgType , Room , RoomMember } from "matrix-js-sdk/src/matrix" ;
18+ import fetchMock from "fetch-mock-jest" ;
1819
19- import { mkStubRoom , REPEATABLE_DATE , stubClient } from "../../test-utils" ;
20+ import { filterConsole , mkStubRoom , REPEATABLE_DATE , stubClient } from "../../test-utils" ;
2021import { ExportType , IExportOptions } from "../../../src/utils/exportUtils/exportUtils" ;
2122import SdkConfig from "../../../src/SdkConfig" ;
2223import HTMLExporter from "../../../src/utils/exportUtils/HtmlExport" ;
2324import DMRoomMap from "../../../src/utils/DMRoomMap" ;
25+ import { mediaFromMxc } from "../../../src/customisations/Media" ;
2426
2527jest . mock ( "jszip" ) ;
2628
29+ const EVENT_MESSAGE : IRoomEvent = {
30+ event_id : "$1" ,
31+ type : EventType . RoomMessage ,
32+ sender : "@bob:example.com" ,
33+ origin_server_ts : 0 ,
34+ content : {
35+ msgtype : "m.text" ,
36+ body : "Message" ,
37+ avatar_url : "mxc://example.org/avatar.bmp" ,
38+ } ,
39+ } ;
40+
41+ const EVENT_ATTACHMENT : IRoomEvent = {
42+ event_id : "$2" ,
43+ type : EventType . RoomMessage ,
44+ sender : "@alice:example.com" ,
45+ origin_server_ts : 1 ,
46+ content : {
47+ msgtype : MsgType . File ,
48+ body : "hello.txt" ,
49+ filename : "hello.txt" ,
50+ url : "mxc://example.org/test-id" ,
51+ } ,
52+ } ;
53+
2754describe ( "HTMLExport" , ( ) => {
2855 let client : jest . Mocked < MatrixClient > ;
56+ let room : Room ;
57+
58+ filterConsole (
59+ "Starting export" ,
60+ "events in" , // Fetched # events in # seconds
61+ "events so far" ,
62+ "Export successful!" ,
63+ "does not have an m.room.create event" ,
64+ "Creating HTML" ,
65+ "Generating a ZIP" ,
66+ "Cleaning up" ,
67+ ) ;
2968
3069 beforeEach ( ( ) => {
3170 jest . useFakeTimers ( ) ;
3271 jest . setSystemTime ( REPEATABLE_DATE ) ;
3372
3473 client = stubClient ( ) as jest . Mocked < MatrixClient > ;
3574 DMRoomMap . makeShared ( ) ;
75+
76+ room = new Room ( "!myroom:example.org" , client , "@me:example.org" ) ;
77+ client . getRoom . mockReturnValue ( room ) ;
3678 } ) ;
3779
38- function getMessageFile ( exporter : HTMLExporter ) : Blob {
80+ function mockMessages ( ...events : IRoomEvent [ ] ) : void {
81+ client . createMessagesRequest . mockImplementation ( ( _roomId , fromStr , limit = 30 ) => {
82+ const from = fromStr === null ? 0 : parseInt ( fromStr ) ;
83+ const chunk = events . slice ( from , limit ) ;
84+ return Promise . resolve ( {
85+ chunk,
86+ from : from . toString ( ) ,
87+ to : ( from + limit ) . toString ( ) ,
88+ } ) ;
89+ } ) ;
90+ }
91+
92+ /** Retrieve a map of files within the zip. */
93+ function getFiles ( exporter : HTMLExporter ) : { [ filename : string ] : Blob } {
3994 //@ts -ignore private access
4095 const files = exporter . files ;
41- const file = files . find ( ( f ) => f . name == "messages.html" ) ! ;
42- return file . blob ;
96+ return files . reduce ( ( d , f ) => ( { ...d , [ f . name ] : f . blob } ) , { } ) ;
97+ }
98+
99+ function getMessageFile ( exporter : HTMLExporter ) : Blob {
100+ const files = getFiles ( exporter ) ;
101+ return files [ "messages.html" ] ! ;
102+ }
103+
104+ /** set a mock fetch response for an MXC */
105+ function mockMxc ( mxc : string , body : string ) {
106+ const media = mediaFromMxc ( mxc , client ) ;
107+ fetchMock . get ( media . srcHttp , body ) ;
43108 }
44109
45110 it ( "should have an SDK-branded destination file name" , ( ) => {
@@ -59,10 +124,8 @@ describe("HTMLExport", () => {
59124 } ) ;
60125
61126 it ( "should export" , async ( ) => {
62- const room = new Room ( "!myroom:example.org" , client , "@me:example.org" ) ;
63-
64127 const events = [ ...Array ( 50 ) ] . map < IRoomEvent > ( ( _ , i ) => ( {
65- event_id : "$1" ,
128+ event_id : ` ${ i } ` ,
66129 type : EventType . RoomMessage ,
67130 sender : `@user${ i } :example.com` ,
68131 origin_server_ts : 5_000 + i * 1000 ,
@@ -71,9 +134,7 @@ describe("HTMLExport", () => {
71134 body : `Message #${ i } ` ,
72135 } ,
73136 } ) ) ;
74-
75- client . getRoom . mockReturnValue ( room ) ;
76- client . createMessagesRequest . mockResolvedValue ( { chunk : events } ) ;
137+ mockMessages ( ...events ) ;
77138
78139 const exporter = new HTMLExporter (
79140 room ,
@@ -91,4 +152,167 @@ describe("HTMLExport", () => {
91152 const file = getMessageFile ( exporter ) ;
92153 expect ( await file . text ( ) ) . toMatchSnapshot ( ) ;
93154 } ) ;
155+
156+ it ( "should include the room's avatar" , async ( ) => {
157+ mockMessages ( EVENT_MESSAGE ) ;
158+
159+ const mxc = "mxc://www.example.com/avatars/nice-room.jpeg" ;
160+ const avatar = "011011000110111101101100" ;
161+ jest . spyOn ( room , "getMxcAvatarUrl" ) . mockReturnValue ( mxc ) ;
162+ mockMxc ( mxc , avatar ) ;
163+
164+ const exporter = new HTMLExporter (
165+ room ,
166+ ExportType . Timeline ,
167+ {
168+ attachmentsIncluded : false ,
169+ maxSize : 1_024 * 1_024 ,
170+ } ,
171+ ( ) => { } ,
172+ ) ;
173+
174+ await exporter . export ( ) ;
175+
176+ const files = getFiles ( exporter ) ;
177+ expect ( await files [ "room.png" ] ! . text ( ) ) . toBe ( avatar ) ;
178+ } ) ;
179+
180+ it ( "should include the creation event" , async ( ) => {
181+ const creator = "@bob:example.com" ;
182+ mockMessages ( EVENT_MESSAGE ) ;
183+ room . currentState . setStateEvents ( [
184+ new MatrixEvent ( {
185+ type : EventType . RoomCreate ,
186+ event_id : "$00001" ,
187+ room_id : room . roomId ,
188+ sender : creator ,
189+ origin_server_ts : 0 ,
190+ content : { } ,
191+ state_key : "" ,
192+ } ) ,
193+ ] ) ;
194+
195+ const exporter = new HTMLExporter (
196+ room ,
197+ ExportType . Timeline ,
198+ {
199+ attachmentsIncluded : false ,
200+ maxSize : 1_024 * 1_024 ,
201+ } ,
202+ ( ) => { } ,
203+ ) ;
204+
205+ await exporter . export ( ) ;
206+
207+ expect ( await getMessageFile ( exporter ) . text ( ) ) . toContain ( `${ creator } created this room.` ) ;
208+ } ) ;
209+
210+ it ( "should include the topic" , async ( ) => {
211+ const topic = ":^-) (-^:" ;
212+ mockMessages ( EVENT_MESSAGE ) ;
213+ room . currentState . setStateEvents ( [
214+ new MatrixEvent ( {
215+ type : EventType . RoomTopic ,
216+ event_id : "$00001" ,
217+ room_id : room . roomId ,
218+ sender : "@alice:example.com" ,
219+ origin_server_ts : 0 ,
220+ content : { topic } ,
221+ state_key : "" ,
222+ } ) ,
223+ ] ) ;
224+
225+ const exporter = new HTMLExporter (
226+ room ,
227+ ExportType . Timeline ,
228+ {
229+ attachmentsIncluded : false ,
230+ maxSize : 1_024 * 1_024 ,
231+ } ,
232+ ( ) => { } ,
233+ ) ;
234+
235+ await exporter . export ( ) ;
236+
237+ expect ( await getMessageFile ( exporter ) . text ( ) ) . toContain ( `Topic: ${ topic } ` ) ;
238+ } ) ;
239+
240+ it ( "should include avatars" , async ( ) => {
241+ mockMessages ( EVENT_MESSAGE ) ;
242+
243+ jest . spyOn ( RoomMember . prototype , "getMxcAvatarUrl" ) . mockReturnValue ( "mxc://example.org/avatar.bmp" ) ;
244+
245+ const avatarContent = "this is a bitmap all the pixels are red :^-)" ;
246+ mockMxc ( "mxc://example.org/avatar.bmp" , avatarContent ) ;
247+
248+ const exporter = new HTMLExporter (
249+ room ,
250+ ExportType . Timeline ,
251+ {
252+ attachmentsIncluded : false ,
253+ maxSize : 1_024 * 1_024 ,
254+ } ,
255+ ( ) => { } ,
256+ ) ;
257+
258+ await exporter . export ( ) ;
259+
260+ // Ensure that the avatar is present
261+ const files = getFiles ( exporter ) ;
262+ const file = files [ "users/@bob-example.com.png" ] ;
263+ expect ( file ) . not . toBeUndefined ( ) ;
264+
265+ // Ensure it has the expected content
266+ expect ( await file . text ( ) ) . toBe ( avatarContent ) ;
267+ } ) ;
268+
269+ it ( "should include attachments" , async ( ) => {
270+ mockMessages ( EVENT_MESSAGE , EVENT_ATTACHMENT ) ;
271+ const attachmentBody = "Lorem ipsum dolor sit amet" ;
272+
273+ mockMxc ( "mxc://example.org/test-id" , attachmentBody ) ;
274+
275+ const exporter = new HTMLExporter (
276+ room ,
277+ ExportType . Timeline ,
278+ {
279+ attachmentsIncluded : true ,
280+ maxSize : 1_024 * 1_024 ,
281+ } ,
282+ ( ) => { } ,
283+ ) ;
284+
285+ await exporter . export ( ) ;
286+
287+ // Ensure that the attachment is present
288+ const files = getFiles ( exporter ) ;
289+ const file = files [ "files/hello-1-1-1970 at 12-00-00 AM.txt" ] ;
290+ expect ( file ) . not . toBeUndefined ( ) ;
291+
292+ // Ensure that the attachment has the expected content
293+ const text = await file . text ( ) ;
294+ expect ( text ) . toBe ( attachmentBody ) ;
295+ } ) ;
296+
297+ it ( "should omit attachments" , async ( ) => {
298+ mockMessages ( EVENT_MESSAGE , EVENT_ATTACHMENT ) ;
299+
300+ const exporter = new HTMLExporter (
301+ room ,
302+ ExportType . Timeline ,
303+ {
304+ attachmentsIncluded : false ,
305+ maxSize : 1_024 * 1_024 ,
306+ } ,
307+ ( ) => { } ,
308+ ) ;
309+
310+ await exporter . export ( ) ;
311+
312+ // Ensure that the attachment is present
313+ const files = getFiles ( exporter ) ;
314+ for ( const fileName of Object . keys ( files ) ) {
315+ expect ( fileName ) . not . toMatch ( / ^ f i l e s \/ h e l l o / ) ;
316+ }
317+ } ) ;
94318} ) ;
0 commit comments