From afc889ff4d5d21581f9f46e849065e73513efc1e Mon Sep 17 00:00:00 2001
From: Richard van der Hoff <richard@matrix.org>
Date: Wed, 10 Aug 2016 00:15:04 +0100
Subject: [PATCH] Some tests of the application load process

---
 src/vector/index.js       |  33 +----
 src/vector/url_utils.js   |  30 +++++
 test/app-tests/loading.js | 249 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 281 insertions(+), 31 deletions(-)
 create mode 100644 src/vector/url_utils.js
 create mode 100644 test/app-tests/loading.js

diff --git a/src/vector/index.js b/src/vector/index.js
index 4c9dd9ee..a29502f8 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 00000000..69354b5d
--- /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 00000000..ad840cff
--- /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(
+            <MatrixChat
+                onNewScreen={onNewScreen}
+                config={{}}
+                startingQueryParams={fragParts.params}
+                enableGuest={true}
+            />, 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 <Login> 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 <Login> 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);
+
+        });
+    });
+});