diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js deleted file mode 100644 index 41d0f4469b..0000000000 --- a/test/components/structures/ScrollPanel-test.js +++ /dev/null @@ -1,283 +0,0 @@ -/* -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'; -import { EventEmitter } from "events"; - -const sdk = require('matrix-react-sdk'); - -const ScrollPanel = sdk.getComponent('structures.ScrollPanel'); -const test_utils = require('test-utils'); - -const Tester = React.createClass({ - getInitialState: function() { - return { - tileKeys: [], - resizeNotifier: new EventEmitter(), - }; - }, - - componentWillMount: function() { - this.fillCounts = {'b': 0, 'f': 0}; - this._fillHandlers = {'b': null, 'f': null}; - this._fillDefers = {'b': null, 'f': null}; - this._scrollDefer = null; - - // scrollTop at the last scroll event - this.lastScrollEvent = null; - }, - - _onFillRequest: function(back) { - const dir = back ? 'b': 'f'; - console.log("FillRequest: " + dir); - this.fillCounts[dir]++; - - const handler = this._fillHandlers[dir]; - const defer = this._fillDefers[dir]; - - // don't use the same handler twice - this._fillHandlers[dir] = null; - this._fillDefers[dir] = null; - - let res; - if (handler) { - res = handler(); - } else { - res = Promise.resolve(false); - } - - if (defer) { - defer.resolve(); - } - return res; - }, - - addFillHandler: function(dir, handler) { - this._fillHandlers[dir] = handler; - }, - - /* returns a promise which will resolve when the fill happens */ - awaitFill: function(dir) { - console.log("ScrollPanel Tester: awaiting " + dir + " fill"); - const defer = Promise.defer(); - this._fillDefers[dir] = defer; - return defer.promise; - }, - - _onScroll: function(ev) { - const st = ev.target.scrollTop; - console.log("ScrollPanel Tester: scroll event; scrollTop: " + st); - this.lastScrollEvent = st; - - const d = this._scrollDefer; - if (d) { - this._scrollDefer = null; - d.resolve(); - } - }, - - /* returns a promise which will resolve when a scroll event happens */ - awaitScroll: function() { - console.log("Awaiting scroll"); - this._scrollDefer = Promise.defer(); - return this._scrollDefer.promise; - }, - - setTileKeys: function(keys) { - console.log("Updating keys: len=" + keys.length); - this.setState({tileKeys: keys.slice()}); - }, - - scrollPanel: function() { - return this.refs.sp; - }, - - _mkTile: function(key) { - // each tile is 150 pixels high: - // 98 pixels of body - // 2 pixels of border - // 50 pixels of margin - // - // there is an extra 50 pixels of margin at the bottom. - return ( -
  • -
    - { key } -
    -
  • - ); - }, - - render: function() { - const tiles = this.state.tileKeys.map(this._mkTile); - console.log("rendering with " + tiles.length + " tiles"); - return ( - - { tiles } - - ); - }, -}); - -describe('ScrollPanel', function() { - let parentDiv; - let tester; - let scrollingDiv; - - beforeEach(function(done) { - test_utils.beforeEach(this); - - // 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'; - parentDiv.style.height = '600px'; - parentDiv.style.overflow = 'hidden'; - document.body.appendChild(parentDiv); - - tester = ReactDOM.render(, parentDiv); - expect(tester.fillCounts.b).toEqual(1); - expect(tester.fillCounts.f).toEqual(1); - - scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass( - tester, "gm-scroll-view"); - - // we need to make sure we don't call done() until q has finished - // running the completion handlers from the fill requests. We can't - // just use .done(), because that will end up ahead of those handlers - // in the queue. We can't use window.setTimeout(0), because that also might - // run ahead of those handlers. - const sp = tester.scrollPanel(); - let retriesRemaining = 1; - const awaitReady = function() { - return Promise.resolve().then(() => { - if (sp._pendingFillRequests.b === false && - sp._pendingFillRequests.f === false - ) { - return; - } - - if (retriesRemaining == 0) { - throw new Error("fillRequests did not complete"); - } - retriesRemaining--; - return awaitReady(); - }); - }; - awaitReady().done(done); - }); - - afterEach(function() { - if (parentDiv) { - document.body.removeChild(parentDiv); - parentDiv = null; - } - }); - - it('should handle scrollEvent strangeness', function() { - const events = []; - - return Promise.resolve().then(() => { - // initialise with a load of events - for (let i = 0; i < 20; i++) { - events.push(i+80); - } - tester.setTileKeys(events); - expect(scrollingDiv.scrollHeight).toEqual(3050); // 20*150 + 50 - expect(scrollingDiv.scrollTop).toEqual(3050 - 600); - return tester.awaitScroll(); - }).then(() => { - expect(tester.lastScrollEvent).toBe(3050 - 600); - - tester.scrollPanel().scrollToToken("92", 0); - - // at this point, ScrollPanel will have updated scrollTop, but - // the event hasn't fired. - expect(tester.lastScrollEvent).toEqual(3050 - 600); - expect(scrollingDiv.scrollTop).toEqual(1950); - - // now stamp over the scrollTop. - console.log('faking #528'); - scrollingDiv.scrollTop = 500; - - return tester.awaitScroll(); - }).then(() => { - expect(tester.lastScrollEvent).toBe(1950); - expect(scrollingDiv.scrollTop).toEqual(1950); - }); - }); - - it('should not get stuck in #528 workaround', function(done) { - let events = []; - Promise.resolve().then(() => { - // initialise with a bunch of events - for (let i = 0; i < 40; i++) { - events.push(i); - } - tester.setTileKeys(events); - expect(tester.fillCounts.b).toEqual(1); - expect(tester.fillCounts.f).toEqual(2); - expect(scrollingDiv.scrollHeight).toEqual(6050); // 40*150 + 50 - expect(scrollingDiv.scrollTop).toEqual(6050 - 600); - - // try to scroll up, to a non-integer offset. - tester.scrollPanel().scrollToToken("30", -101/3); - - expect(scrollingDiv.scrollTop).toEqual(4616); // 31*150 - 34 - - // wait for the scroll event to land - return tester.awaitScroll(); // fails - }).then(() => { - expect(tester.lastScrollEvent).toEqual(4616); - - // Now one more event; this will make it reset the scroll, but - // because the delta will be less than 1, will not trigger a - // scroll event, this leaving recentEventScroll defined. - console.log("Adding event 50"); - events.push(50); - tester.setTileKeys(events); - - // wait for the scrollpanel to stop trying to paginate - }).then(() => { - // Now, simulate hitting "scroll to bottom". - events = []; - for (let i = 100; i < 120; i++) { - events.push(i); - } - tester.setTileKeys(events); - tester.scrollPanel().scrollToBottom(); - - // wait for the scroll event to land - return tester.awaitScroll(); // fails - }).then(() => { - expect(scrollingDiv.scrollTop).toEqual(20*150 + 50 - 600); - - // simulate a user-initiated scroll on the div - scrollingDiv.scrollTop = 1200; - return tester.awaitScroll(); - }).then(() => { - expect(scrollingDiv.scrollTop).toEqual(1200); - }).done(done); - }); -}); diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js deleted file mode 100644 index 01ea6d8421..0000000000 --- a/test/components/structures/TimelinePanel-test.js +++ /dev/null @@ -1,372 +0,0 @@ -/* -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 ; - }, -}); - - -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.currentState = sinon.createStubInstance(jssdk.RoomState); - room.currentState.members = {}; - 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( - , - 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( - , - 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.resetHistory(); - 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( - {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); - }); -});