Some basic tests for MessagePanel
Check that it puts the read marker in the right place, and check that the animation works. ... all of which has been surprisingly painful.pull/21833/head
parent
d2e69c819f
commit
dc5c0928b2
|
@ -1,6 +1,5 @@
|
||||||
// karma.conf.js - the config file for karma, which runs our tests.
|
// karma.conf.js - the config file for karma, which runs our tests.
|
||||||
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -8,6 +7,10 @@ var path = require('path');
|
||||||
* to build everything; however it's the easiest way to load our dependencies
|
* to build everything; however it's the easiest way to load our dependencies
|
||||||
* from node_modules.
|
* from node_modules.
|
||||||
*
|
*
|
||||||
|
* If you run karma in multi-run mode (with `npm run test-multi`), it will watch
|
||||||
|
* the tests for changes, and webpack will rebuild using a cache. This is much quicker
|
||||||
|
* than a clean rebuild.
|
||||||
|
*
|
||||||
* TODO:
|
* TODO:
|
||||||
* - can we run one test at a time?
|
* - can we run one test at a time?
|
||||||
*/
|
*/
|
||||||
|
@ -26,11 +29,18 @@ module.exports = function (config) {
|
||||||
],
|
],
|
||||||
|
|
||||||
// list of files to exclude
|
// list of files to exclude
|
||||||
// (this doesn't work, and I don't know why - we still rerun the tests
|
//
|
||||||
// when lockfiles are created)
|
// This doesn't work. It turns out that it's webpack which does the
|
||||||
exclude: [
|
// watching of the /test directory (possibly karma only watches
|
||||||
'**/.#*'
|
// tests.js itself). Webpack watches the directory so that it can spot
|
||||||
],
|
// new tests, which is fair enough; unfortunately it triggers a rebuild
|
||||||
|
// every time a lockfile is created in that directory, and there
|
||||||
|
// doesn't seem to be any way to tell webpack to ignore particular
|
||||||
|
// files in a watched directory.
|
||||||
|
//
|
||||||
|
// exclude: [
|
||||||
|
// '**/.#*'
|
||||||
|
// ],
|
||||||
|
|
||||||
// preprocess matching files before serving them to the browser
|
// preprocess matching files before serving them to the browser
|
||||||
// available preprocessors:
|
// available preprocessors:
|
||||||
|
@ -83,13 +93,6 @@ module.exports = function (config) {
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
loaders: [
|
||||||
{ test: /\.json$/, loader: "json" },
|
{ test: /\.json$/, loader: "json" },
|
||||||
{
|
|
||||||
// disable 'require' and 'define' for sinon, per
|
|
||||||
// https://github.com/webpack/webpack/issues/304#issuecomment-170883329
|
|
||||||
test: /sinon\/pkg\/sinon\.js/,
|
|
||||||
// TODO: use 'query'?
|
|
||||||
loader: 'imports?define=>false,require=>false',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.js$/, loader: "babel",
|
test: /\.js$/, loader: "babel",
|
||||||
include: [path.resolve('./src'),
|
include: [path.resolve('./src'),
|
||||||
|
@ -107,6 +110,11 @@ module.exports = function (config) {
|
||||||
// there is no need for webpack to parse them - they can
|
// there is no need for webpack to parse them - they can
|
||||||
// just be included as-is.
|
// just be included as-is.
|
||||||
/highlight\.js\/lib\/languages/,
|
/highlight\.js\/lib\/languages/,
|
||||||
|
|
||||||
|
// also disable parsing for sinon, because it
|
||||||
|
// tries to do voodoo with 'require' which upsets
|
||||||
|
// webpack (https://github.com/webpack/webpack/issues/304)
|
||||||
|
/sinon\/pkg\/sinon\.js$/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -114,6 +122,10 @@ module.exports = function (config) {
|
||||||
'matrix-react-sdk': path.resolve('src/index.js'),
|
'matrix-react-sdk': path.resolve('src/index.js'),
|
||||||
'sinon': 'sinon/pkg/sinon.js',
|
'sinon': 'sinon/pkg/sinon.js',
|
||||||
},
|
},
|
||||||
|
root: [
|
||||||
|
path.resolve('./src'),
|
||||||
|
path.resolve('./test'),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
"babel-preset-react": "^6.5.0",
|
"babel-preset-react": "^6.5.0",
|
||||||
"babel-runtime": "^6.6.1",
|
"babel-runtime": "^6.6.1",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"imports-loader": "^0.6.5",
|
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^0.13.22",
|
||||||
"karma-chrome-launcher": "^0.2.3",
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
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 TestUtils = require('react-addons-test-utils');
|
||||||
|
var expect = require('expect');
|
||||||
|
|
||||||
|
var sdk = require('matrix-react-sdk');
|
||||||
|
|
||||||
|
var MessagePanel = sdk.getComponent('structures.MessagePanel');
|
||||||
|
|
||||||
|
var test_utils = require('test-utils');
|
||||||
|
var mockclock = require('mock-clock');
|
||||||
|
|
||||||
|
describe('MessagePanel', function () {
|
||||||
|
var clock = mockclock.clock();
|
||||||
|
var realSetTimeout = window.setTimeout;
|
||||||
|
var events = mkEvents();
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
clock.uninstall();
|
||||||
|
});
|
||||||
|
|
||||||
|
function mkEvents() {
|
||||||
|
var events = [];
|
||||||
|
var ts0 = Date.now();
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
events.push(test_utils.mkMessage(
|
||||||
|
{
|
||||||
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
|
ts: ts0 + i*1000,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should show the events', function() {
|
||||||
|
var res = TestUtils.renderIntoDocument(
|
||||||
|
<MessagePanel events={events} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// just check we have the right number of tiles for now
|
||||||
|
var tiles = TestUtils.scryRenderedComponentsWithType(
|
||||||
|
res, sdk.getComponent('rooms.EventTile'));
|
||||||
|
expect(tiles.length).toEqual(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the read-marker in the right place', function() {
|
||||||
|
var res = TestUtils.renderIntoDocument(
|
||||||
|
<MessagePanel events={events} readMarkerEventId={events[4].getId()}
|
||||||
|
readMarkerVisible={true} />
|
||||||
|
);
|
||||||
|
|
||||||
|
var tiles = TestUtils.scryRenderedComponentsWithType(
|
||||||
|
res, sdk.getComponent('rooms.EventTile'));
|
||||||
|
|
||||||
|
// find the <li> which wraps the read marker
|
||||||
|
var rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
|
||||||
|
|
||||||
|
// it should follow the <li> which wraps the event tile for event 4
|
||||||
|
var eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
||||||
|
expect(rm.previousSibling).toEqual(eventContainer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a ghost read-marker when the read-marker moves', function(done) {
|
||||||
|
// fake the clock so that we can test the velocity animation.
|
||||||
|
clock.install();
|
||||||
|
clock.mockDate();
|
||||||
|
|
||||||
|
var parentDiv = document.createElement('div');
|
||||||
|
|
||||||
|
// first render with the RM in one place
|
||||||
|
var mp = ReactDOM.render(
|
||||||
|
<MessagePanel events={events} readMarkerEventId={events[4].getId()}
|
||||||
|
readMarkerVisible={true}
|
||||||
|
/>, parentDiv);
|
||||||
|
|
||||||
|
var tiles = TestUtils.scryRenderedComponentsWithType(
|
||||||
|
mp, sdk.getComponent('rooms.EventTile'));
|
||||||
|
|
||||||
|
// find the <li> which wraps the read marker
|
||||||
|
var rm = TestUtils.findRenderedDOMComponentWithClass(mp, 'mx_RoomView_myReadMarker_container');
|
||||||
|
var eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
||||||
|
expect(rm.previousSibling).toEqual(eventContainer);
|
||||||
|
|
||||||
|
// now move the RM
|
||||||
|
mp = ReactDOM.render(
|
||||||
|
<MessagePanel events={events} readMarkerEventId={events[6].getId()}
|
||||||
|
readMarkerVisible={true}
|
||||||
|
/>, parentDiv);
|
||||||
|
|
||||||
|
// now there should be two RM containers
|
||||||
|
var found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container');
|
||||||
|
expect(found.length).toEqual(2);
|
||||||
|
|
||||||
|
// the first should be the ghost
|
||||||
|
var ghost = found[0];
|
||||||
|
eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
||||||
|
expect(ghost.previousSibling).toEqual(eventContainer);
|
||||||
|
var hr = ghost.children[0];
|
||||||
|
console.log("Opacity:", hr.style.opacity);
|
||||||
|
|
||||||
|
// the first should be the ghost
|
||||||
|
eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
||||||
|
expect(found[0].previousSibling).toEqual(eventContainer);
|
||||||
|
|
||||||
|
// the second should be the real thing
|
||||||
|
eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
||||||
|
expect(ghost.previousSibling).toEqual(eventContainer);
|
||||||
|
|
||||||
|
// advance the clock, and then let the browser run an animation frame,
|
||||||
|
// to let the animation start
|
||||||
|
clock.tick(1500);
|
||||||
|
|
||||||
|
realSetTimeout(() => {
|
||||||
|
// then advance it again to let it complete
|
||||||
|
clock.tick(1000);
|
||||||
|
realSetTimeout(() => {
|
||||||
|
// the ghost should now have finished
|
||||||
|
expect(hr.style.opacity).toEqual(0);
|
||||||
|
done();
|
||||||
|
}, 100);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,8 +4,17 @@
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = function(opts) {
|
||||||
render: function() {
|
opts = opts || {};
|
||||||
return <div />;
|
if (!opts.displayName) {
|
||||||
},
|
opts.displayName = 'StubComponent';
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (!opts.render) {
|
||||||
|
opts.render = function() {
|
||||||
|
return <div>{this.displayName}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createClass(opts);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,420 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2008-2015 Pivotal Labs
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* This is jasmine's implementation of a mock clock, lifted from the depths of
|
||||||
|
* jasmine-core and exposed as a standalone module. The interface is just the
|
||||||
|
* same as that of jasmine.clock. For example:
|
||||||
|
*
|
||||||
|
* var mock_clock = require("mock-clock").clock();
|
||||||
|
* mock_clock.install();
|
||||||
|
* setTimeout(function() {
|
||||||
|
* timerCallback();
|
||||||
|
* }, 100);
|
||||||
|
*
|
||||||
|
* expect(timerCallback).not.toHaveBeenCalled();
|
||||||
|
* mock_clock.tick(101);
|
||||||
|
* expect(timerCallback).toHaveBeenCalled();
|
||||||
|
*
|
||||||
|
* mock_clock.uninstall();
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The reason for C&Ping jasmine's clock here is that jasmine itself is
|
||||||
|
* difficult to webpack, and we don't really want all of it. Sinon also has a
|
||||||
|
* mock-clock implementation, but again, it is difficult to webpack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var j$ = {};
|
||||||
|
|
||||||
|
j$.Clock = function () {
|
||||||
|
function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
|
||||||
|
var self = this,
|
||||||
|
realTimingFunctions = {
|
||||||
|
setTimeout: global.setTimeout,
|
||||||
|
clearTimeout: global.clearTimeout,
|
||||||
|
setInterval: global.setInterval,
|
||||||
|
clearInterval: global.clearInterval
|
||||||
|
},
|
||||||
|
fakeTimingFunctions = {
|
||||||
|
setTimeout: setTimeout,
|
||||||
|
clearTimeout: clearTimeout,
|
||||||
|
setInterval: setInterval,
|
||||||
|
clearInterval: clearInterval
|
||||||
|
},
|
||||||
|
installed = false,
|
||||||
|
delayedFunctionScheduler,
|
||||||
|
timer;
|
||||||
|
|
||||||
|
|
||||||
|
self.install = function() {
|
||||||
|
if(!originalTimingFunctionsIntact()) {
|
||||||
|
throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?');
|
||||||
|
}
|
||||||
|
replace(global, fakeTimingFunctions);
|
||||||
|
timer = fakeTimingFunctions;
|
||||||
|
delayedFunctionScheduler = delayedFunctionSchedulerFactory();
|
||||||
|
installed = true;
|
||||||
|
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.uninstall = function() {
|
||||||
|
delayedFunctionScheduler = null;
|
||||||
|
mockDate.uninstall();
|
||||||
|
replace(global, realTimingFunctions);
|
||||||
|
|
||||||
|
timer = realTimingFunctions;
|
||||||
|
installed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.withMock = function(closure) {
|
||||||
|
this.install();
|
||||||
|
try {
|
||||||
|
closure();
|
||||||
|
} finally {
|
||||||
|
this.uninstall();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.mockDate = function(initialDate) {
|
||||||
|
mockDate.install(initialDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.setTimeout = function(fn, delay, params) {
|
||||||
|
if (legacyIE()) {
|
||||||
|
if (arguments.length > 2) {
|
||||||
|
throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill');
|
||||||
|
}
|
||||||
|
return timer.setTimeout(fn, delay);
|
||||||
|
}
|
||||||
|
return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.setInterval = function(fn, delay, params) {
|
||||||
|
if (legacyIE()) {
|
||||||
|
if (arguments.length > 2) {
|
||||||
|
throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill');
|
||||||
|
}
|
||||||
|
return timer.setInterval(fn, delay);
|
||||||
|
}
|
||||||
|
return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.clearTimeout = function(id) {
|
||||||
|
return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.clearInterval = function(id) {
|
||||||
|
return Function.prototype.call.apply(timer.clearInterval, [global, id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tick = function(millis) {
|
||||||
|
if (installed) {
|
||||||
|
mockDate.tick(millis);
|
||||||
|
delayedFunctionScheduler.tick(millis);
|
||||||
|
} else {
|
||||||
|
throw new Error('Mock clock is not installed, use jasmine.clock().install()');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
|
||||||
|
function originalTimingFunctionsIntact() {
|
||||||
|
return global.setTimeout === realTimingFunctions.setTimeout &&
|
||||||
|
global.clearTimeout === realTimingFunctions.clearTimeout &&
|
||||||
|
global.setInterval === realTimingFunctions.setInterval &&
|
||||||
|
global.clearInterval === realTimingFunctions.clearInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyIE() {
|
||||||
|
//if these methods are polyfilled, apply will be present
|
||||||
|
return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(dest, source) {
|
||||||
|
for (var prop in source) {
|
||||||
|
dest[prop] = source[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTimeout(fn, delay) {
|
||||||
|
return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTimeout(id) {
|
||||||
|
return delayedFunctionScheduler.removeFunctionWithId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInterval(fn, interval) {
|
||||||
|
return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInterval(id) {
|
||||||
|
return delayedFunctionScheduler.removeFunctionWithId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function argSlice(argsObj, n) {
|
||||||
|
return Array.prototype.slice.call(argsObj, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Clock;
|
||||||
|
}();
|
||||||
|
|
||||||
|
|
||||||
|
j$.DelayedFunctionScheduler = function() {
|
||||||
|
function DelayedFunctionScheduler() {
|
||||||
|
var self = this;
|
||||||
|
var scheduledLookup = [];
|
||||||
|
var scheduledFunctions = {};
|
||||||
|
var currentTime = 0;
|
||||||
|
var delayedFnCount = 0;
|
||||||
|
|
||||||
|
self.tick = function(millis) {
|
||||||
|
millis = millis || 0;
|
||||||
|
var endTime = currentTime + millis;
|
||||||
|
|
||||||
|
runScheduledFunctions(endTime);
|
||||||
|
currentTime = endTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
|
||||||
|
var f;
|
||||||
|
if (typeof(funcToCall) === 'string') {
|
||||||
|
/* jshint evil: true */
|
||||||
|
f = function() { return eval(funcToCall); };
|
||||||
|
/* jshint evil: false */
|
||||||
|
} else {
|
||||||
|
f = funcToCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
millis = millis || 0;
|
||||||
|
timeoutKey = timeoutKey || ++delayedFnCount;
|
||||||
|
runAtMillis = runAtMillis || (currentTime + millis);
|
||||||
|
|
||||||
|
var funcToSchedule = {
|
||||||
|
runAtMillis: runAtMillis,
|
||||||
|
funcToCall: f,
|
||||||
|
recurring: recurring,
|
||||||
|
params: params,
|
||||||
|
timeoutKey: timeoutKey,
|
||||||
|
millis: millis
|
||||||
|
};
|
||||||
|
|
||||||
|
if (runAtMillis in scheduledFunctions) {
|
||||||
|
scheduledFunctions[runAtMillis].push(funcToSchedule);
|
||||||
|
} else {
|
||||||
|
scheduledFunctions[runAtMillis] = [funcToSchedule];
|
||||||
|
scheduledLookup.push(runAtMillis);
|
||||||
|
scheduledLookup.sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeoutKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.removeFunctionWithId = function(timeoutKey) {
|
||||||
|
for (var runAtMillis in scheduledFunctions) {
|
||||||
|
var funcs = scheduledFunctions[runAtMillis];
|
||||||
|
var i = indexOfFirstToPass(funcs, function (func) {
|
||||||
|
return func.timeoutKey === timeoutKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i > -1) {
|
||||||
|
if (funcs.length === 1) {
|
||||||
|
delete scheduledFunctions[runAtMillis];
|
||||||
|
deleteFromLookup(runAtMillis);
|
||||||
|
} else {
|
||||||
|
funcs.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// intervals get rescheduled when executed, so there's never more
|
||||||
|
// than a single scheduled function with a given timeoutKey
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
|
||||||
|
function indexOfFirstToPass(array, testFn) {
|
||||||
|
var index = -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < array.length; ++i) {
|
||||||
|
if (testFn(array[i])) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFromLookup(key) {
|
||||||
|
var value = Number(key);
|
||||||
|
var i = indexOfFirstToPass(scheduledLookup, function (millis) {
|
||||||
|
return millis === value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i > -1) {
|
||||||
|
scheduledLookup.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reschedule(scheduledFn) {
|
||||||
|
self.scheduleFunction(scheduledFn.funcToCall,
|
||||||
|
scheduledFn.millis,
|
||||||
|
scheduledFn.params,
|
||||||
|
true,
|
||||||
|
scheduledFn.timeoutKey,
|
||||||
|
scheduledFn.runAtMillis + scheduledFn.millis);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEachFunction(funcsToRun, callback) {
|
||||||
|
for (var i = 0; i < funcsToRun.length; ++i) {
|
||||||
|
callback(funcsToRun[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runScheduledFunctions(endTime) {
|
||||||
|
if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
currentTime = scheduledLookup.shift();
|
||||||
|
|
||||||
|
var funcsToRun = scheduledFunctions[currentTime];
|
||||||
|
delete scheduledFunctions[currentTime];
|
||||||
|
|
||||||
|
forEachFunction(funcsToRun, function(funcToRun) {
|
||||||
|
if (funcToRun.recurring) {
|
||||||
|
reschedule(funcToRun);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
forEachFunction(funcsToRun, function(funcToRun) {
|
||||||
|
funcToRun.funcToCall.apply(null, funcToRun.params || []);
|
||||||
|
});
|
||||||
|
} while (scheduledLookup.length > 0 &&
|
||||||
|
// checking first if we're out of time prevents setTimeout(0)
|
||||||
|
// scheduled in a funcToRun from forcing an extra iteration
|
||||||
|
currentTime !== endTime &&
|
||||||
|
scheduledLookup[0] <= endTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DelayedFunctionScheduler;
|
||||||
|
}();
|
||||||
|
|
||||||
|
|
||||||
|
j$.MockDate = function() {
|
||||||
|
function MockDate(global) {
|
||||||
|
var self = this;
|
||||||
|
var currentTime = 0;
|
||||||
|
|
||||||
|
if (!global || !global.Date) {
|
||||||
|
self.install = function() {};
|
||||||
|
self.tick = function() {};
|
||||||
|
self.uninstall = function() {};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
var GlobalDate = global.Date;
|
||||||
|
|
||||||
|
self.install = function(mockDate) {
|
||||||
|
if (mockDate instanceof GlobalDate) {
|
||||||
|
currentTime = mockDate.getTime();
|
||||||
|
} else {
|
||||||
|
currentTime = new GlobalDate().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Date = FakeDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tick = function(millis) {
|
||||||
|
millis = millis || 0;
|
||||||
|
currentTime = currentTime + millis;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.uninstall = function() {
|
||||||
|
currentTime = 0;
|
||||||
|
global.Date = GlobalDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
createDateProperties();
|
||||||
|
|
||||||
|
return self;
|
||||||
|
|
||||||
|
function FakeDate() {
|
||||||
|
switch(arguments.length) {
|
||||||
|
case 0:
|
||||||
|
return new GlobalDate(currentTime);
|
||||||
|
case 1:
|
||||||
|
return new GlobalDate(arguments[0]);
|
||||||
|
case 2:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1]);
|
||||||
|
case 3:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2]);
|
||||||
|
case 4:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]);
|
||||||
|
case 5:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||||||
|
arguments[4]);
|
||||||
|
case 6:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||||||
|
arguments[4], arguments[5]);
|
||||||
|
default:
|
||||||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||||||
|
arguments[4], arguments[5], arguments[6]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDateProperties() {
|
||||||
|
FakeDate.prototype = GlobalDate.prototype;
|
||||||
|
|
||||||
|
FakeDate.now = function() {
|
||||||
|
if (GlobalDate.now) {
|
||||||
|
return currentTime;
|
||||||
|
} else {
|
||||||
|
throw new Error('Browser does not support Date.now()');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FakeDate.toSource = GlobalDate.toSource;
|
||||||
|
FakeDate.toString = GlobalDate.toString;
|
||||||
|
FakeDate.parse = GlobalDate.parse;
|
||||||
|
FakeDate.UTC = GlobalDate.UTC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MockDate;
|
||||||
|
}();
|
||||||
|
|
||||||
|
var clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global));
|
||||||
|
|
||||||
|
module.exports.clock = function() {
|
||||||
|
return clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,17 @@
|
||||||
* provide
|
* provide
|
||||||
*/
|
*/
|
||||||
var components = require('../src/component-index.js').components;
|
var components = require('../src/component-index.js').components;
|
||||||
var stub = require('./components/stub-component.js');
|
var stubComponent = require('./components/stub-component.js');
|
||||||
|
|
||||||
components['structures.LeftPanel'] = stub;
|
components['structures.LeftPanel'] = stubComponent();
|
||||||
components['structures.RightPanel'] = stub;
|
components['structures.RightPanel'] = stubComponent();
|
||||||
components['structures.RoomDirectory'] = stub;
|
components['structures.RoomDirectory'] = stubComponent();
|
||||||
components['views.globals.MatrixToolbar'] = stub;
|
components['views.globals.MatrixToolbar'] = stubComponent();
|
||||||
components['views.globals.GuestWarningBar'] = stub;
|
components['views.globals.GuestWarningBar'] = stubComponent();
|
||||||
components['views.globals.NewVersionBar'] = stub;
|
components['views.globals.NewVersionBar'] = stubComponent();
|
||||||
components['views.elements.Spinner'] = stub;
|
components['views.elements.Spinner'] = stubComponent({displayName: 'Spinner'});
|
||||||
|
components['views.messages.DateSeparator'] = stubComponent({displayName: 'DateSeparator'});
|
||||||
|
components['views.messages.MessageTimestamp'] = stubComponent({displayName: 'MessageTimestamp'});
|
||||||
|
components['views.messages.SenderProfile'] = stubComponent({displayName: 'SenderProfile'});
|
||||||
|
|
||||||
module.exports.components = components;
|
module.exports.components = components;
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
var peg = require('../src/MatrixClientPeg.js');
|
var peg = require('../src/MatrixClientPeg.js');
|
||||||
var jssdk = require('matrix-js-sdk');
|
var jssdk = require('matrix-js-sdk');
|
||||||
|
var MatrixEvent = jssdk.MatrixEvent;
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub out the MatrixClient, and configure the MatrixClientPeg object to
|
* Stub out the MatrixClient, and configure the MatrixClientPeg object to
|
||||||
* return it when get() is called.
|
* return it when get() is called.
|
||||||
|
@ -16,6 +18,117 @@ module.exports.stubClient = function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Event.
|
||||||
|
* @param {Object} opts Values for the event.
|
||||||
|
* @param {string} opts.type The event.type
|
||||||
|
* @param {string} opts.room The event.room_id
|
||||||
|
* @param {string} opts.user The event.user_id
|
||||||
|
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||||
|
* @param {Number} opts.ts Optional. Timestamp for the event
|
||||||
|
* @param {Object} opts.content The event.content
|
||||||
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
|
* @return {Object} a JSON object representing this event.
|
||||||
|
*/
|
||||||
|
module.exports.mkEvent = function(opts) {
|
||||||
|
if (!opts.type || !opts.content) {
|
||||||
|
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||||
|
}
|
||||||
|
var event = {
|
||||||
|
type: opts.type,
|
||||||
|
room_id: opts.room,
|
||||||
|
sender: opts.user,
|
||||||
|
content: opts.content,
|
||||||
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
origin_server_ts: opts.ts,
|
||||||
|
};
|
||||||
|
if (opts.skey) {
|
||||||
|
event.state_key = opts.skey;
|
||||||
|
}
|
||||||
|
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||||
|
"m.room.power_levels", "m.room.topic",
|
||||||
|
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||||
|
event.state_key = "";
|
||||||
|
}
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.presence event.
|
||||||
|
* @param {Object} opts Values for the presence.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
module.exports.mkPresence = function(opts) {
|
||||||
|
if (!opts.user) {
|
||||||
|
throw new Error("Missing user");
|
||||||
|
}
|
||||||
|
var event = {
|
||||||
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
type: "m.presence",
|
||||||
|
sender: opts.user,
|
||||||
|
content: {
|
||||||
|
avatar_url: opts.url,
|
||||||
|
displayname: opts.name,
|
||||||
|
last_active_ago: opts.ago,
|
||||||
|
presence: opts.presence || "offline"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.member event.
|
||||||
|
* @param {Object} opts Values for the membership.
|
||||||
|
* @param {string} opts.room The room ID for the event.
|
||||||
|
* @param {string} opts.mship The content.membership for the event.
|
||||||
|
* @param {string} opts.user The user ID for the event.
|
||||||
|
* @param {string} opts.skey The other user ID for the event if applicable
|
||||||
|
* e.g. for invites/bans.
|
||||||
|
* @param {string} opts.name The content.displayname for the event.
|
||||||
|
* @param {string} opts.url The content.avatar_url for the event.
|
||||||
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
module.exports.mkMembership = function(opts) {
|
||||||
|
opts.type = "m.room.member";
|
||||||
|
if (!opts.skey) {
|
||||||
|
opts.skey = opts.user;
|
||||||
|
}
|
||||||
|
if (!opts.mship) {
|
||||||
|
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
||||||
|
}
|
||||||
|
opts.content = {
|
||||||
|
membership: opts.mship
|
||||||
|
};
|
||||||
|
if (opts.name) { opts.content.displayname = opts.name; }
|
||||||
|
if (opts.url) { opts.content.avatar_url = opts.url; }
|
||||||
|
return module.exports.mkEvent(opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.message event.
|
||||||
|
* @param {Object} opts Values for the message
|
||||||
|
* @param {string} opts.room The room ID for the event.
|
||||||
|
* @param {string} opts.user The user ID for the event.
|
||||||
|
* @param {string} opts.msg Optional. The content.body for the event.
|
||||||
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
module.exports.mkMessage = function(opts) {
|
||||||
|
opts.type = "m.room.message";
|
||||||
|
if (!opts.msg) {
|
||||||
|
opts.msg = "Random->" + Math.random();
|
||||||
|
}
|
||||||
|
if (!opts.room || !opts.user) {
|
||||||
|
throw new Error("Missing .room or .user from", opts);
|
||||||
|
}
|
||||||
|
opts.content = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: opts.msg
|
||||||
|
};
|
||||||
|
return module.exports.mkEvent(opts);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* make the test fail, with the given exception
|
* make the test fail, with the given exception
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue