diff --git a/.eslintrc.js b/.eslintrc.js index d5684e21a7..92280344fa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,7 +53,11 @@ module.exports = { * things that are errors in the js-sdk config that the current * code does not adhere to, turned down to warn */ - "max-len": ["warn"], + "max-len": ["warn", { + // apparently people believe the length limit shouldn't apply + // to JSX. + ignorePattern: '^\\s*<', + }], "valid-jsdoc": ["warn"], "new-cap": ["warn"], "key-spacing": ["warn"], diff --git a/karma.conf.js b/karma.conf.js index 131a03ce79..6d3047bb3b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -165,6 +165,14 @@ module.exports = function (config) { }, devtool: 'inline-source-map', }, + + webpackMiddleware: { + stats: { + // don't fill the console up with a mahoosive list of modules + chunks: false, + }, + }, + browserNoActivityTimeout: 15000, }); }; diff --git a/src/RtsClient.js b/src/RtsClient.js new file mode 100644 index 0000000000..ae62fb8b22 --- /dev/null +++ b/src/RtsClient.js @@ -0,0 +1,80 @@ +import 'whatwg-fetch'; + +function checkStatus(response) { + if (!response.ok) { + return response.text().then((text) => { + throw new Error(text); + }); + } + return response; +} + +function parseJson(response) { + return response.json(); +} + +function encodeQueryParams(params) { + return '?' + Object.keys(params).map((k) => { + return k + '=' + encodeURIComponent(params[k]); + }).join('&'); +} + +const request = (url, opts) => { + if (opts && opts.qs) { + url += encodeQueryParams(opts.qs); + delete opts.qs; + } + if (opts && opts.body) { + if (!opts.headers) { + opts.headers = {}; + } + opts.body = JSON.stringify(opts.body); + opts.headers['Content-Type'] = 'application/json'; + } + return fetch(url, opts) + .then(checkStatus) + .then(parseJson); +}; + + +export default class RtsClient { + constructor(url) { + this._url = url; + } + + getTeamsConfig() { + return request(this._url + '/teams'); + } + + /** + * Track a referral with the Riot Team Server. This should be called once a referred + * user has been successfully registered. + * @param {string} referrer the user ID of one who referred the user to Riot. + * @param {string} userId the user ID of the user being referred. + * @param {string} userEmail the email address linked to `userId`. + * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon + * success. + */ + trackReferral(referrer, userId, userEmail) { + return request(this._url + '/register', + { + body: { + referrer: referrer, + user_id: userId, + user_email: userEmail, + }, + method: 'POST', + } + ); + } + + getTeam(teamToken) { + return request(this._url + '/teamConfiguration', + { + qs: { + team_token: teamToken, + }, + } + ); + } +} diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 816b8eb73d..56b9d56cc9 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -71,7 +71,7 @@ export default React.createClass({ return this.props.matrixClient.exportRoomKeys(); }).then((k) => { return MegolmExportEncryption.encryptMegolmKeyFile( - JSON.stringify(k), passphrase + JSON.stringify(k), passphrase, ); }).then((f) => { const blob = new Blob([f], { @@ -95,9 +95,14 @@ export default React.createClass({ }); }, + _onCancelClick: function(ev) { + ev.preventDefault(); + this.props.onFinished(false); + return false; + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const disableForm = (this.state.phase === PHASE_EXPORTING); @@ -159,10 +164,9 @@ export default React.createClass({ - + diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 586bd9b6cc..ddd13813e2 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -80,7 +80,7 @@ export default React.createClass({ return readFileAsArrayBuffer(file).then((arrayBuffer) => { return MegolmExportEncryption.decryptMegolmKeyFile( - arrayBuffer, passphrase + arrayBuffer, passphrase, ); }).then((keys) => { return this.props.matrixClient.importRoomKeys(JSON.parse(keys)); @@ -98,9 +98,14 @@ export default React.createClass({ }); }, + _onCancelClick: function(ev) { + ev.preventDefault(); + this.props.onFinished(false); + return false; + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const disableForm = (this.state.phase !== PHASE_EDIT); @@ -158,10 +163,9 @@ export default React.createClass({ - + diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c00bd2c6db..44beb787c8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -171,6 +171,7 @@ export default React.createClass({ brand={this.props.config.brand} collapsedRhs={this.props.collapse_rhs} enableLabs={this.props.config.enableLabs} + referralBaseUrl={this.props.config.referralBaseUrl} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cb61041d48..6a84fb940f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1055,12 +1055,13 @@ module.exports = React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} + referrer={this.props.startingFragmentQueryParams.referrer} username={this.state.upgradeUsername} guestAccessToken={this.state.guestAccessToken} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - teamsConfig={this.props.config.teamsConfig} + teamServerConfig={this.props.config.teamServerConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 24c8ff53c0..38b3346e29 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1332,12 +1332,14 @@ module.exports = React.createClass({ }, onStatusBarVisible: function() { + if (this.unmounted) return; this.setState({ statusBarVisible: true, }); }, onStatusBarHidden: function() { + if (this.unmounted) return; this.setState({ statusBarVisible: false, }); @@ -1507,13 +1509,14 @@ module.exports = React.createClass({ }); var statusBar; + let isStatusAreaExpanded = true; if (ContentMessages.getCurrentUploads().length > 0) { var UploadBar = sdk.getComponent('structures.UploadBar'); statusBar = ; } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); - + isStatusAreaExpanded = this.state.statusBarVisible; statusBar = +

Referral

+
+ Refer a friend to Riot: {href} +
+ + ); + }, + _renderUserInterfaceSettings: function() { var client = MatrixClientPeg.get(); @@ -819,6 +843,8 @@ module.exports = React.createClass({ {accountJsx} + {this._renderReferral()} + {notification_area} {this._renderUserInterfaceSettings()} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 90140b3280..0fc0cac527 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig"); var MatrixClientPeg = require("../../../MatrixClientPeg"); var RegistrationForm = require("../../views/login/RegistrationForm"); var CaptchaForm = require("../../views/login/CaptchaForm"); +var RtsClient = require("../../../RtsClient"); var MIN_PASSWORD_LENGTH = 6; @@ -47,23 +48,16 @@ module.exports = React.createClass({ defaultIsUrl: React.PropTypes.string, brand: React.PropTypes.string, email: React.PropTypes.string, + referrer: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, - teamsConfig: React.PropTypes.shape({ + teamServerConfig: React.PropTypes.shape({ // Email address to request new teams - supportEmail: React.PropTypes.string, - teams: React.PropTypes.arrayOf(React.PropTypes.shape({ - // The displayed name of the team - "name": React.PropTypes.string, - // The suffix with which every team email address ends - "emailSuffix": React.PropTypes.string, - // The rooms to use during auto-join - "rooms": React.PropTypes.arrayOf(React.PropTypes.shape({ - "id": React.PropTypes.string, - "autoJoin": React.PropTypes.bool, - })), - })).required, + supportEmail: React.PropTypes.string.isRequired, + // URL of the riot-team-server to get team configurations and track referrals + teamServerURL: React.PropTypes.string.isRequired, }), + teamSelected: React.PropTypes.object, defaultDeviceDisplayName: React.PropTypes.string, @@ -75,6 +69,7 @@ module.exports = React.createClass({ getInitialState: function() { return { busy: false, + teamServerBusy: false, errorText: null, // We remember the values entered by the user because // the registration form will be unmounted during the @@ -90,6 +85,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this._unmounted = false; this.dispatcherRef = dis.register(this.onAction); // attach this to the instance rather than this.state since it isn't UI this.registerLogic = new Signup.Register( @@ -103,10 +99,40 @@ module.exports = React.createClass({ this.registerLogic.setIdSid(this.props.idSid); this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); this.registerLogic.recheckState(); + + if ( + this.props.teamServerConfig && + this.props.teamServerConfig.teamServerURL && + !this._rtsClient + ) { + this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); + + this.setState({ + teamServerBusy: true, + }); + // GET team configurations including domains, names and icons + this._rtsClient.getTeamsConfig().then((data) => { + const teamsConfig = { + teams: data, + supportEmail: this.props.teamServerConfig.supportEmail, + }; + console.log('Setting teams config to ', teamsConfig); + this.setState({ + teamsConfig: teamsConfig, + teamServerBusy: false, + }); + }, (err) => { + console.error('Error retrieving config for teams', err); + this.setState({ + teamServerBusy: false, + }); + }); + } }, componentWillUnmount: function() { dis.unregister(this.dispatcherRef); + this._unmounted = true; }, componentDidMount: function() { @@ -184,24 +210,41 @@ module.exports = React.createClass({ accessToken: response.access_token }); - // Auto-join rooms - if (self.props.teamsConfig && self.props.teamsConfig.teams) { - for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { - let team = self.props.teamsConfig.teams[i]; - if (self.state.formVals.email.endsWith(team.emailSuffix)) { - console.log("User successfully registered with team " + team.name); + if ( + self._rtsClient && + self.props.referrer && + self.state.teamSelected + ) { + // Track referral, get team_token in order to retrieve team config + self._rtsClient.trackReferral( + self.props.referrer, + response.user_id, + self.state.formVals.email + ).then((data) => { + const teamToken = data.team_token; + // Store for use /w welcome pages + window.localStorage.setItem('mx_team_token', teamToken); + + self._rtsClient.getTeam(teamToken).then((team) => { + console.log( + `User successfully registered with team ${team.name}` + ); if (!team.rooms) { - break; + return; } + // Auto-join rooms team.rooms.forEach((room) => { - if (room.autoJoin) { - console.log("Auto-joining " + room.id); - MatrixClientPeg.get().joinRoom(room.id); + if (room.auto_join && room.room_id) { + console.log(`Auto-joining ${room.room_id}`); + MatrixClientPeg.get().joinRoom(room.room_id); } }); - break; - } - } + }, (err) => { + console.error('Error getting team config', err); + }); + }, (err) => { + console.error('Error tracking referral', err); + }); } if (self.props.brand) { @@ -273,7 +316,15 @@ module.exports = React.createClass({ }); }, + onTeamSelected: function(teamSelected) { + if (!this._unmounted) { + this.setState({ teamSelected }); + } + }, + _getRegisterContentJsx: function() { + const Spinner = sdk.getComponent("elements.Spinner"); + var currStep = this.registerLogic.getStep(); var registerStep; switch (currStep) { @@ -283,17 +334,23 @@ module.exports = React.createClass({ case "Register.STEP_m.login.dummy": // NB. Our 'username' prop is specifically for upgrading // a guest account + if (this.state.teamServerBusy) { + registerStep = ; + break; + } registerStep = ( + onRegisterClick={this.onFormSubmit} + onTeamSelected={this.onTeamSelected} + /> ); break; case "Register.STEP_m.login.email.identity": @@ -322,7 +379,6 @@ module.exports = React.createClass({ } var busySpinner; if (this.state.busy) { - var Spinner = sdk.getComponent("elements.Spinner"); busySpinner = ( ); @@ -367,7 +423,7 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 61503196e5..ca3b07aa00 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); -var classNames = require('classnames'); -var sdk = require("../../../index"); -var Invite = require("../../../Invite"); -var createRoom = require("../../../createRoom"); -var MatrixClientPeg = require("../../../MatrixClientPeg"); -var DMRoomMap = require('../../../utils/DMRoomMap'); -var rate_limited_func = require("../../../ratelimitedfunc"); -var dis = require("../../../dispatcher"); -var Modal = require('../../../Modal'); +import React from 'react'; +import classNames from 'classnames'; +import sdk from '../../../index'; +import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; +import createRoom from '../../../createRoom'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import rate_limited_func from '../../../ratelimitedfunc'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; +import q from 'q'; const TRUNCATE_QUERY_LIST = 40; @@ -186,13 +187,17 @@ module.exports = React.createClass({ // If the query isn't a user we know about, but is a // valid address, add an entry for that if (queryList.length == 0) { - const addrType = Invite.getAddressType(query); + const addrType = getAddressType(query); if (addrType !== null) { - queryList.push({ + queryList[0] = { addressType: addrType, address: query, isKnown: false, - }); + }; + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (addrType == 'email') { + this._lookupThreepid(addrType, query).done(); + } } } } @@ -212,6 +217,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }; }, @@ -229,6 +235,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }, _getDirectMessageRoom: function(addr) { @@ -266,7 +273,7 @@ module.exports = React.createClass({ if (this.props.roomId) { // Invite new user to a room var self = this; - Invite.inviteMultipleToRoom(this.props.roomId, addrTexts) + inviteMultipleToRoom(this.props.roomId, addrTexts) .then(function(addrs) { var room = MatrixClientPeg.get().getRoom(self.props.roomId); return self._showAnyInviteErrors(addrs, room); @@ -300,7 +307,7 @@ module.exports = React.createClass({ var room; createRoom().then(function(roomId) { room = MatrixClientPeg.get().getRoom(roomId); - return Invite.inviteMultipleToRoom(roomId, addrTexts); + return inviteMultipleToRoom(roomId, addrTexts); }) .then(function(addrs) { return self._showAnyInviteErrors(addrs, room); @@ -380,7 +387,7 @@ module.exports = React.createClass({ }, _isDmChat: function(addrs) { - if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) { + if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) { return true; } else { return false; @@ -408,7 +415,7 @@ module.exports = React.createClass({ _addInputToList: function() { const addressText = this.refs.textinput.value.trim(); - const addrType = Invite.getAddressType(addressText); + const addrType = getAddressType(addressText); const addrObj = { addressType: addrType, address: addressText, @@ -432,9 +439,45 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); return inviteList; }, + _lookupThreepid: function(medium, address) { + let cancelled = false; + // Note that we can't safely remove this after we're done + // because we don't know that it's the same one, so we just + // leave it: it's replacing the old one each time so it's + // not like they leak. + this._cancelThreepidLookup = function() { + cancelled = true; + } + + // wait a bit to let the user finish typing + return q.delay(500).then(() => { + if (cancelled) return null; + return MatrixClientPeg.get().lookupThreePid(medium, address); + }).then((res) => { + if (res === null || !res.mxid) return null; + if (cancelled) return null; + + return MatrixClientPeg.get().getProfileInfo(res.mxid); + }).then((res) => { + if (res === null) return null; + if (cancelled) return null; + this.setState({ + queryList: [{ + // an InviteAddressType + addressType: medium, + address: address, + displayName: res.displayname, + avatarMxc: res.avatar_url, + isKnown: true, + }] + }); + }); + }, + render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const AddressSelector = sdk.getComponent("elements.AddressSelector"); diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 01c1ed3255..18492d8ae6 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -94,14 +94,14 @@ export default React.createClass({ const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const nameClasses = classNames({ + "mx_AddressTile_name": true, + "mx_AddressTile_justified": this.props.justified, + }); + let info; let error = false; if (address.addressType === "mx" && address.isKnown) { - const nameClasses = classNames({ - "mx_AddressTile_name": true, - "mx_AddressTile_justified": this.props.justified, - }); - const idClasses = classNames({ "mx_AddressTile_id": true, "mx_AddressTile_justified": this.props.justified, @@ -123,13 +123,21 @@ export default React.createClass({
{ this.props.address.address }
); } else if (address.addressType === "email") { - var emailClasses = classNames({ + const emailClasses = classNames({ "mx_AddressTile_email": true, "mx_AddressTile_justified": this.props.justified, }); + let nameNode = null; + if (address.displayName) { + nameNode =
{ address.displayName }
+ } + info = ( -
{ address.address }
+
+
{ address.address }
+ {nameNode} +
); } else { error = true; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index f8a0863f70..1cb8253812 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -44,8 +44,8 @@ module.exports = React.createClass({ teams: React.PropTypes.arrayOf(React.PropTypes.shape({ // The displayed name of the team "name": React.PropTypes.string, - // The suffix with which every team email address ends - "emailSuffix": React.PropTypes.string, + // The domain of team email addresses + "domain": React.PropTypes.string, })).required, }), @@ -117,9 +117,6 @@ module.exports = React.createClass({ _doSubmit: function() { let email = this.refs.email.value.trim(); - if (this.state.selectedTeam) { - email += "@" + this.state.selectedTeam.emailSuffix; - } var promise = this.props.onRegisterClick({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), @@ -134,25 +131,6 @@ module.exports = React.createClass({ } }, - onSelectTeam: function(teamIndex) { - let team = this._getSelectedTeam(teamIndex); - if (team) { - this.refs.email.value = this.refs.email.value.split("@")[0]; - } - this.setState({ - selectedTeam: team, - showSupportEmail: teamIndex === "other", - }); - }, - - _getSelectedTeam: function(teamIndex) { - if (this.props.teamsConfig && - this.props.teamsConfig.teams[teamIndex]) { - return this.props.teamsConfig.teams[teamIndex]; - } - return null; - }, - /** * Returns true if all fields were valid last time * they were validated. @@ -167,20 +145,36 @@ module.exports = React.createClass({ return true; }, + _isUniEmail: function(email) { + return email.endsWith('.ac.uk') || email.endsWith('.edu'); + }, + validateField: function(field_id) { var pwd1 = this.refs.password.value.trim(); var pwd2 = this.refs.passwordConfirm.value.trim(); switch (field_id) { case FIELD_EMAIL: - let email = this.refs.email.value; - if (this.props.teamsConfig) { - let team = this.state.selectedTeam; - if (team) { - email = email + "@" + team.emailSuffix; - } + const email = this.refs.email.value; + if (this.props.teamsConfig && this._isUniEmail(email)) { + const matchingTeam = this.props.teamsConfig.teams.find( + (team) => { + return email.split('@').pop() === team.domain; + } + ) || null; + this.setState({ + selectedTeam: matchingTeam, + showSupportEmail: !matchingTeam, + }); + this.props.onTeamSelected(matchingTeam); + } else { + this.props.onTeamSelected(null); + this.setState({ + selectedTeam: null, + showSupportEmail: false, + }); } - let valid = email === '' || Email.looksValid(email); + const valid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_USERNAME: @@ -260,61 +254,35 @@ module.exports = React.createClass({ return cls; }, - _renderEmailInputSuffix: function() { - let suffix = null; - if (!this.state.selectedTeam) { - return suffix; - } - let team = this.state.selectedTeam; - if (team) { - suffix = "@" + team.emailSuffix; - } - return suffix; - }, - render: function() { var self = this; - var emailSection, teamSection, teamAdditionSupport, registerButton; + var emailSection, belowEmailSection, registerButton; if (this.props.showEmail) { - let emailSuffix = this._renderEmailInputSuffix(); emailSection = ( -
- - {emailSuffix ? : null } -
+ ); if (this.props.teamsConfig) { - teamSection = ( - - ); if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { - teamAdditionSupport = ( - - If your team is not listed, email  + belowEmailSection = ( +

+ Sorry, but your university is not registered with us just yet.  + Email us on  {this.props.teamsConfig.supportEmail} - - +   + to get your university signed up. Or continue to register with Riot to enjoy our open source platform. +

+ ); + } else if (this.state.selectedTeam) { + belowEmailSection = ( +

+ You are registering with {this.state.selectedTeam.name} +

); } } @@ -333,11 +301,8 @@ module.exports = React.createClass({ return (
- {teamSection} - {teamAdditionSupport} -
{emailSection} -
+ {belowEmailSection} { const [aes_key, hmac_key] = keys; diff --git a/test/components/structures/RoomView-test.js b/test/components/structures/RoomView-test.js index 58db29b1ee..8e7c8160b8 100644 --- a/test/components/structures/RoomView-test.js +++ b/test/components/structures/RoomView-test.js @@ -42,17 +42,12 @@ describe('RoomView', function () { it('resolves a room alias to a room id', function (done) { peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); - var onRoomIdResolved = sinon.spy(); + function onRoomIdResolved(room_id) { + expect(room_id).toEqual("!randomcharacters:aser.ver"); + done(); + } ReactDOM.render(, parentDiv); - - process.nextTick(function() { - // These expect()s don't read very well and don't give very good failure - // messages, but expect's toHaveBeenCalled only takes an expect spy object, - // not a sinon spy object. - expect(onRoomIdResolved.called).toExist(); - done(); - }); }); it('joins by alias if given an alias', function (done) { diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js index 13721c9ecd..eacaeb5fb4 100644 --- a/test/components/structures/ScrollPanel-test.js +++ b/test/components/structures/ScrollPanel-test.js @@ -73,6 +73,7 @@ var Tester = React.createClass({ /* returns a promise which will resolve when the fill happens */ awaitFill: function(dir) { + console.log("ScrollPanel Tester: awaiting " + dir + " fill"); var defer = q.defer(); this._fillDefers[dir] = defer; return defer.promise; @@ -80,7 +81,7 @@ var Tester = React.createClass({ _onScroll: function(ev) { var st = ev.target.scrollTop; - console.log("Scroll event; scrollTop: " + st); + console.log("ScrollPanel Tester: scroll event; scrollTop: " + st); this.lastScrollEvent = st; var d = this._scrollDefer; @@ -159,10 +160,29 @@ describe('ScrollPanel', function() { scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass( tester, "gm-scroll-view"); - // wait for a browser tick to let the initial paginates complete - setTimeout(function() { - done(); - }, 0); + // 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 q().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() { diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index b2cdfbd590..be60691b5c 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -99,7 +99,11 @@ describe('TimelinePanel', function() { // the document so that we can interact with it properly. parentDiv = document.createElement('div'); parentDiv.style.width = '800px'; - parentDiv.style.height = '600px'; + + // 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); }); @@ -235,7 +239,7 @@ describe('TimelinePanel', function() { expect(client.paginateEventTimeline.callCount).toEqual(0); done(); }, 0); - }, 0); + }, 10); }); it("should let you scroll down to the bottom after you've scrolled up", function(done) { diff --git a/test/test-utils.js b/test/test-utils.js index cdfae4421c..71d3bd92d6 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -14,7 +14,15 @@ var MatrixEvent = jssdk.MatrixEvent; */ export function beforeEach(context) { var 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("=")); }; diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index 28752ae529..0c49fd48d1 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() { .toThrow('Trailer line not found'); }); + it('should handle a too-short body', function() { + const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- +AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx +cissyYBxjsfsAn +-----END MEGOLM SESSION DATA----- +`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Invalid file: too short'); + }); + it('should decrypt a range of inputs', function(done) { function next(i) { if (i >= TEST_VECTORS.length) {