diff --git a/karma.conf.js b/karma.conf.js
index 7a8831635b..e59a6c9e15 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -1,6 +1,5 @@
// karma.conf.js - the config file for karma, which runs our tests.
-var webpack = require('webpack');
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
* 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:
* - can we run one test at a time?
*/
@@ -26,11 +29,18 @@ module.exports = function (config) {
],
// list of files to exclude
- // (this doesn't work, and I don't know why - we still rerun the tests
- // when lockfiles are created)
- exclude: [
- '**/.#*'
- ],
+ //
+ // This doesn't work. It turns out that it's webpack which does the
+ // 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
// available preprocessors:
@@ -83,13 +93,6 @@ module.exports = function (config) {
module: {
loaders: [
{ 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",
include: [path.resolve('./src'),
@@ -107,6 +110,11 @@ module.exports = function (config) {
// there is no need for webpack to parse them - they can
// just be included as-is.
/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: {
@@ -114,6 +122,10 @@ module.exports = function (config) {
'matrix-react-sdk': path.resolve('src/index.js'),
'sinon': 'sinon/pkg/sinon.js',
},
+ root: [
+ path.resolve('./src'),
+ path.resolve('./test'),
+ ],
},
devtool: 'inline-source-map',
},
diff --git a/package.json b/package.json
index 8041d4154d..7c4bd714ef 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,6 @@
"babel-preset-react": "^6.5.0",
"babel-runtime": "^6.6.1",
"expect": "^1.16.0",
- "imports-loader": "^0.6.5",
"json-loader": "^0.5.3",
"karma": "^0.13.22",
"karma-chrome-launcher": "^0.2.3",
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
new file mode 100644
index 0000000000..37b79a8f0a
--- /dev/null
+++ b/test/components/structures/MessagePanel-test.js
@@ -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(
+
+ );
+
+ // 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(
+
+ );
+
+ var tiles = TestUtils.scryRenderedComponentsWithType(
+ res, sdk.getComponent('rooms.EventTile'));
+
+ // find the
which wraps the read marker
+ var rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
+
+ // it should follow the 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(
+ , parentDiv);
+
+ var tiles = TestUtils.scryRenderedComponentsWithType(
+ mp, sdk.getComponent('rooms.EventTile'));
+
+ // find the 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(
+ , 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);
+ });
+});
diff --git a/test/components/stub-component.js b/test/components/stub-component.js
index 8882a0b35d..ba37850ec6 100644
--- a/test/components/stub-component.js
+++ b/test/components/stub-component.js
@@ -4,8 +4,17 @@
var React = require('react');
-module.exports = React.createClass({
- render: function() {
- return ;
- },
-});
+module.exports = function(opts) {
+ opts = opts || {};
+ if (!opts.displayName) {
+ opts.displayName = 'StubComponent';
+ }
+
+ if (!opts.render) {
+ opts.render = function() {
+ return {this.displayName}
;
+ }
+ }
+
+ return React.createClass(opts);
+};
diff --git a/test/mock-clock.js b/test/mock-clock.js
new file mode 100644
index 0000000000..a99b01a0fb
--- /dev/null
+++ b/test/mock-clock.js
@@ -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;
+}
+
+
diff --git a/test/test-component-index.js b/test/test-component-index.js
index 9b5c07af54..0ab5f693dd 100644
--- a/test/test-component-index.js
+++ b/test/test-component-index.js
@@ -5,14 +5,17 @@
* provide
*/
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.RightPanel'] = stub;
-components['structures.RoomDirectory'] = stub;
-components['views.globals.MatrixToolbar'] = stub;
-components['views.globals.GuestWarningBar'] = stub;
-components['views.globals.NewVersionBar'] = stub;
-components['views.elements.Spinner'] = stub;
+components['structures.LeftPanel'] = stubComponent();
+components['structures.RightPanel'] = stubComponent();
+components['structures.RoomDirectory'] = stubComponent();
+components['views.globals.MatrixToolbar'] = stubComponent();
+components['views.globals.GuestWarningBar'] = stubComponent();
+components['views.globals.NewVersionBar'] = stubComponent();
+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;
diff --git a/test/test-utils.js b/test/test-utils.js
index 7c0f38693a..956ced0554 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -2,8 +2,10 @@
var peg = require('../src/MatrixClientPeg.js');
var jssdk = require('matrix-js-sdk');
+var MatrixEvent = jssdk.MatrixEvent;
var sinon = require('sinon');
+
/**
* Stub out the MatrixClient, and configure the MatrixClientPeg object to
* 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
*