371 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			371 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
/*
 | 
						|
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.
 | 
						|
*/
 | 
						|
 | 
						|
const React = require('react');
 | 
						|
const ReactDOM = require('react-dom');
 | 
						|
const ReactTestUtils = require('react-addons-test-utils');
 | 
						|
const expect = require('expect');
 | 
						|
import Promise from 'bluebird';
 | 
						|
const sinon = require('sinon');
 | 
						|
 | 
						|
const jssdk = require('matrix-js-sdk');
 | 
						|
const EventTimeline = jssdk.EventTimeline;
 | 
						|
 | 
						|
const sdk = require('matrix-react-sdk');
 | 
						|
const TimelinePanel = sdk.getComponent('structures.TimelinePanel');
 | 
						|
const peg = require('../../../src/MatrixClientPeg');
 | 
						|
 | 
						|
const test_utils = require('test-utils');
 | 
						|
 | 
						|
const ROOM_ID = '!room:localhost';
 | 
						|
const USER_ID = '@me:localhost';
 | 
						|
 | 
						|
// 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} />;
 | 
						|
    },
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
describe('TimelinePanel', function() {
 | 
						|
    let sandbox;
 | 
						|
    let timelineSet;
 | 
						|
    let room;
 | 
						|
    let client;
 | 
						|
    let timeline;
 | 
						|
    let parentDiv;
 | 
						|
 | 
						|
    // 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) {
 | 
						|
        return test_utils.mkMessage(
 | 
						|
            {
 | 
						|
                event: true, room: ROOM_ID, user: USER_ID,
 | 
						|
                ts: Date.now() + eventNum,
 | 
						|
                msg: "Event " + eventNum,
 | 
						|
                ...opts,
 | 
						|
            });
 | 
						|
    }
 | 
						|
 | 
						|
    function scryEventTiles(panel) {
 | 
						|
        return ReactTestUtils.scryRenderedComponentsWithType(
 | 
						|
            panel, sdk.getComponent('rooms.EventTile'));
 | 
						|
    }
 | 
						|
 | 
						|
    beforeEach(function() {
 | 
						|
        test_utils.beforeEach(this);
 | 
						|
        sandbox = test_utils.stubClient(sandbox);
 | 
						|
 | 
						|
        room = sinon.createStubInstance(jssdk.Room);
 | 
						|
        room.roomId = ROOM_ID;
 | 
						|
 | 
						|
        timelineSet = sinon.createStubInstance(jssdk.EventTimelineSet);
 | 
						|
        timelineSet.getPendingEvents.returns([]);
 | 
						|
        timelineSet.room = room;
 | 
						|
 | 
						|
        timeline = new jssdk.EventTimeline(timelineSet);
 | 
						|
 | 
						|
        timelineSet.getLiveTimeline.returns(timeline);
 | 
						|
 | 
						|
        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';
 | 
						|
 | 
						|
        // This has to be slightly carefully chosen. We expect to have to do
 | 
						|
        // exactly one pagination to fill it.
 | 
						|
        parentDiv.style.height = '500px';
 | 
						|
 | 
						|
        parentDiv.style.overflow = 'hidden';
 | 
						|
        document.body.appendChild(parentDiv);
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(function() {
 | 
						|
        if (parentDiv) {
 | 
						|
            ReactDOM.unmountComponentAtNode(parentDiv);
 | 
						|
            parentDiv.remove();
 | 
						|
            parentDiv = null;
 | 
						|
        }
 | 
						|
        sandbox.restore();
 | 
						|
    });
 | 
						|
 | 
						|
    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
 | 
						|
        const N_EVENTS = 30;
 | 
						|
        for (let i = 0; i < N_EVENTS; i++) {
 | 
						|
            timeline.addEvent(mkMessage(i));
 | 
						|
        }
 | 
						|
 | 
						|
        let scrollDefer;
 | 
						|
        const onScroll = (e) => {
 | 
						|
            console.log(`TimelinePanel called onScroll: ${e.target.scrollTop}`);
 | 
						|
            if (scrollDefer) {
 | 
						|
                scrollDefer.resolve();
 | 
						|
            }
 | 
						|
        };
 | 
						|
        const rendered = ReactDOM.render(
 | 
						|
                <WrappedTimelinePanel timelineSet={timelineSet} onScroll={onScroll} />,
 | 
						|
                parentDiv,
 | 
						|
        );
 | 
						|
        const panel = rendered.refs.panel;
 | 
						|
        const scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
 | 
						|
            panel, "gm-scroll-view");
 | 
						|
 | 
						|
        // helper function which will return a promise which resolves when the
 | 
						|
        // panel isn't paginating
 | 
						|
        var awaitPaginationCompletion = function() {
 | 
						|
            if(!panel.state.forwardPaginating) {return Promise.resolve();} else {return Promise.delay(0).then(awaitPaginationCompletion);}
 | 
						|
        };
 | 
						|
 | 
						|
        // helper function which will return a promise which resolves when
 | 
						|
        // the TimelinePanel fires a scroll event
 | 
						|
        const awaitScroll = function() {
 | 
						|
            scrollDefer = Promise.defer();
 | 
						|
            return scrollDefer.promise;
 | 
						|
        };
 | 
						|
 | 
						|
        // let the first round of pagination finish off
 | 
						|
        Promise.delay(5).then(() => {
 | 
						|
            expect(panel.state.canBackPaginate).toBe(false);
 | 
						|
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
 | 
						|
 | 
						|
            // scroll up
 | 
						|
            console.log("setting scrollTop = 0");
 | 
						|
            scrollingDiv.scrollTop = 0;
 | 
						|
 | 
						|
            // wait for the scroll event to land
 | 
						|
        }).then(awaitScroll).then(() => {
 | 
						|
            expect(scrollingDiv.scrollTop).toEqual(0);
 | 
						|
 | 
						|
            // 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!
 | 
						|
            const ev = mkMessage(N_EVENTS+1);
 | 
						|
            timeline.addEvent(ev);
 | 
						|
            panel.onRoomTimeline(ev, room, false, false, {
 | 
						|
                liveEvent: true,
 | 
						|
                timeline: timeline,
 | 
						|
            });
 | 
						|
 | 
						|
            // 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.
 | 
						|
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
 | 
						|
 | 
						|
            scrollingDiv.scrollTop = 10;
 | 
						|
 | 
						|
            return awaitScroll();
 | 
						|
        }).then(awaitPaginationCompletion).then(() => {
 | 
						|
            expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
 | 
						|
        }).done(done, done);
 | 
						|
    });
 | 
						|
 | 
						|
    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
 | 
						|
        const d = Date.now();
 | 
						|
        for (let i = 0; i < 3; i++) {
 | 
						|
            timeline.addEvent(mkMessage(i));
 | 
						|
        }
 | 
						|
        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);
 | 
						|
            return Promise.resolve(true);
 | 
						|
        });
 | 
						|
 | 
						|
        const rendered = ReactDOM.render(
 | 
						|
            <WrappedTimelinePanel timelineSet={timelineSet} />,
 | 
						|
            parentDiv,
 | 
						|
        );
 | 
						|
        const panel = rendered.refs.panel;
 | 
						|
 | 
						|
        const messagePanel = ReactTestUtils.findRenderedComponentWithType(
 | 
						|
            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);
 | 
						|
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
 | 
						|
 | 
						|
            // 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);
 | 
						|
        }, 10);
 | 
						|
    });
 | 
						|
 | 
						|
    it("should let you scroll down to the bottom after you've scrolled up", function(done) {
 | 
						|
        const N_EVENTS = 120; // the number of events to simulate being added to the timeline
 | 
						|
 | 
						|
        // sadly, loading all those events takes a while
 | 
						|
        this.timeout(N_EVENTS * 50);
 | 
						|
 | 
						|
        // 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
 | 
						|
        for (let i = 0; i < N_EVENTS; i++) {
 | 
						|
            timeline.addEvent(mkMessage(i));
 | 
						|
        }
 | 
						|
        console.log("added events to timeline");
 | 
						|
 | 
						|
        let scrollDefer;
 | 
						|
        const rendered = ReactDOM.render(
 | 
						|
            <WrappedTimelinePanel timelineSet={timelineSet} onScroll={() => {scrollDefer.resolve();}} />,
 | 
						|
            parentDiv,
 | 
						|
        );
 | 
						|
        console.log("TimelinePanel rendered");
 | 
						|
        const panel = rendered.refs.panel;
 | 
						|
        const messagePanel = ReactTestUtils.findRenderedComponentWithType(
 | 
						|
            panel, sdk.getComponent('structures.MessagePanel'));
 | 
						|
        const scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
 | 
						|
            panel, "gm-scroll-view");
 | 
						|
 | 
						|
        // helper function which will return a promise which resolves when
 | 
						|
        // the TimelinePanel fires a scroll event
 | 
						|
        const awaitScroll = function() {
 | 
						|
            scrollDefer = Promise.defer();
 | 
						|
 | 
						|
            return scrollDefer.promise.then(() => {
 | 
						|
                console.log("got scroll event; scrollTop now " +
 | 
						|
                            scrollingDiv.scrollTop);
 | 
						|
            });
 | 
						|
        };
 | 
						|
 | 
						|
        function setScrollTop(scrollTop) {
 | 
						|
            const before = scrollingDiv.scrollTop;
 | 
						|
            scrollingDiv.scrollTop = scrollTop;
 | 
						|
            console.log("setScrollTop: before update: " + before +
 | 
						|
                        "; assigned: " + scrollTop +
 | 
						|
                        "; after update: " + scrollingDiv.scrollTop);
 | 
						|
        }
 | 
						|
 | 
						|
        function backPaginate() {
 | 
						|
            console.log("back paginating...");
 | 
						|
            setScrollTop(0);
 | 
						|
            return awaitScroll().then(() => {
 | 
						|
                const eventTiles = scryEventTiles(panel);
 | 
						|
                const firstEvent = eventTiles[0].props.mxEvent;
 | 
						|
 | 
						|
                console.log("TimelinePanel contains " + eventTiles.length +
 | 
						|
                            " events; first is " +
 | 
						|
                            firstEvent.getContent().body);
 | 
						|
 | 
						|
                if(scrollingDiv.scrollTop > 0) {
 | 
						|
                    // need to go further
 | 
						|
                    return backPaginate();
 | 
						|
                }
 | 
						|
                console.log("paginated to start.");
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        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(() => {
 | 
						|
                const eventTiles = scryEventTiles(panel);
 | 
						|
                const events = timeline.getEvents();
 | 
						|
 | 
						|
                const lastEventInPanel = eventTiles[eventTiles.length - 1].props.mxEvent;
 | 
						|
                const lastEventInTimeline = events[events.length - 1];
 | 
						|
 | 
						|
                // 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.");
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        // 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);
 | 
						|
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
 | 
						|
 | 
						|
            // back-paginate until we hit the start
 | 
						|
            return backPaginate();
 | 
						|
        }).then(() => {
 | 
						|
            // hopefully, we got to the start of the timeline
 | 
						|
            expect(messagePanel.props.backPaginating).toBe(false);
 | 
						|
 | 
						|
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
 | 
						|
            const events = scryEventTiles(panel);
 | 
						|
            expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
 | 
						|
 | 
						|
            // 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.
 | 
						|
 | 
						|
            // scroll all the way to the bottom
 | 
						|
            return scrollDown();
 | 
						|
        }).then(() => {
 | 
						|
            expect(messagePanel.props.backPaginating).toBe(false);
 | 
						|
            expect(messagePanel.props.forwardPaginating).toBe(false);
 | 
						|
 | 
						|
            const events = scryEventTiles(panel);
 | 
						|
 | 
						|
            // Expect to be able to see the most recent event
 | 
						|
            const lastEventInPanel = events[events.length - 1].props.mxEvent;
 | 
						|
            const lastEventInTimeline = timeline.getEvents()[timeline.getEvents().length - 1];
 | 
						|
            expect(lastEventInPanel.getContent()).toBe(lastEventInTimeline.getContent());
 | 
						|
 | 
						|
            console.log("done");
 | 
						|
        }).done(done, done);
 | 
						|
    });
 | 
						|
});
 |