diff --git a/karma.conf.js b/karma.conf.js index 89437c203a..eed6d580fa 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,6 +1,7 @@ // karma.conf.js - the config file for karma, which runs our tests. var path = require('path'); +var fs = require('fs'); /* * We use webpack to build our tests. It's a pain to have to wait for webpack @@ -17,6 +18,21 @@ var path = require('path'); process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs'; +function fileExists(name) { + try { + fs.statSync(gsCss); + return true; + } catch (e) { + return false; + } +} + +// try find the gemini-scrollbar css in an npm-version-agnostic way +var gsCss = 'node_modules/gemini-scrollbar/gemini-scrollbar.css'; +if (!fileExists(gsCss)) { + gsCss = 'node_modules/react-gemini-scrollbar/'+gsCss; +} + module.exports = function (config) { config.set({ // frameworks to use @@ -26,6 +42,7 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ 'test/tests.js', + gsCss, ], // list of files to exclude diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index ddec81c107..6825e41cdb 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -20,6 +20,7 @@ var GeminiScrollbar = require('react-gemini-scrollbar'); var q = require("q"); var DEBUG_SCROLL = false; +// var DEBUG_SCROLL = true; if (DEBUG_SCROLL) { // using bind means that we get to keep useful line numbers in the console @@ -144,7 +145,8 @@ module.exports = React.createClass({ onScroll: function(ev) { var sn = this._getScrollNode(); - debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll); + debuglog("Scroll event: offset now:", sn.scrollTop, + "_lastSetScroll:", this._lastSetScroll); // Sometimes we see attempts to write to scrollTop essentially being // ignored. (Or rather, it is successfully written, but on the next @@ -158,13 +160,10 @@ module.exports = React.createClass({ // By way of a workaround, we detect this situation and just keep // resetting scrollTop until we see the scroll node have the right // value. - if (this.recentEventScroll !== undefined) { - if(sn.scrollTop < this.recentEventScroll-200) { - console.log("Working around vector-im/vector-web#528"); - this._restoreSavedScrollState(); - return; - } - this.recentEventScroll = undefined; + if (this._lastSetScroll !== undefined && sn.scrollTop < this._lastSetScroll-200) { + console.log("Working around vector-im/vector-web#528"); + this._restoreSavedScrollState(); + return; } // If there weren't enough children to fill the viewport, the scroll we @@ -395,17 +394,14 @@ module.exports = React.createClass({ var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect(); var boundingRect = node.getBoundingClientRect(); var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; + + debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" + + pixelOffset + " (delta: "+scrollDelta+")"); + if(scrollDelta != 0) { this._setScrollTop(scrollNode.scrollTop + scrollDelta); - - // see the comments in onScroll regarding recentEventScroll - this.recentEventScroll = scrollNode.scrollTop; } - debuglog("Scrolled to token", node.dataset.scrollToken, "+", - pixelOffset+":", scrollNode.scrollTop, - "(delta: "+scrollDelta+")"); - debuglog("recentEventScroll now "+this.recentEventScroll); }, _saveScrollState: function() { diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js new file mode 100644 index 0000000000..13721c9ecd --- /dev/null +++ b/test/components/structures/ScrollPanel-test.js @@ -0,0 +1,275 @@ +/* +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. +*/ + +var React = require('react'); +var ReactDOM = require("react-dom"); +var ReactTestUtils = require('react-addons-test-utils'); +var expect = require('expect'); +var q = require('q'); + +var sdk = require('matrix-react-sdk'); + +var ScrollPanel = sdk.getComponent('structures.ScrollPanel'); +var test_utils = require('test-utils'); + +var Tester = React.createClass({ + getInitialState: function() { + return { + tileKeys: [], + }; + }, + + 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) { + var dir = back ? 'b': 'f'; + console.log("FillRequest: " + dir); + this.fillCounts[dir]++; + + var handler = this._fillHandlers[dir]; + var defer = this._fillDefers[dir]; + + // don't use the same handler twice + this._fillHandlers[dir] = null; + this._fillDefers[dir] = null; + + var res; + if (handler) { + res = handler(); + } else { + res = q(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) { + var defer = q.defer(); + this._fillDefers[dir] = defer; + return defer.promise; + }, + + _onScroll: function(ev) { + var st = ev.target.scrollTop; + console.log("Scroll event; scrollTop: " + st); + this.lastScrollEvent = st; + + var 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 = q.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 ( +
This is useful for use with integration tests which use asyncronous - * methods: it can be added as a 'catch' handler in a promise chain. - * - * @param {Error} error exception to be reported - * - * @example - * it("should not throw", function(done) { - * asynchronousMethod().then(function() { - * // some tests - * }).catch(utils.failTest).done(done); - * }); - */ -module.exports.failTest = function(error) { - expect(error.stack).toBe(null); -};