"use strict"; import 'source-map-support/register'; const q = require("q"); const sdk = require("../.."); const HttpBackend = require("../mock-request"); const utils = require("../test-utils"); const EventTimeline = sdk.EventTimeline; const baseUrl = "http://localhost.or.something"; const userId = "@alice:localhost"; const userName = "Alice"; const accessToken = "aseukfgwef"; const roomId = "!foo:bar"; const otherUserId = "@bob:localhost"; const USER_MEMBERSHIP_EVENT = utils.mkMembership({ room: roomId, mship: "join", user: userId, name: userName, }); const ROOM_NAME_EVENT = utils.mkEvent({ type: "m.room.name", room: roomId, user: otherUserId, content: { name: "Old room name", }, }); const INITIAL_SYNC_DATA = { next_batch: "s_5_3", rooms: { join: { "!foo:bar": { // roomId timeline: { events: [ utils.mkMessage({ room: roomId, user: otherUserId, msg: "hello", }), ], prev_batch: "f_1_1", }, state: { events: [ ROOM_NAME_EVENT, utils.mkMembership({ room: roomId, mship: "join", user: otherUserId, name: "Bob", }), USER_MEMBERSHIP_EVENT, utils.mkEvent({ type: "m.room.create", room: roomId, user: userId, content: { creator: userId, }, }), ], }, }, }, }, }; const EVENTS = [ utils.mkMessage({ room: roomId, user: userId, msg: "we", }), utils.mkMessage({ room: roomId, user: userId, msg: "could", }), utils.mkMessage({ room: roomId, user: userId, msg: "be", }), utils.mkMessage({ room: roomId, user: userId, msg: "heroes", }), ]; // start the client, and wait for it to initialise function startClient(httpBackend, client) { httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); client.startClient(); // set up a promise which will resolve once the client is initialised const deferred = q.defer(); client.on("sync", function(state) { console.log("sync", state); if (state != "SYNCING") { return; } deferred.resolve(); }); httpBackend.flush(); return deferred.promise; } describe("getEventTimeline support", function() { let httpBackend; let client; beforeEach(function() { utils.beforeEach(this); // eslint-disable-line no-invalid-this httpBackend = new HttpBackend(); sdk.request(httpBackend.requestFn); }); afterEach(function() { if (client) { client.stopClient(); } }); it("timeline support must be enabled to work", function(done) { client = sdk.createClient({ baseUrl: baseUrl, userId: userId, accessToken: accessToken, }); startClient(httpBackend, client, ).then(function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; expect(function() { client.getEventTimeline(timelineSet, "event"); }).toThrow(); }).catch(utils.failTest).done(done); }); it("timeline support works when enabled", function(done) { client = sdk.createClient({ baseUrl: baseUrl, userId: userId, accessToken: accessToken, timelineSupport: true, }); startClient(httpBackend, client, ).then(function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; expect(function() { client.getEventTimeline(timelineSet, "event"); }).toNotThrow(); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); it("scrollback should be able to scroll back to before a gappy /sync", function(done) { // need a client with timelineSupport disabled to make this work client = sdk.createClient({ baseUrl: baseUrl, userId: userId, accessToken: accessToken, }); let room; startClient(httpBackend, client, ).then(function() { room = client.getRoom(roomId); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[0], ], prev_batch: "f_1_1", }, }, }, }, }); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_5", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[1], ], limited: true, prev_batch: "f_1_2", }, }, }, }, }); httpBackend.when("GET", "/messages").respond(200, { chunk: [EVENTS[0]], start: "pagin_start", end: "pagin_end", }); return httpBackend.flush("/sync", 2); }).then(function() { expect(room.timeline.length).toEqual(1); expect(room.timeline[0].event).toEqual(EVENTS[1]); httpBackend.flush("/messages", 1); return client.scrollback(room); }).then(function() { expect(room.timeline.length).toEqual(2); expect(room.timeline[0].event).toEqual(EVENTS[0]); expect(room.timeline[1].event).toEqual(EVENTS[1]); expect(room.oldState.paginationToken).toEqual("pagin_end"); }).catch(utils.failTest).done(done); }); }); import expect from 'expect'; describe("MatrixClient event timelines", function() { let client = null; let httpBackend = null; beforeEach(function(done) { utils.beforeEach(this); // eslint-disable-line no-invalid-this httpBackend = new HttpBackend(); sdk.request(httpBackend.requestFn); client = sdk.createClient({ baseUrl: baseUrl, userId: userId, accessToken: accessToken, timelineSupport: true, }); startClient(httpBackend, client) .catch(utils.failTest).done(done); }); afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); }); describe("getEventTimeline", function() { it("should create a new timeline for new events", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar") .respond(200, function() { return { start: "start_token", events_before: [EVENTS[1], EVENTS[0]], event: EVENTS[2], events_after: [EVENTS[3]], state: [ ROOM_NAME_EVENT, USER_MEMBERSHIP_EVENT, ], end: "end_token", }; }); client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) { expect(tl.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { expect(tl.getEvents()[i].event).toEqual(EVENTS[i]); expect(tl.getEvents()[i].sender.name).toEqual(userName); } expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token"); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); it("should return existing timeline for known events", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[0], ], prev_batch: "f_1_2", }, }, }, }, }); httpBackend.flush("/sync").then(function() { return client.getEventTimeline(timelineSet, EVENTS[0].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(EVENTS[0]); expect(tl.getEvents()[1].sender.name).toEqual(userName); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4"); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); it("should update timelines where they overlap a previous /sync", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ EVENTS[3], ], prev_batch: "f_1_2", }, }, }, }, }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[2].event_id)) .respond(200, function() { return { start: "start_token", events_before: [EVENTS[1]], event: EVENTS[2], events_after: [EVENTS[3]], end: "end_token", state: [], }; }); const deferred = q.defer(); client.on("sync", function() { client.getEventTimeline(timelineSet, EVENTS[2].event_id, ).then(function(tl) { expect(tl.getEvents().length).toEqual(4); expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); expect(tl.getEvents()[3].event).toEqual(EVENTS[3]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) // .toEqual("s_5_4"); }).done(() => deferred.resolve(), (e) => deferred.reject(e)); }); return q.all([ httpBackend.flush(), deferred.promise, ]); }); it("should join timelines where they overlap a previous /context", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[2].event_id)) .respond(200, function() { return { start: "start_token2", events_before: [], event: EVENTS[2], events_after: [], end: "end_token2", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[3].event_id)) .respond(200, function() { return { start: "start_token3", events_before: [], event: EVENTS[3], events_after: [], end: "end_token3", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[1].event_id)) .respond(200, function() { return { start: "start_token4", events_before: [EVENTS[0]], event: EVENTS[1], events_after: [EVENTS[2], EVENTS[3]], end: "end_token4", state: [], }; }); let tl0; let tl3; client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl) { expect(tl.getEvents().length).toEqual(1); tl0 = tl; return client.getEventTimeline(timelineSet, EVENTS[2].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(1); return client.getEventTimeline(timelineSet, EVENTS[3].event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(1); tl3 = tl; return client.getEventTimeline(timelineSet, EVENTS[1].event_id); }).then(function(tl) { // we expect it to get merged in with event 2 expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS)) .toBe(tl0); expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS)) .toBe(tl3); expect(tl0.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); expect(tl0.getPaginationToken(EventTimeline.FORWARDS)) .toBe(null); expect(tl3.getPaginationToken(EventTimeline.BACKWARDS)) .toBe(null); expect(tl3.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token3"); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); it("should fail gracefully if there is no event field", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1") .respond(200, function() { return { start: "start_token", events_before: [], events_after: [], end: "end_token", state: [], }; }); client.getEventTimeline(timelineSet, "event1", ).then(function(tl) { // could do with a fail() expect(true).toBeFalsy(); }).catch(function(e) { expect(String(e)).toMatch(/'event'/); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); }); describe("paginateEventTimeline", function() { it("should allow you to paginate backwards", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { const params = req.queryParams; expect(params.dir).toEqual("b"); expect(params.from).toEqual("start_token0"); expect(params.limit).toEqual(30); }).respond(200, function() { return { chunk: [EVENTS[1], EVENTS[2]], end: "start_token1", }; }); let tl; client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline(tl, {backwards: true}); }).then(function(success) { expect(success).toBeTruthy(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[0].event).toEqual(EVENTS[2]); expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); expect(tl.getEvents()[2].event).toEqual(EVENTS[0]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token1"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token0"); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); it("should allow you to paginate forwards", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) .respond(200, function() { return { start: "start_token0", events_before: [], event: EVENTS[0], events_after: [], end: "end_token0", state: [], }; }); httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { const params = req.queryParams; expect(params.dir).toEqual("f"); expect(params.from).toEqual("end_token0"); expect(params.limit).toEqual(20); }).respond(200, function() { return { chunk: [EVENTS[1], EVENTS[2]], end: "end_token1", }; }); let tl; client.getEventTimeline(timelineSet, EVENTS[0].event_id, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline( tl, {backwards: false, limit: 20}); }).then(function(success) { expect(success).toBeTruthy(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[0].event).toEqual(EVENTS[0]); expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); expect(tl.getEvents()[2].event).toEqual(EVENTS[2]); expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); expect(tl.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token1"); }).catch(utils.failTest).done(done); httpBackend.flush().catch(utils.failTest); }); }); describe("event timeline for sent events", function() { const TXN_ID = "txn1"; const event = utils.mkMessage({ room: roomId, user: userId, msg: "a body", }); event.unsigned = {transaction_id: TXN_ID}; beforeEach(function() { // set up handlers for both the message send, and the // /sync httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID) .respond(200, { event_id: event.event_id, }); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", rooms: { join: { "!foo:bar": { timeline: { events: [ event, ], prev_batch: "f_1_1", }, }, }, }, }); }); it("should work when /send returns before /sync", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { expect(res.event_id).toEqual(event.event_id); return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { // 2 because the initial sync contained an event expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].getContent().body).toEqual("a body"); // now let the sync complete, and check it again return httpBackend.flush("/sync", 1); }).then(function() { return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(event); }).catch(utils.failTest).done(done); httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest); }); it("should work when /send returns after /sync", function(done) { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; // initiate the send, and set up checks to be done when it completes // - but note that it won't complete until after the /sync does, below. client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { console.log("sendTextMessage completed"); expect(res.event_id).toEqual(event.event_id); return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { console.log("getEventTimeline completed (2)"); expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].getContent().body).toEqual("a body"); }).catch(utils.failTest).done(done); httpBackend.flush("/sync", 1).then(function() { return client.getEventTimeline(timelineSet, event.event_id); }).then(function(tl) { console.log("getEventTimeline completed (1)"); expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents()[1].event).toEqual(event); // now let the send complete. return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1); }).catch(utils.failTest); }); }); it("should handle gappy syncs after redactions", function(done) { // https://github.com/vector-im/vector-web/issues/1389 // a state event, followed by a redaction thereof const event = utils.mkMembership({ room: roomId, mship: "join", user: otherUserId, }); const redaction = utils.mkEvent({ type: "m.room.redaction", room_id: roomId, sender: otherUserId, content: {}, }); redaction.redacts = event.event_id; const syncData = { next_batch: "batch1", rooms: { join: {}, }, }; syncData.rooms.join[roomId] = { timeline: { events: [ event, redaction, ], limited: false, }, }; httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.flush().then(function() { const room = client.getRoom(roomId); const tl = room.getLiveTimeline(); expect(tl.getEvents().length).toEqual(3); expect(tl.getEvents()[1].isRedacted()).toBe(true); const sync2 = { next_batch: "batch2", rooms: { join: {}, }, }; sync2.rooms.join[roomId] = { timeline: { events: [ utils.mkMessage({ room: roomId, user: otherUserId, msg: "world", }), ], limited: true, prev_batch: "newerTok", }, }; httpBackend.when("GET", "/sync").respond(200, sync2); return httpBackend.flush(); }).then(function() { const room = client.getRoom(roomId); const tl = room.getLiveTimeline(); expect(tl.getEvents().length).toEqual(1); }).catch(utils.failTest).done(done); }); });