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/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({}),