From 88624c8548723b1bb494711d725af98a94ba8d6b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 12 Apr 2018 17:16:18 +0100 Subject: [PATCH 1/3] Upgrade to mocha 5.05 primarily for use of `describe.only` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77338b4874..de0179c870 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "karma-summary-reporter": "^1.3.3", "karma-webpack": "^1.7.0", "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", From a1c442422438866fda9371c2128c5dcca63fd5ef Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Apr 2018 14:28:24 +0100 Subject: [PATCH 2/3] Add tests for GroupView --- package.json | 1 + test/components/structures/GroupView-test.js | 378 +++++++++++++++++++ test/test-utils.js | 1 + 3 files changed, 380 insertions(+) create mode 100644 test/components/structures/GroupView-test.js diff --git a/package.json b/package.json index de0179c870..1f979369e3 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "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": "^5.0.5", "parallelshell": "^3.0.2", diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js new file mode 100644 index 0000000000..2df0599f89 --- /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.only('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('
    '); + 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({}), From 7915d97ed70cf5cab3f0b83d807db228aa978f10 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 27 Apr 2018 14:56:48 +0100 Subject: [PATCH 3/3] Also run other tests --- test/components/structures/GroupView-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 2df0599f89..71df26da46 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -52,7 +52,7 @@ function waitForUpdate(inst) { }); } -describe.only('GroupView', function() { +describe('GroupView', function() { let root; let rootElement; let httpBackend;