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 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('