2016-04-08 15:58:24 +02:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const React = require('react');
|
|
|
|
const ReactDOM = require('react-dom');
|
|
|
|
const ReactTestUtils = require('react-addons-test-utils');
|
|
|
|
const expect = require('expect');
|
2017-07-12 14:58:14 +02:00
|
|
|
import Promise from 'bluebird';
|
2017-10-11 18:56:17 +02:00
|
|
|
const sinon = require('sinon');
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const jssdk = require('matrix-js-sdk');
|
|
|
|
const EventTimeline = jssdk.EventTimeline;
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const sdk = require('matrix-react-sdk');
|
|
|
|
const TimelinePanel = sdk.getComponent('structures.TimelinePanel');
|
|
|
|
const peg = require('../../../src/MatrixClientPeg');
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const test_utils = require('test-utils');
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const ROOM_ID = '!room:localhost';
|
|
|
|
const USER_ID = '@me:localhost';
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2016-11-14 19:20:15 +01:00
|
|
|
// wrap TimelinePanel with a component which provides the MatrixClient in the context.
|
|
|
|
const WrappedTimelinePanel = React.createClass({
|
|
|
|
childContextTypes: {
|
|
|
|
matrixClient: React.PropTypes.object,
|
|
|
|
},
|
|
|
|
|
|
|
|
getChildContext: function() {
|
|
|
|
return {
|
|
|
|
matrixClient: peg.get(),
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
render: function() {
|
|
|
|
return <TimelinePanel ref="panel" {...this.props} />;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2016-04-08 15:58:24 +02:00
|
|
|
describe('TimelinePanel', function() {
|
2017-10-11 18:56:17 +02:00
|
|
|
let sandbox;
|
|
|
|
let timelineSet;
|
|
|
|
let room;
|
|
|
|
let client;
|
|
|
|
let timeline;
|
|
|
|
let parentDiv;
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2016-10-11 14:54:57 +02:00
|
|
|
// make a dummy message. eventNum is put in the message text to help
|
|
|
|
// identification during debugging, and also in the timestamp so that we
|
|
|
|
// don't get lots of events with the same timestamp.
|
|
|
|
function mkMessage(eventNum, opts) {
|
2016-04-11 15:05:04 +02:00
|
|
|
return test_utils.mkMessage(
|
|
|
|
{
|
|
|
|
event: true, room: ROOM_ID, user: USER_ID,
|
2016-10-11 14:54:57 +02:00
|
|
|
ts: Date.now() + eventNum,
|
|
|
|
msg: "Event " + eventNum,
|
2017-10-11 18:56:17 +02:00
|
|
|
...opts,
|
2016-04-11 15:05:04 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function scryEventTiles(panel) {
|
|
|
|
return ReactTestUtils.scryRenderedComponentsWithType(
|
|
|
|
panel, sdk.getComponent('rooms.EventTile'));
|
2017-10-11 18:56:17 +02:00
|
|
|
}
|
2016-04-11 15:05:04 +02:00
|
|
|
|
2016-04-08 15:58:24 +02:00
|
|
|
beforeEach(function() {
|
|
|
|
test_utils.beforeEach(this);
|
|
|
|
sandbox = test_utils.stubClient(sandbox);
|
|
|
|
|
|
|
|
room = sinon.createStubInstance(jssdk.Room);
|
2016-09-08 23:48:44 +02:00
|
|
|
room.roomId = ROOM_ID;
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2016-09-06 01:59:17 +02:00
|
|
|
timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);
|
2016-09-07 22:10:31 +02:00
|
|
|
timelineSet.getPendingEvents.returns([]);
|
2016-09-06 01:59:17 +02:00
|
|
|
timelineSet.room = room;
|
|
|
|
|
2016-09-08 23:48:44 +02:00
|
|
|
timeline = new jssdk.EventTimeline(timelineSet);
|
|
|
|
|
|
|
|
timelineSet.getLiveTimeline.returns(timeline);
|
|
|
|
|
2016-04-08 15:58:24 +02:00
|
|
|
client = peg.get();
|
|
|
|
client.credentials = {userId: USER_ID};
|
|
|
|
|
|
|
|
// create a div of a useful size to put our panel in, and attach it to
|
|
|
|
// the document so that we can interact with it properly.
|
|
|
|
parentDiv = document.createElement('div');
|
|
|
|
parentDiv.style.width = '800px';
|
2017-01-31 23:40:53 +01:00
|
|
|
|
|
|
|
// This has to be slightly carefully chosen. We expect to have to do
|
|
|
|
// exactly one pagination to fill it.
|
|
|
|
parentDiv.style.height = '500px';
|
|
|
|
|
2016-04-08 15:58:24 +02:00
|
|
|
parentDiv.style.overflow = 'hidden';
|
|
|
|
document.body.appendChild(parentDiv);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function() {
|
|
|
|
if (parentDiv) {
|
2016-04-21 14:41:25 +02:00
|
|
|
ReactDOM.unmountComponentAtNode(parentDiv);
|
|
|
|
parentDiv.remove();
|
2016-04-08 15:58:24 +02:00
|
|
|
parentDiv = null;
|
|
|
|
}
|
|
|
|
sandbox.restore();
|
|
|
|
});
|
|
|
|
|
2016-04-11 15:05:04 +02:00
|
|
|
it('should load new events even if you are scrolled up', function(done) {
|
|
|
|
// this is https://github.com/vector-im/vector-web/issues/1367
|
|
|
|
|
|
|
|
// enough events to allow us to scroll back
|
2017-10-11 18:56:17 +02:00
|
|
|
const N_EVENTS = 30;
|
|
|
|
for (let i = 0; i < N_EVENTS; i++) {
|
2016-10-11 14:54:57 +02:00
|
|
|
timeline.addEvent(mkMessage(i));
|
2016-04-11 15:05:04 +02:00
|
|
|
}
|
|
|
|
|
2017-07-11 18:21:41 +02:00
|
|
|
let scrollDefer;
|
|
|
|
const onScroll = (e) => {
|
|
|
|
console.log(`TimelinePanel called onScroll: ${e.target.scrollTop}`);
|
|
|
|
if (scrollDefer) {
|
|
|
|
scrollDefer.resolve();
|
|
|
|
}
|
|
|
|
};
|
2017-10-11 18:56:17 +02:00
|
|
|
const rendered = ReactDOM.render(
|
2017-07-11 18:21:41 +02:00
|
|
|
<WrappedTimelinePanel timelineSet={timelineSet} onScroll={onScroll} />,
|
2016-04-11 15:05:04 +02:00
|
|
|
parentDiv,
|
|
|
|
);
|
2017-10-11 18:56:17 +02:00
|
|
|
const panel = rendered.refs.panel;
|
|
|
|
const scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
2016-04-11 15:05:04 +02:00
|
|
|
panel, "gm-scroll-view");
|
|
|
|
|
|
|
|
// helper function which will return a promise which resolves when the
|
|
|
|
// panel isn't paginating
|
|
|
|
var awaitPaginationCompletion = function() {
|
2017-10-11 18:56:17 +02:00
|
|
|
if(!panel.state.forwardPaginating) {return Promise.resolve();} else {return Promise.delay(0).then(awaitPaginationCompletion);}
|
2016-04-11 15:05:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// helper function which will return a promise which resolves when
|
|
|
|
// the TimelinePanel fires a scroll event
|
2017-10-11 18:56:17 +02:00
|
|
|
const awaitScroll = function() {
|
2017-07-12 15:04:20 +02:00
|
|
|
scrollDefer = Promise.defer();
|
2016-04-11 15:05:04 +02:00
|
|
|
return scrollDefer.promise;
|
|
|
|
};
|
|
|
|
|
2017-07-11 18:21:41 +02:00
|
|
|
// let the first round of pagination finish off
|
2017-07-12 15:04:20 +02:00
|
|
|
Promise.delay(5).then(() => {
|
2016-04-11 15:05:04 +02:00
|
|
|
expect(panel.state.canBackPaginate).toBe(false);
|
2016-04-19 22:10:23 +02:00
|
|
|
expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
|
2016-04-11 15:05:04 +02:00
|
|
|
|
|
|
|
// scroll up
|
|
|
|
console.log("setting scrollTop = 0");
|
|
|
|
scrollingDiv.scrollTop = 0;
|
|
|
|
|
|
|
|
// wait for the scroll event to land
|
|
|
|
}).then(awaitScroll).then(() => {
|
2017-07-11 18:21:41 +02:00
|
|
|
expect(scrollingDiv.scrollTop).toEqual(0);
|
|
|
|
|
2016-04-11 15:05:04 +02:00
|
|
|
// there should be no pagination going on now
|
|
|
|
expect(panel.state.backPaginating).toBe(false);
|
|
|
|
expect(panel.state.forwardPaginating).toBe(false);
|
|
|
|
expect(panel.state.canBackPaginate).toBe(false);
|
|
|
|
expect(panel.state.canForwardPaginate).toBe(false);
|
|
|
|
expect(panel.isAtEndOfLiveTimeline()).toBe(false);
|
|
|
|
expect(scrollingDiv.scrollTop).toEqual(0);
|
|
|
|
|
|
|
|
console.log("adding event");
|
|
|
|
|
|
|
|
// a new event!
|
2017-10-11 18:56:17 +02:00
|
|
|
const ev = mkMessage(N_EVENTS+1);
|
2016-04-11 15:05:04 +02:00
|
|
|
timeline.addEvent(ev);
|
2016-09-06 01:59:17 +02:00
|
|
|
panel.onRoomTimeline(ev, room, false, false, {
|
|
|
|
liveEvent: true,
|
2016-09-08 23:48:44 +02:00
|
|
|
timeline: timeline,
|
2016-09-06 01:59:17 +02:00
|
|
|
});
|
2016-04-11 15:05:04 +02:00
|
|
|
|
|
|
|
// that won't make much difference, because we don't paginate
|
|
|
|
// unless we're at the bottom of the timeline, but a scroll event
|
|
|
|
// should be enough to set off a pagination.
|
2016-04-19 22:10:23 +02:00
|
|
|
expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
|
2016-04-11 15:05:04 +02:00
|
|
|
|
|
|
|
scrollingDiv.scrollTop = 10;
|
2016-10-11 14:54:57 +02:00
|
|
|
|
|
|
|
return awaitScroll();
|
|
|
|
}).then(awaitPaginationCompletion).then(() => {
|
2016-04-19 22:10:23 +02:00
|
|
|
expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
|
|
|
|
}).done(done, done);
|
2016-04-11 15:05:04 +02:00
|
|
|
});
|
|
|
|
|
2016-04-08 15:58:24 +02:00
|
|
|
it('should not paginate forever if there are no events', function(done) {
|
|
|
|
// start with a handful of events in the timeline, as would happen when
|
|
|
|
// joining a room
|
2017-10-11 18:56:17 +02:00
|
|
|
const d = Date.now();
|
|
|
|
for (let i = 0; i < 3; i++) {
|
2016-10-11 14:54:57 +02:00
|
|
|
timeline.addEvent(mkMessage(i));
|
2016-04-08 15:58:24 +02:00
|
|
|
}
|
|
|
|
timeline.setPaginationToken('tok', EventTimeline.BACKWARDS);
|
|
|
|
|
|
|
|
// back-pagination returns a promise for true, but adds no events
|
|
|
|
client.paginateEventTimeline = sinon.spy((tl, opts) => {
|
|
|
|
console.log("paginate:", opts);
|
|
|
|
expect(opts.backwards).toBe(true);
|
2017-07-12 15:02:00 +02:00
|
|
|
return Promise.resolve(true);
|
2016-04-08 15:58:24 +02:00
|
|
|
});
|
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const rendered = ReactDOM.render(
|
|
|
|
<WrappedTimelinePanel timelineSet={timelineSet} />,
|
|
|
|
parentDiv,
|
2016-04-08 15:58:24 +02:00
|
|
|
);
|
2017-10-11 18:56:17 +02:00
|
|
|
const panel = rendered.refs.panel;
|
2016-04-08 15:58:24 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const messagePanel = ReactTestUtils.findRenderedComponentWithType(
|
2016-04-08 15:58:24 +02:00
|
|
|
panel, sdk.getComponent('structures.MessagePanel'));
|
|
|
|
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(true);
|
|
|
|
|
|
|
|
// let the first round of pagination finish off
|
|
|
|
setTimeout(() => {
|
|
|
|
// at this point, the timeline window should have tried to paginate
|
|
|
|
// 5 times, and we should have given up paginating
|
|
|
|
expect(client.paginateEventTimeline.callCount).toEqual(5);
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(false);
|
2017-09-06 18:40:58 +02:00
|
|
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
|
2016-04-08 15:58:24 +02:00
|
|
|
|
|
|
|
// now, if we update the events, there shouldn't be any
|
|
|
|
// more requests.
|
|
|
|
client.paginateEventTimeline.reset();
|
|
|
|
panel.forceUpdate();
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(false);
|
|
|
|
setTimeout(() => {
|
|
|
|
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
|
|
|
done();
|
|
|
|
}, 0);
|
2017-01-31 23:40:53 +01:00
|
|
|
}, 10);
|
2016-04-08 15:58:24 +02:00
|
|
|
});
|
2016-04-21 14:37:31 +02:00
|
|
|
|
2016-11-18 12:44:45 +01:00
|
|
|
it("should let you scroll down to the bottom after you've scrolled up", function(done) {
|
2017-10-11 18:56:17 +02:00
|
|
|
const N_EVENTS = 120; // the number of events to simulate being added to the timeline
|
2016-04-21 14:37:31 +02:00
|
|
|
|
|
|
|
// sadly, loading all those events takes a while
|
2016-08-24 16:48:19 +02:00
|
|
|
this.timeout(N_EVENTS * 50);
|
2016-04-21 14:37:31 +02:00
|
|
|
|
|
|
|
// client.getRoom is called a /lot/ in this test, so replace
|
|
|
|
// sinon's spy with a fast noop.
|
|
|
|
client.getRoom = function(id) { return null; };
|
|
|
|
|
|
|
|
// fill the timeline with lots of events
|
2017-10-11 18:56:17 +02:00
|
|
|
for (let i = 0; i < N_EVENTS; i++) {
|
2016-10-11 14:54:57 +02:00
|
|
|
timeline.addEvent(mkMessage(i));
|
2016-04-21 14:37:31 +02:00
|
|
|
}
|
2016-06-23 17:20:40 +02:00
|
|
|
console.log("added events to timeline");
|
2016-04-21 14:37:31 +02:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
let scrollDefer;
|
|
|
|
const rendered = ReactDOM.render(
|
|
|
|
<WrappedTimelinePanel timelineSet={timelineSet} onScroll={() => {scrollDefer.resolve();}} />,
|
|
|
|
parentDiv,
|
2016-04-21 14:37:31 +02:00
|
|
|
);
|
2016-06-23 17:20:40 +02:00
|
|
|
console.log("TimelinePanel rendered");
|
2017-10-11 18:56:17 +02:00
|
|
|
const panel = rendered.refs.panel;
|
|
|
|
const messagePanel = ReactTestUtils.findRenderedComponentWithType(
|
2016-04-21 14:37:31 +02:00
|
|
|
panel, sdk.getComponent('structures.MessagePanel'));
|
2017-10-11 18:56:17 +02:00
|
|
|
const scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
2016-04-21 14:37:31 +02:00
|
|
|
panel, "gm-scroll-view");
|
|
|
|
|
|
|
|
// helper function which will return a promise which resolves when
|
|
|
|
// the TimelinePanel fires a scroll event
|
2017-10-11 18:56:17 +02:00
|
|
|
const awaitScroll = function() {
|
2017-07-12 15:04:20 +02:00
|
|
|
scrollDefer = Promise.defer();
|
2016-11-18 12:44:45 +01:00
|
|
|
|
2016-08-03 16:23:12 +02:00
|
|
|
return scrollDefer.promise.then(() => {
|
|
|
|
console.log("got scroll event; scrollTop now " +
|
|
|
|
scrollingDiv.scrollTop);
|
|
|
|
});
|
2016-04-21 14:37:31 +02:00
|
|
|
};
|
|
|
|
|
2016-08-03 16:23:12 +02:00
|
|
|
function setScrollTop(scrollTop) {
|
|
|
|
const before = scrollingDiv.scrollTop;
|
|
|
|
scrollingDiv.scrollTop = scrollTop;
|
|
|
|
console.log("setScrollTop: before update: " + before +
|
|
|
|
"; assigned: " + scrollTop +
|
|
|
|
"; after update: " + scrollingDiv.scrollTop);
|
|
|
|
}
|
|
|
|
|
2016-04-21 14:37:31 +02:00
|
|
|
function backPaginate() {
|
2016-08-03 16:23:12 +02:00
|
|
|
console.log("back paginating...");
|
|
|
|
setScrollTop(0);
|
2016-04-21 14:37:31 +02:00
|
|
|
return awaitScroll().then(() => {
|
2017-10-11 18:56:17 +02:00
|
|
|
const eventTiles = scryEventTiles(panel);
|
|
|
|
const firstEvent = eventTiles[0].props.mxEvent;
|
2016-08-24 16:48:19 +02:00
|
|
|
|
|
|
|
console.log("TimelinePanel contains " + eventTiles.length +
|
|
|
|
" events; first is " +
|
|
|
|
firstEvent.getContent().body);
|
|
|
|
|
2016-04-21 14:37:31 +02:00
|
|
|
if(scrollingDiv.scrollTop > 0) {
|
|
|
|
// need to go further
|
|
|
|
return backPaginate();
|
|
|
|
}
|
2016-08-03 16:23:12 +02:00
|
|
|
console.log("paginated to start.");
|
2016-04-21 14:37:31 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-18 12:44:45 +01:00
|
|
|
function scrollDown() {
|
|
|
|
// Scroll the bottom of the viewport to the bottom of the panel
|
|
|
|
setScrollTop(scrollingDiv.scrollHeight - scrollingDiv.clientHeight);
|
|
|
|
console.log("scrolling down... " + scrollingDiv.scrollTop);
|
|
|
|
return awaitScroll().delay(0).then(() => {
|
2017-10-11 18:56:17 +02:00
|
|
|
const eventTiles = scryEventTiles(panel);
|
|
|
|
const events = timeline.getEvents();
|
2016-11-18 12:44:45 +01:00
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const lastEventInPanel = eventTiles[eventTiles.length - 1].props.mxEvent;
|
|
|
|
const lastEventInTimeline = events[events.length - 1];
|
2016-11-18 12:44:45 +01:00
|
|
|
|
|
|
|
// Scroll until the last event in the panel = the last event in the timeline
|
|
|
|
if(lastEventInPanel.getId() !== lastEventInTimeline.getId()) {
|
|
|
|
// need to go further
|
|
|
|
return scrollDown();
|
|
|
|
}
|
|
|
|
console.log("paginated to end.");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-04-21 14:37:31 +02:00
|
|
|
// let the first round of pagination finish off
|
|
|
|
awaitScroll().then(() => {
|
|
|
|
// we should now have loaded the first few events
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(false);
|
2017-09-06 18:40:58 +02:00
|
|
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
|
2016-04-21 14:37:31 +02:00
|
|
|
|
|
|
|
// back-paginate until we hit the start
|
|
|
|
return backPaginate();
|
|
|
|
}).then(() => {
|
2016-08-24 16:48:19 +02:00
|
|
|
// hopefully, we got to the start of the timeline
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(false);
|
|
|
|
|
2017-09-06 18:40:58 +02:00
|
|
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
|
2017-10-11 18:56:17 +02:00
|
|
|
const events = scryEventTiles(panel);
|
2016-08-24 16:48:19 +02:00
|
|
|
expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
|
2016-11-18 12:44:45 +01:00
|
|
|
|
2016-11-22 18:43:45 +01:00
|
|
|
// At this point, we make no assumption that unpagination has happened. This doesn't
|
|
|
|
// mean that we shouldn't be able to scroll all the way down to the bottom to see the
|
|
|
|
// most recent event in the timeline.
|
2016-11-18 12:44:45 +01:00
|
|
|
|
|
|
|
// scroll all the way to the bottom
|
|
|
|
return scrollDown();
|
2016-08-24 16:48:19 +02:00
|
|
|
}).then(() => {
|
|
|
|
expect(messagePanel.props.backPaginating).toBe(false);
|
|
|
|
expect(messagePanel.props.forwardPaginating).toBe(false);
|
|
|
|
|
2017-10-11 18:56:17 +02:00
|
|
|
const events = scryEventTiles(panel);
|
2016-08-24 16:48:19 +02:00
|
|
|
|
2016-11-18 12:44:45 +01:00
|
|
|
// Expect to be able to see the most recent event
|
2017-10-11 18:56:17 +02:00
|
|
|
const lastEventInPanel = events[events.length - 1].props.mxEvent;
|
|
|
|
const lastEventInTimeline = timeline.getEvents()[timeline.getEvents().length - 1];
|
2016-11-18 12:44:45 +01:00
|
|
|
expect(lastEventInPanel.getContent()).toBe(lastEventInTimeline.getContent());
|
2016-08-24 16:48:19 +02:00
|
|
|
|
2016-04-21 14:37:31 +02:00
|
|
|
console.log("done");
|
|
|
|
}).done(done, done);
|
|
|
|
});
|
2016-04-08 15:58:24 +02:00
|
|
|
});
|