diff --git a/package-lock.json b/package-lock.json
index 6dd02674be..23ffa68bef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3719,9 +3719,9 @@
"integrity": "sha1-uKnFSTISqTkvAiK2SclhFJfr+4g="
},
"hoek": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
- "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
+ "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"home-or-tmp": {
"version": "2.0.0",
diff --git a/package.json b/package.json
index 77338b4874..1f979369e3 100644
--- a/package.json
+++ b/package.json
@@ -125,8 +125,9 @@
"karma-spec-reporter": "^0.0.31",
"karma-summary-reporter": "^1.3.3",
"karma-webpack": "^1.7.0",
+ "matrix-mock-request": "^1.2.1",
"matrix-react-test-utils": "^0.1.1",
- "mocha": "^2.4.5",
+ "mocha": "^5.0.5",
"parallelshell": "^3.0.2",
"react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1",
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index a22a5aeebd..7378e982ef 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -65,14 +65,14 @@ import sdk from './index';
* Resolves to `true` if we ended up starting a session, or `false` if we
* failed.
*/
-export function loadSession(opts) {
- let enableGuest = opts.enableGuest || false;
- const guestHsUrl = opts.guestHsUrl;
- const guestIsUrl = opts.guestIsUrl;
- const fragmentQueryParams = opts.fragmentQueryParams || {};
- const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
+export async function loadSession(opts) {
+ try {
+ let enableGuest = opts.enableGuest || false;
+ const guestHsUrl = opts.guestHsUrl;
+ const guestIsUrl = opts.guestIsUrl;
+ const fragmentQueryParams = opts.fragmentQueryParams || {};
+ const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
- return Promise.resolve().then(() => {
if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false;
@@ -91,8 +91,7 @@ export function loadSession(opts) {
guest: true,
}, true).then(() => true);
}
- return _restoreFromLocalStorage();
- }).then((success) => {
+ const success = await _restoreFromLocalStorage();
if (success) {
return true;
}
@@ -103,9 +102,9 @@ export function loadSession(opts) {
// fall back to login screen
return false;
- }).catch((e) => {
+ } catch (e) {
return _handleLoadSessionFailure(e);
- });
+ }
}
/**
@@ -199,40 +198,39 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. teamToken, isGuest etc.)
-function _restoreFromLocalStorage() {
- return Promise.resolve().then(() => {
- if (!localStorage) {
- return Promise.resolve(false);
- }
- const hsUrl = localStorage.getItem("mx_hs_url");
- const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
- const accessToken = localStorage.getItem("mx_access_token");
- const userId = localStorage.getItem("mx_user_id");
- const deviceId = localStorage.getItem("mx_device_id");
+async function _restoreFromLocalStorage() {
+ if (!localStorage) {
+ return false;
+ }
+ const hsUrl = localStorage.getItem("mx_hs_url");
+ const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
+ const accessToken = localStorage.getItem("mx_access_token");
+ const userId = localStorage.getItem("mx_user_id");
+ const deviceId = localStorage.getItem("mx_device_id");
- let isGuest;
- if (localStorage.getItem("mx_is_guest") !== null) {
- isGuest = localStorage.getItem("mx_is_guest") === "true";
- } else {
- // legacy key name
- isGuest = localStorage.getItem("matrix-is-guest") === "true";
- }
+ let isGuest;
+ if (localStorage.getItem("mx_is_guest") !== null) {
+ isGuest = localStorage.getItem("mx_is_guest") === "true";
+ } else {
+ // legacy key name
+ isGuest = localStorage.getItem("matrix-is-guest") === "true";
+ }
- if (accessToken && userId && hsUrl) {
- console.log(`Restoring session for ${userId}`);
- return _doSetLoggedIn({
- userId: userId,
- deviceId: deviceId,
- accessToken: accessToken,
- homeserverUrl: hsUrl,
- identityServerUrl: isUrl,
- guest: isGuest,
- }, false).then(() => true);
- } else {
- console.log("No previous session found.");
- return Promise.resolve(false);
- }
- });
+ if (accessToken && userId && hsUrl) {
+ console.log(`Restoring session for ${userId}`);
+ await _doSetLoggedIn({
+ userId: userId,
+ deviceId: deviceId,
+ accessToken: accessToken,
+ homeserverUrl: hsUrl,
+ identityServerUrl: isUrl,
+ guest: isGuest,
+ }, false);
+ return true;
+ } else {
+ console.log("No previous session found.");
+ return false;
+ }
}
function _handleLoadSessionFailure(e) {
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index cdeb99ef53..8df46d2f7c 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -360,7 +360,7 @@ export default React.createClass({
// Note we don't catch errors from this: we catch everything within
// loadSession as there's logic there to ask the user if they want
// to try logging out.
- }).done();
+ });
},
componentWillUnmount: function() {
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index fb116cdffe..2f2d6fe8cb 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -37,9 +37,9 @@ export default React.createClass({
propTypes: {
// onFinished callback to call when Escape is pressed
- // Take a boolean which is true is the dialog was dismissed
+ // Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
- // cancelled (from BaseDialog, this is always false).
+ // cancelled (BaseDialog itself only calls this with false).
onFinished: PropTypes.func.isRequired,
// Whether the dialog should have a 'close' button that will
diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js
new file mode 100644
index 0000000000..71df26da46
--- /dev/null
+++ b/test/components/structures/GroupView-test.js
@@ -0,0 +1,378 @@
+/*
+Copyright 2018 New Vector 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.
+*/
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactTestUtils from 'react-dom/test-utils';
+import expect from 'expect';
+import Promise from 'bluebird';
+
+import MockHttpBackend from 'matrix-mock-request';
+import MatrixClientPeg from '../../../src/MatrixClientPeg';
+import sdk from 'matrix-react-sdk';
+import Matrix from 'matrix-js-sdk';
+
+import * as TestUtils from 'test-utils';
+
+const GroupView = sdk.getComponent('structures.GroupView');
+const WrappedGroupView = TestUtils.wrapInMatrixClientContext(GroupView);
+
+const Spinner = sdk.getComponent('elements.Spinner');
+
+/**
+ * Call fn before calling componentDidUpdate on a react component instance, inst.
+ * @param {React.Component} inst an instance of a React component.
+ * @returns {Promise} promise that resolves when componentDidUpdate is called on
+ * given component instance.
+ */
+function waitForUpdate(inst) {
+ return new Promise((resolve, reject) => {
+ const cdu = inst.componentDidUpdate;
+
+ inst.componentDidUpdate = (prevProps, prevState, snapshot) => {
+ resolve();
+
+ if (cdu) cdu(prevProps, prevState, snapshot);
+
+ inst.componentDidUpdate = cdu;
+ };
+ });
+}
+
+describe('GroupView', function() {
+ let root;
+ let rootElement;
+ let httpBackend;
+ let summaryResponse;
+ let summaryResponseWithComplicatedLongDesc;
+ let summaryResponseWithNoLongDesc;
+ let summaryResponseWithBadImg;
+ let groupId;
+ let groupIdEncoded;
+
+ // Summary response fields
+ const user = {
+ is_privileged: true, // can edit the group
+ is_public: true, // appear as a member to non-members
+ is_publicised: true, // display flair
+ };
+ const usersSection = {
+ roles: {},
+ total_user_count_estimate: 0,
+ users: [],
+ };
+ const roomsSection = {
+ categories: {},
+ rooms: [],
+ total_room_count_estimate: 0,
+ };
+
+ beforeEach(function() {
+ TestUtils.beforeEach(this);
+
+ httpBackend = new MockHttpBackend();
+
+ Matrix.request(httpBackend.requestFn);
+
+ MatrixClientPeg.get = () => Matrix.createClient({
+ baseUrl: 'https://my.home.server',
+ userId: '@me:here',
+ accessToken: '123456789',
+ });
+
+ summaryResponse = {
+ profile: {
+ avatar_url: "mxc://someavatarurl",
+ is_openly_joinable: true,
+ is_public: true,
+ long_description: "This is a LONG description.",
+ name: "The name of a community",
+ short_description: "This is a community",
+ },
+ user,
+ users_section: usersSection,
+ rooms_section: roomsSection,
+ };
+ summaryResponseWithNoLongDesc = {
+ profile: {
+ avatar_url: "mxc://someavatarurl",
+ is_openly_joinable: true,
+ is_public: true,
+ long_description: null,
+ name: "The name of a community",
+ short_description: "This is a community",
+ },
+ user,
+ users_section: usersSection,
+ rooms_section: roomsSection,
+ };
+ summaryResponseWithComplicatedLongDesc = {
+ profile: {
+ avatar_url: "mxc://someavatarurl",
+ is_openly_joinable: true,
+ is_public: true,
+ long_description: `
+
This is a more complicated group page
+With paragraphs
+
+ - And lists!
+ - With list items.
+
+And also images: 
`,
+ name: "The name of a community",
+ short_description: "This is a community",
+ },
+ user,
+ users_section: usersSection,
+ rooms_section: roomsSection,
+ };
+
+ summaryResponseWithBadImg = {
+ profile: {
+ avatar_url: "mxc://someavatarurl",
+ is_openly_joinable: true,
+ is_public: true,
+ long_description: 'Evil image: 
',
+ name: "The name of a community",
+ short_description: "This is a community",
+ },
+ user,
+ users_section: usersSection,
+ rooms_section: roomsSection,
+ };
+
+ groupId = "+" + Math.random().toString(16).slice(2) + ':domain';
+ groupIdEncoded = encodeURIComponent(groupId);
+
+ rootElement = document.createElement('div');
+ root = ReactDOM.render(, rootElement);
+ });
+
+ afterEach(function() {
+ ReactDOM.unmountComponentAtNode(rootElement);
+ });
+
+ it('should show a spinner when first displayed', function() {
+ ReactTestUtils.findRenderedComponentWithType(root, Spinner);
+
+ // If we don't respond here, the rate limiting done to ensure a maximum of
+ // 3 concurrent network requests for GroupStore will block subsequent requests
+ // in other tests.
+ //
+ // This is a good case for doing the rate limiting somewhere other than the module
+ // scope of GroupStore.js
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ return httpBackend.flush(undefined, undefined, 0);
+ });
+
+ it('should indicate failure after failed /summary', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error');
+ });
+
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(500, {});
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a group avatar, name, id and short description after successful /summary', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
+
+ const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar'));
+ const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img');
+ const avatarImgElement = ReactDOM.findDOMNode(img);
+ expect(avatarImgElement).toExist();
+ expect(avatarImgElement.src).toInclude(
+ 'https://my.home.server/_matrix/media/v1/thumbnail/' +
+ 'someavatarurl?width=48&height=48&method=crop',
+ );
+
+ const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name');
+ const nameElement = ReactDOM.findDOMNode(name);
+ expect(nameElement).toExist();
+ expect(nameElement.innerText).toInclude('The name of a community');
+ expect(nameElement.innerText).toInclude(groupId);
+
+ const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc');
+ const shortDescElement = ReactDOM.findDOMNode(shortDesc);
+ expect(shortDescElement).toExist();
+ expect(shortDescElement.innerText).toBe('This is a community');
+ });
+
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a simple long description after successful /summary', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView');
+
+ const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
+ const longDescElement = ReactDOM.findDOMNode(longDesc);
+ expect(longDescElement).toExist();
+ expect(longDescElement.innerText).toBe('This is a LONG description.');
+ expect(longDescElement.innerHTML).toBe('This is a LONG description.
');
+ });
+
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a placeholder if a long description is not set', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ const placeholder = ReactTestUtils
+ .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder');
+ const placeholderElement = ReactDOM.findDOMNode(placeholder);
+ expect(placeholderElement).toExist();
+ });
+
+ httpBackend
+ .when('GET', '/groups/' + groupIdEncoded + '/summary')
+ .respond(200, summaryResponseWithNoLongDesc);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a complicated long description after successful /summary', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
+ const longDescElement = ReactDOM.findDOMNode(longDesc);
+ expect(longDescElement).toExist();
+
+ expect(longDescElement.innerHTML).toInclude('This is a more complicated group page
');
+ expect(longDescElement.innerHTML).toInclude('With paragraphs
');
+ expect(longDescElement.innerHTML).toInclude('');
+ expect(longDescElement.innerHTML).toInclude('- And lists!
');
+
+ const imgSrc = "https://my.home.server/_matrix/media/v1/thumbnail/someimageurl?width=800&height=600";
+ expect(longDescElement.innerHTML).toInclude('
');
+ });
+
+ httpBackend
+ .when('GET', '/groups/' + groupIdEncoded + '/summary')
+ .respond(200, summaryResponseWithComplicatedLongDesc);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should disallow images with non-mxc URLs', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc');
+ const longDescElement = ReactDOM.findDOMNode(longDesc);
+ expect(longDescElement).toExist();
+
+ // If this fails, the URL could be in an img `src`, which is what we care about but
+ // there's no harm in keeping this simple and checking the entire HTML string.
+ expect(longDescElement.innerHTML).toExclude('evilimageurl');
+ });
+
+ httpBackend
+ .when('GET', '/groups/' + groupIdEncoded + '/summary')
+ .respond(200, summaryResponseWithBadImg);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
+ const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
+ expect(roomDetailListElement).toExist();
+ });
+
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+
+ it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() {
+ const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView);
+ const prom = waitForUpdate(groupView).then(() => {
+ const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList');
+ const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList);
+ expect(roomDetailListElement).toExist();
+
+ const roomDetailListRoomName = ReactTestUtils.findRenderedDOMComponentWithClass(
+ root,
+ 'mx_RoomDirectory_name',
+ );
+ const roomDetailListRoomNameElement = ReactDOM.findDOMNode(roomDetailListRoomName);
+
+ expect(roomDetailListRoomNameElement).toExist();
+ expect(roomDetailListRoomNameElement.innerText).toEqual('Some room name');
+ });
+
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse);
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] });
+ httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [{
+ avatar_url: "mxc://someroomavatarurl",
+ canonical_alias: "#somealias:domain",
+ guest_can_join: true,
+ is_public: true,
+ name: "Some room name",
+ num_joined_members: 123,
+ room_id: "!someroomid",
+ topic: "some topic",
+ world_readable: true,
+ }] });
+
+ httpBackend.flush(undefined, undefined, 0);
+ return prom;
+ });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index b593761bd4..d2c685b371 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -92,6 +92,7 @@ export function createTestClient() {
content: {},
});
},
+ mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
setAccountData: sinon.stub(),
sendTyping: sinon.stub().returns(Promise.resolve({})),
sendTextMessage: () => Promise.resolve({}),