"use strict"; import sinon from 'sinon'; import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; import dis from '../src/dispatcher'; import jssdk from 'matrix-js-sdk'; import {makeType} from "../src/utils/TypeUtils"; import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; const MatrixEvent = jssdk.MatrixEvent; /** * Perform common actions before each test case, e.g. printing the test case * name to stdout. * @param {Mocha.Context} context The test context */ export function beforeEach(context) { const desc = context.currentTest.fullTitle(); console.log(); // this puts a mark in the chrome devtools timeline, which can help // figure out what's been going on. if (console.timeStamp) { console.timeStamp(desc); } console.log(desc); console.log(new Array(1 + desc.length).join("=")); } /** * Stub out the MatrixClient, and configure the MatrixClientPeg object to * return it when get() is called. * * TODO: once the components are updated to get their MatrixClients from * the react context, we can get rid of this and just inject a test client * via the context instead. * * @returns {sinon.Sandbox}; remember to call sandbox.restore afterwards. */ export function stubClient() { const sandbox = sinon.sandbox.create(); const client = createTestClient(); // stub out the methods in MatrixClientPeg // // 'sandbox.restore()' doesn't work correctly on inherited methods, // so we do this for each method const methods = ['get', 'unset', 'replaceUsingCreds']; for (let i = 0; i < methods.length; i++) { sandbox.stub(peg, methods[i]); } // MatrixClientPeg.get() is called a /lot/, so implement it with our own // fast stub function rather than a sinon stub peg.get = function() { return client; }; return sandbox; } /** * Create a stubbed-out MatrixClient * * @returns {object} MatrixClient stub */ export function createTestClient() { return { getHomeserverUrl: sinon.stub(), getIdentityServerUrl: sinon.stub(), getDomain: sinon.stub().returns("matrix.rog"), getUserId: sinon.stub().returns("@userId:matrix.rog"), getPushActionsForEvent: sinon.stub(), getRoom: sinon.stub().returns(mkStubRoom()), getRooms: sinon.stub().returns([]), getVisibleRooms: sinon.stub().returns([]), getGroups: sinon.stub().returns([]), loginFlows: sinon.stub(), on: sinon.stub(), removeListener: sinon.stub(), isRoomEncrypted: sinon.stub().returns(false), peekInRoom: sinon.stub().returns(Promise.resolve(mkStubRoom())), paginateEventTimeline: sinon.stub().returns(Promise.resolve()), sendReadReceipt: sinon.stub().returns(Promise.resolve()), getRoomIdForAlias: sinon.stub().returns(Promise.resolve()), getRoomDirectoryVisibility: sinon.stub().returns(Promise.resolve()), getProfileInfo: sinon.stub().returns(Promise.resolve({})), getAccountData: (type) => { return mkEvent({ type, event: true, content: {}, }); }, mxcUrlToHttp: (mxc) => 'http://this.is.a.url/', setAccountData: sinon.stub(), sendTyping: sinon.stub().returns(Promise.resolve({})), sendMessage: () => Promise.resolve({}), getSyncState: () => "SYNCING", generateClientSecret: () => "t35tcl1Ent5ECr3T", isGuest: () => false, }; } /** * 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. */ export function mkEvent(opts) { if (!opts.type || !opts.content) { throw new Error("Missing .type or .content =>" + JSON.stringify(opts)); } const event = { type: opts.type, room_id: opts.room, sender: opts.user, content: opts.content, prev_content: opts.prev_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 */ export function mkPresence(opts) { if (!opts.user) { throw new Error("Missing user"); } const 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.prevMship The prev_content.membership for the event. * @param {string} opts.user The user ID for the event. * @param {RoomMember} opts.target The target of 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 */ export function mkMembership(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.prevMship) { opts.prev_content = { membership: opts.prevMship }; } if (opts.name) { opts.content.displayname = opts.name; } if (opts.url) { opts.content.avatar_url = opts.url; } const e = mkEvent(opts); if (opts.target) { e.target = opts.target; } return e; } /** * 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 */ export function mkMessage(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 mkEvent(opts); } export function mkStubRoom(roomId = null) { const stubTimeline = { getEvents: () => [] }; return { roomId, getReceiptsForEvent: sinon.stub().returns([]), getMember: sinon.stub().returns({ userId: '@member:domain.bla', name: 'Member', roomId: roomId, getAvatarUrl: () => 'mxc://avatar.url/image.png', }), getMembersWithMembership: sinon.stub().returns([]), getJoinedMembers: sinon.stub().returns([]), getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, getUnfilteredTimelineSet: () => null, getAccountData: () => null, hasMembershipState: () => null, getVersion: () => '1', shouldUpgradeToVersion: () => null, getMyMembership: () => "join", currentState: { getStateEvents: sinon.stub(), mayClientSendStateEvent: sinon.stub().returns(true), maySendStateEvent: sinon.stub().returns(true), members: [], }, tags: { "m.favourite": { order: 0.5, }, }, setBlacklistUnverifiedDevices: sinon.stub(), }; } export function mkServerConfig(hsUrl, isUrl) { return makeType(ValidatedServerConfig, { hsUrl, hsName: "TEST_ENVIRONMENT", hsNameIsDifferent: false, // yes, we lie isUrl, }); } export function getDispatchForStore(store) { // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a // dispatcher `_isDispatching` is true. return (payload) => { dis._isDispatching = true; dis._callbacks[store._dispatchToken](payload); dis._isDispatching = false; }; } export function wrapInMatrixClientContext(WrappedComponent) { class Wrapper extends React.Component { static childContextTypes = { matrixClient: PropTypes.object, } getChildContext() { return { matrixClient: this._matrixClient, }; } componentWillMount() { this._matrixClient = peg.get(); } render() { return <WrappedComponent ref={this.props.wrappedRef} {...this.props} />; } } return Wrapper; } /** * Call fn before calling componentDidUpdate on a react component instance, inst. * @param {React.Component} inst an instance of a React component. * @param {integer} updates Number of updates to wait for. (Defaults to 1.) * @returns {Promise} promise that resolves when componentDidUpdate is called on * given component instance. */ export function waitForUpdate(inst, updates = 1) { return new Promise((resolve, reject) => { const cdu = inst.componentDidUpdate; console.log(`Waiting for ${updates} update(s)`); inst.componentDidUpdate = (prevProps, prevState, snapshot) => { updates--; console.log(`Got update, ${updates} remaining`); if (updates == 0) { inst.componentDidUpdate = cdu; resolve(); } if (cdu) cdu(prevProps, prevState, snapshot); }; }); }