diff --git a/src/vector/index.js b/src/vector/index.js index 4c9dd9ee0..a29502f83 100644 --- a/src/vector/index.js +++ b/src/vector/index.js @@ -45,7 +45,7 @@ var UpdateChecker = require("./updater"); var q = require('q'); var request = require('browser-request'); -var qs = require("querystring"); +import {parseQs, parseQsFromFragment} from './url_utils'; var lastLocationHashSet = null; @@ -81,41 +81,12 @@ var validBrowser = checkBrowserFeatures([ "objectfit" ]); -// We want to support some name / value pairs in the fragment -// so we're re-using query string like format -// -// returns {location, params} -function parseQsFromFragment(location) { - // if we have a fragment, it will start with '#', which we need to drop. - // (if we don't, this will return ''). - var fragment = location.hash.substring(1); - - // our fragment may contain a query-param-like section. we need to fish - // this out *before* URI-decoding because the params may contain ? and & - // characters which are only URI-encoded once. - var hashparts = fragment.split('?'); - - var result = { - location: decodeURIComponent(hashparts[0]), - params: {} - }; - - if (hashparts.length > 1) { - result.params = qs.parse(hashparts[1]); - } - return result; -} - -function parseQs(location) { - return qs.parse(location.search.substring(1)); -} - // Here, we do some crude URL analysis to allow // deep-linking. function routeUrl(location) { if (!window.matrixChat) return; - console.log("Routing URL "+window.location); + console.log("Routing URL "+location); var params = parseQs(location); var loginToken = params.loginToken; if (loginToken) { diff --git a/src/vector/url_utils.js b/src/vector/url_utils.js new file mode 100644 index 000000000..69354b5d0 --- /dev/null +++ b/src/vector/url_utils.js @@ -0,0 +1,30 @@ +import qs from 'querystring'; + +// We want to support some name / value pairs in the fragment +// so we're re-using query string like format +// +// returns {location, params} +export function parseQsFromFragment(location) { + // if we have a fragment, it will start with '#', which we need to drop. + // (if we don't, this will return ''). + var fragment = location.hash.substring(1); + + // our fragment may contain a query-param-like section. we need to fish + // this out *before* URI-decoding because the params may contain ? and & + // characters which are only URI-encoded once. + var hashparts = fragment.split('?'); + + var result = { + location: decodeURIComponent(hashparts[0]), + params: {} + }; + + if (hashparts.length > 1) { + result.params = qs.parse(hashparts[1]); + } + return result; +} + +export function parseQs(location) { + return qs.parse(location.search.substring(1)); +} diff --git a/test/app-tests/loading.js b/test/app-tests/loading.js new file mode 100644 index 000000000..ad840cff4 --- /dev/null +++ b/test/app-tests/loading.js @@ -0,0 +1,249 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +/* loading.js: test the myriad paths we have for loading the application */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils'; +import expect from 'expect'; +import q from 'q'; + +import jssdk from 'matrix-js-sdk'; + +import sdk from 'matrix-react-sdk'; +import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg'; + +import test_utils from '../test-utils'; +import MockHttpBackend from '../mock-request'; +import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils'; + + +describe('loading:', function () { + let parentDiv; + let httpBackend; + + // an Object simulating the window.location + let windowLocation; + + beforeEach(function() { + test_utils.beforeEach(this); + httpBackend = new MockHttpBackend(); + jssdk.request(httpBackend.requestFn); + parentDiv = document.createElement('div'); + + // uncomment this to actually add the div to the UI, to help with + // debugging (but slow things down) + // document.body.appendChild(parentDiv); + + windowLocation = null; + }); + + afterEach(function() { + if (parentDiv) { + ReactDOM.unmountComponentAtNode(parentDiv); + parentDiv.remove(); + parentDiv = null; + } + }); + + /* simulate the load process done by index.js + * + * TODO: it would be nice to factor some of this stuff out of index.js so + * that we can test it rather than our own implementation of it. + */ + function loadApp(uriFragment) { + windowLocation = { + search: "", + hash: uriFragment, + toString: function() { return this.search + this.hash; }, + }; + + let lastLoadedScreen = null; + let appLoaded = false; + function onNewScreen(screen) { + console.log("newscreen "+screen); + if (!appLoaded) { + lastLoadedScreen = screen; + } else { + var hash = '#/' + screen; + windowLocation.hash = hash; + console.log("browser URI now "+ windowLocation); + } + } + + const MatrixChat = sdk.getComponent('structures.MatrixChat'); + const fragParts = parseQsFromFragment(windowLocation); + const matrixChat = ReactDOM.render( + , parentDiv + ); + + function routeUrl(location, matrixChat) { + console.log("Routing URL "+location); + var params = parseQs(location); + var loginToken = params.loginToken; + if (loginToken) { + matrixChat.showScreen('token_login', params); + return; + } + + var fragparts = parseQsFromFragment(location); + matrixChat.showScreen(fragparts.location.substring(1), + fragparts.params); + } + + // pause for a cycle, then simulate the window.onload handler + q.delay(0).then(() => { + console.log("simulating window.onload"); + routeUrl(windowLocation, matrixChat); + appLoaded = true; + if (lastLoadedScreen) { + onNewScreen(lastLoadedScreen); + lastLoadedScreen = null; + } + }).done(); + + return matrixChat; + } + + describe("Clean load with no stored credentials:", function() { + it('gives a login panel by default', function (done) { + let matrixChat = loadApp(""); + + q.delay(1).then(() => { + // at this point, we're trying to do a guest registration; + // we expect a spinner + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('elements.Spinner')); + + httpBackend.when('POST', '/register').check(function(req) { + expect(req.queryParams.kind).toEqual('guest'); + }).respond(403, "Guest access is disabled"); + + return httpBackend.flush(); + }).then(() => { + // we expect a single component + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('structures.login.Login')); + expect(windowLocation.hash).toEqual(""); + }).done(done, done); + }); + + it('should follow the original link after successful login', function(done) { + let matrixChat = loadApp("#/room/!room:id"); + + q.delay(1).then(() => { + // at this point, we're trying to do a guest registration; + // we expect a spinner + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('elements.Spinner')); + + httpBackend.when('POST', '/register').check(function(req) { + expect(req.queryParams.kind).toEqual('guest'); + }).respond(403, "Guest access is disabled"); + + return httpBackend.flush(); + }).then(() => { + // we expect a single component + let login = ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('structures.login.Login')); + httpBackend.when('POST', '/login').check(function(req) { + expect(req.data.type).toEqual('m.login.password'); + expect(req.data.user).toEqual('user'); + expect(req.data.password).toEqual('pass'); + }).respond(200, { user_id: 'user_id' }); + login.onPasswordLogin("user", "pass") + return httpBackend.flush(); + }).then(() => { + // we expect a spinner + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('elements.Spinner')); + + httpBackend.when('GET', '/pushrules').respond(200, {}); + httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' }); + httpBackend.when('GET', '/sync').respond(200, {}); + return httpBackend.flush(); + }).then(() => { + // once the sync completes, we should have a room view + httpBackend.verifyNoOutstandingExpectation(); + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('structures.RoomView')); + expect(windowLocation.hash).toEqual("#/room/!room:id"); + }).done(done, done); + }); + }); + + describe("MatrixClient rehydrated from stored credentials:", function() { + beforeEach(function() { + // start with a logged-in client + MatrixClientPeg.replaceUsingCreds({ + homeserverUrl: 'http://localhost', + identityServerUrl: 'http://localhost', + userId: '@me:localhost', + accessToken: 'access_token', + guest: false, + }); + }); + + it('shows a directory by default if we have no joined rooms', function(done) { + httpBackend.when('GET', '/pushrules').respond(200, {}); + httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' }); + httpBackend.when('GET', '/sync').respond(200, {}); + + let matrixChat = loadApp(""); + + q.delay(1).then(() => { + // we expect a spinner + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('elements.Spinner')); + return httpBackend.flush(); + }).then(() => { + // once the sync completes, we should have a directory + httpBackend.verifyNoOutstandingExpectation(); + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('structures.RoomDirectory')); + expect(windowLocation.hash).toEqual("#/directory"); + }).done(done, done); + }); + + it('shows a room view if we followed a room link', function(done) { + httpBackend.when('GET', '/pushrules').respond(200, {}); + httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' }); + httpBackend.when('GET', '/sync').respond(200, {}); + + let matrixChat = loadApp("#/room/!room:id"); + + q.delay(1).then(() => { + // we expect a spinner + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('elements.Spinner')); + return httpBackend.flush(); + }).then(() => { + // once the sync completes, we should have a room view + httpBackend.verifyNoOutstandingExpectation(); + ReactTestUtils.findRenderedComponentWithType( + matrixChat, sdk.getComponent('structures.RoomView')); + expect(windowLocation.hash).toEqual("#/room/!room:id"); + }).done(done, done); + + }); + }); +});