682 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			682 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2016 OpenMarket Ltd
 | 
						|
Copyright 2020 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.
 | 
						|
*/
 | 
						|
 | 
						|
/* loading.js: test the myriad paths we have for loading the application */
 | 
						|
 | 
						|
import "fake-indexeddb/auto";
 | 
						|
import React from "react";
 | 
						|
import { render, screen, fireEvent, waitFor, RenderResult, waitForElementToBeRemoved } from "@testing-library/react";
 | 
						|
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
 | 
						|
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
 | 
						|
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
 | 
						|
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
 | 
						|
import MockHttpBackend from "matrix-mock-request";
 | 
						|
import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";
 | 
						|
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
 | 
						|
import { QueryDict, sleep } from "matrix-js-sdk/src/utils";
 | 
						|
import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
 | 
						|
import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads";
 | 
						|
 | 
						|
import "../jest-mocks";
 | 
						|
import WebPlatform from "../../src/vector/platform/WebPlatform";
 | 
						|
import { parseQs, parseQsFromFragment } from "../../src/vector/url_utils";
 | 
						|
import { cleanLocalstorage, deleteIndexedDB, waitForLoadingSpinner, waitForWelcomeComponent } from "../test-utils";
 | 
						|
 | 
						|
const DEFAULT_HS_URL = "http://my_server";
 | 
						|
const DEFAULT_IS_URL = "http://my_is";
 | 
						|
 | 
						|
describe("loading:", function () {
 | 
						|
    let httpBackend: MockHttpBackend;
 | 
						|
 | 
						|
    // an Object simulating the window.location
 | 
						|
    let windowLocation: Location | undefined;
 | 
						|
 | 
						|
    // the mounted MatrixChat
 | 
						|
    let matrixChat: RenderResult | undefined;
 | 
						|
 | 
						|
    // a promise which resolves when the MatrixChat calls onTokenLoginCompleted
 | 
						|
    let tokenLoginCompletePromise: Promise<void> | undefined;
 | 
						|
 | 
						|
    beforeEach(function () {
 | 
						|
        httpBackend = new MockHttpBackend();
 | 
						|
        // @ts-ignore
 | 
						|
        window.fetch = httpBackend.fetchFn;
 | 
						|
 | 
						|
        windowLocation = undefined;
 | 
						|
        matrixChat = undefined;
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(async function () {
 | 
						|
        console.log(`${Date.now()}: loading: afterEach`);
 | 
						|
        matrixChat?.unmount();
 | 
						|
        // unmounting should have cleared the MatrixClientPeg
 | 
						|
        expect(MatrixClientPeg.get()).toBe(null);
 | 
						|
 | 
						|
        // clear the indexeddbs so we can start from a clean slate next time.
 | 
						|
        await Promise.all([deleteIndexedDB("matrix-js-sdk:crypto"), deleteIndexedDB("matrix-js-sdk:riot-web-sync")]);
 | 
						|
        cleanLocalstorage();
 | 
						|
        console.log(`${Date.now()}: loading: afterEach complete`);
 | 
						|
    });
 | 
						|
 | 
						|
    /* 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(
 | 
						|
        opts: {
 | 
						|
            queryString?: string;
 | 
						|
            uriFragment?: string;
 | 
						|
            config?: IConfigOptions;
 | 
						|
        } = {},
 | 
						|
    ): void {
 | 
						|
        const queryString = opts.queryString || "";
 | 
						|
        const uriFragment = opts.uriFragment || "";
 | 
						|
 | 
						|
        windowLocation = {
 | 
						|
            search: queryString,
 | 
						|
            hash: uriFragment,
 | 
						|
            toString: function (): string {
 | 
						|
                return this.search + this.hash;
 | 
						|
            },
 | 
						|
        } as Location;
 | 
						|
 | 
						|
        function onNewScreen(screen: string): void {
 | 
						|
            console.log(Date.now() + " newscreen " + screen);
 | 
						|
            const hash = "#/" + screen;
 | 
						|
            windowLocation!.hash = hash;
 | 
						|
            console.log(Date.now() + " browser URI now " + windowLocation);
 | 
						|
        }
 | 
						|
 | 
						|
        // Parse the given window.location and return parameters that can be used when calling
 | 
						|
        // MatrixChat.showScreen(screen, params)
 | 
						|
        function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } {
 | 
						|
            const fragparts = parseQsFromFragment(location);
 | 
						|
            return {
 | 
						|
                screen: fragparts.location.substring(1),
 | 
						|
                params: fragparts.params,
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        const fragParts = parseQsFromFragment(windowLocation);
 | 
						|
 | 
						|
        const config = {
 | 
						|
            default_hs_url: DEFAULT_HS_URL,
 | 
						|
            default_is_url: DEFAULT_IS_URL,
 | 
						|
            validated_server_config: {
 | 
						|
                hsUrl: DEFAULT_HS_URL,
 | 
						|
                hsName: "TEST_ENVIRONMENT",
 | 
						|
                hsNameIsDifferent: false, // yes, we lie
 | 
						|
                isUrl: DEFAULT_IS_URL,
 | 
						|
            } as ValidatedServerConfig,
 | 
						|
            embedded_pages: {
 | 
						|
                home_url: "data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==",
 | 
						|
            },
 | 
						|
            ...(opts.config ?? {}),
 | 
						|
        } as IConfigOptions;
 | 
						|
 | 
						|
        PlatformPeg.set(new WebPlatform());
 | 
						|
 | 
						|
        const params = parseQs(windowLocation);
 | 
						|
 | 
						|
        tokenLoginCompletePromise = new Promise<void>((resolve) => {
 | 
						|
            matrixChat = render(
 | 
						|
                <MatrixChat
 | 
						|
                    onNewScreen={onNewScreen}
 | 
						|
                    config={config!}
 | 
						|
                    realQueryParams={params}
 | 
						|
                    startingFragmentQueryParams={fragParts.params}
 | 
						|
                    enableGuest={true}
 | 
						|
                    onTokenLoginCompleted={resolve}
 | 
						|
                    initialScreenAfterLogin={getScreenFromLocation(windowLocation!)}
 | 
						|
                />,
 | 
						|
            );
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    // set an expectation that we will get a call to /sync, then flush
 | 
						|
    // http requests until we do.
 | 
						|
    //
 | 
						|
    // returns a promise resolving to the received request
 | 
						|
    async function expectAndAwaitSync(opts?: { isGuest?: boolean }): Promise<any> {
 | 
						|
        let syncRequest: (typeof MockHttpBackend.prototype.requests)[number] | null = null;
 | 
						|
        httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
 | 
						|
            versions: ["v1.1"],
 | 
						|
            unstable_features: {},
 | 
						|
        });
 | 
						|
        const isGuest = opts?.isGuest;
 | 
						|
        if (!isGuest) {
 | 
						|
            // the call to create the LL filter
 | 
						|
            httpBackend.when("POST", "/filter").respond(200, { filter_id: "llfid" });
 | 
						|
            httpBackend.when("GET", "/pushrules").respond(200, {});
 | 
						|
        }
 | 
						|
        httpBackend
 | 
						|
            .when("GET", "/sync")
 | 
						|
            .check((r) => {
 | 
						|
                syncRequest = r;
 | 
						|
            })
 | 
						|
            .respond(200, {});
 | 
						|
 | 
						|
        for (let attempts = 10; attempts > 0; attempts--) {
 | 
						|
            console.log(Date.now() + " waiting for /sync");
 | 
						|
            if (syncRequest) {
 | 
						|
                return syncRequest;
 | 
						|
            }
 | 
						|
            await httpBackend.flush(undefined);
 | 
						|
        }
 | 
						|
        throw new Error("Gave up waiting for /sync");
 | 
						|
    }
 | 
						|
 | 
						|
    describe("Clean load with no stored credentials:", function () {
 | 
						|
        it("gives a welcome page by default", function () {
 | 
						|
            loadApp();
 | 
						|
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // at this point, we're trying to do a guest registration;
 | 
						|
                    // we expect a spinner
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/register")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                        })
 | 
						|
                        .respond(403, "Guest access is disabled");
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // Wait for another trip around the event loop for the UI to update
 | 
						|
                    return waitForWelcomeComponent(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return waitFor(() => expect(windowLocation?.hash).toEqual("#/welcome"));
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("should follow the original link after successful login", function () {
 | 
						|
            loadApp({
 | 
						|
                uriFragment: "#/room/!room:id",
 | 
						|
            });
 | 
						|
 | 
						|
            // Pass the liveliness checks
 | 
						|
            httpBackend.when("GET", "/versions").respond(200, { versions: ["v1.1"] });
 | 
						|
            httpBackend.when("GET", "/_matrix/identity/v2").respond(200, {});
 | 
						|
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // at this point, we're trying to do a guest registration;
 | 
						|
                    // we expect a spinner
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/register")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                        })
 | 
						|
                        .respond(403, "Guest access is disabled");
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // Wait for another trip around the event loop for the UI to update
 | 
						|
                    return sleep(10);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return moveFromWelcomeToLogin(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return completeLogin(matrixChat!);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a room view
 | 
						|
                    return awaitRoomView(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/room/!room:id");
 | 
						|
 | 
						|
                    // and the localstorage should have been updated
 | 
						|
                    expect(localStorage.getItem("mx_user_id")).toEqual("@user:id");
 | 
						|
                    expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
 | 
						|
                    expect(localStorage.getItem("mx_hs_url")).toEqual(DEFAULT_HS_URL);
 | 
						|
                    expect(localStorage.getItem("mx_is_url")).toEqual(DEFAULT_IS_URL);
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it.skip("should not register as a guest when using a #/login link", function () {
 | 
						|
            loadApp({
 | 
						|
                uriFragment: "#/login",
 | 
						|
            });
 | 
						|
 | 
						|
            // Pass the liveliness checks
 | 
						|
            httpBackend.when("GET", "/versions").respond(200, { versions: ["v1.1"] });
 | 
						|
            httpBackend.when("GET", "/_matrix/identity/v2").respond(200, {});
 | 
						|
 | 
						|
            return awaitLoginComponent(matrixChat)
 | 
						|
                .then(async () => {
 | 
						|
                    await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading..."));
 | 
						|
                    // we expect a single <Login> component
 | 
						|
                    await screen.findByRole("main");
 | 
						|
                    screen.getAllByText("Sign in");
 | 
						|
 | 
						|
                    // the only outstanding request should be a GET /login
 | 
						|
                    // (in particular there should be no /register request for
 | 
						|
                    // guest registration).
 | 
						|
                    const allowedRequests = ["/_matrix/client/v3/login", "/versions", "/_matrix/identity/v2"];
 | 
						|
                    for (const req of httpBackend.requests) {
 | 
						|
                        if (req.method === "GET" && allowedRequests.find((p) => req.path.endsWith(p))) {
 | 
						|
                            continue;
 | 
						|
                        }
 | 
						|
 | 
						|
                        throw new Error(`Unexpected HTTP request to ${req}`);
 | 
						|
                    }
 | 
						|
                    return completeLogin(matrixChat!);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    expect(matrixChat?.container.querySelector(".mx_HomePage")).toBeTruthy();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/home");
 | 
						|
                });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("MatrixClient rehydrated from stored credentials:", function () {
 | 
						|
        beforeEach(async function () {
 | 
						|
            localStorage.setItem("mx_hs_url", "http://localhost");
 | 
						|
            localStorage.setItem("mx_is_url", "http://localhost");
 | 
						|
            localStorage.setItem("mx_access_token", "access_token");
 | 
						|
            localStorage.setItem("mx_user_id", "@me:localhost");
 | 
						|
            localStorage.setItem("mx_last_room_id", "!last_room:id");
 | 
						|
 | 
						|
            // Create a crypto store as well to satisfy storage consistency checks
 | 
						|
            const cryptoStore = new IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto");
 | 
						|
            await cryptoStore.startup();
 | 
						|
        });
 | 
						|
 | 
						|
        it("shows the last known room by default", function () {
 | 
						|
            loadApp();
 | 
						|
 | 
						|
            return awaitLoggedIn(matrixChat!)
 | 
						|
                .then(() => {
 | 
						|
                    // we are logged in - let the sync complete
 | 
						|
                    return expectAndAwaitSync();
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a room view
 | 
						|
                    return awaitRoomView(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/room/!last_room:id");
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("shows a home page by default if we have no joined rooms", function () {
 | 
						|
            localStorage.removeItem("mx_last_room_id");
 | 
						|
 | 
						|
            loadApp();
 | 
						|
 | 
						|
            return awaitLoggedIn(matrixChat!)
 | 
						|
                .then(() => {
 | 
						|
                    // we are logged in - let the sync complete
 | 
						|
                    return expectAndAwaitSync();
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a home page
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(matrixChat?.container.querySelector(".mx_HomePage")).toBeTruthy();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/home");
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("shows a room view if we followed a room link", function () {
 | 
						|
            loadApp({
 | 
						|
                uriFragment: "#/room/!room:id",
 | 
						|
            });
 | 
						|
 | 
						|
            return awaitLoggedIn(matrixChat!)
 | 
						|
                .then(() => {
 | 
						|
                    // we are logged in - let the sync complete
 | 
						|
                    return expectAndAwaitSync();
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a room view
 | 
						|
                    return awaitRoomView(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/room/!room:id");
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        describe("/#/login link:", function () {
 | 
						|
            beforeEach(function () {
 | 
						|
                loadApp({
 | 
						|
                    uriFragment: "#/login",
 | 
						|
                });
 | 
						|
 | 
						|
                // give the UI a chance to display
 | 
						|
                return expectAndAwaitSync();
 | 
						|
            });
 | 
						|
 | 
						|
            it("does not show a login view", async function () {
 | 
						|
                await awaitRoomView(matrixChat);
 | 
						|
 | 
						|
                await screen.findByLabelText("Spaces");
 | 
						|
                expect(screen.queryAllByText("Sign in")).toHaveLength(0);
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("Guest auto-registration:", function () {
 | 
						|
        it("shows a welcome page by default", function () {
 | 
						|
            loadApp();
 | 
						|
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // at this point, we're trying to do a guest registration;
 | 
						|
                    // we expect a spinner
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/register")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                        })
 | 
						|
                        .respond(200, {
 | 
						|
                            user_id: "@guest:localhost",
 | 
						|
                            access_token: "secret_token",
 | 
						|
                        });
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return awaitLoggedIn(matrixChat!);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // we are logged in - let the sync complete
 | 
						|
                    return expectAndAwaitSync({ isGuest: true });
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a welcome page
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(matrixChat?.container.querySelector(".mx_Welcome")).toBeTruthy();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/welcome");
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("uses the default homeserver to register with", function () {
 | 
						|
            loadApp();
 | 
						|
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // at this point, we're trying to do a guest registration;
 | 
						|
                    // we expect a spinner
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/register")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
 | 
						|
                            expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                        })
 | 
						|
                        .respond(200, {
 | 
						|
                            user_id: "@guest:localhost",
 | 
						|
                            access_token: "secret_token",
 | 
						|
                        });
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return awaitLoggedIn(matrixChat!);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return expectAndAwaitSync({ isGuest: true });
 | 
						|
                })
 | 
						|
                .then((req) => {
 | 
						|
                    expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
 | 
						|
 | 
						|
                    // once the sync completes, we should have a welcome page
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(matrixChat?.container.querySelector(".mx_Welcome")).toBeTruthy();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/welcome");
 | 
						|
                    expect(MatrixClientPeg.safeGet().baseUrl).toEqual(DEFAULT_HS_URL);
 | 
						|
                    expect(MatrixClientPeg.safeGet().idBaseUrl).toEqual(DEFAULT_IS_URL);
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("shows a room view if we followed a room link", function () {
 | 
						|
            loadApp({
 | 
						|
                uriFragment: "#/room/!room:id",
 | 
						|
            });
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // at this point, we're trying to do a guest registration;
 | 
						|
                    // we expect a spinner
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/register")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                        })
 | 
						|
                        .respond(200, {
 | 
						|
                            user_id: "@guest:localhost",
 | 
						|
                            access_token: "secret_token",
 | 
						|
                        });
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return awaitLoggedIn(matrixChat!);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return expectAndAwaitSync({ isGuest: true });
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // once the sync completes, we should have a room view
 | 
						|
                    return awaitRoomView(matrixChat);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    httpBackend.verifyNoOutstandingExpectation();
 | 
						|
                    expect(windowLocation?.hash).toEqual("#/room/!room:id");
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        describe("Login as user", function () {
 | 
						|
            beforeEach(function () {
 | 
						|
                // first we have to load the homepage
 | 
						|
                loadApp();
 | 
						|
 | 
						|
                httpBackend
 | 
						|
                    .when("POST", "/register")
 | 
						|
                    .check(function (req) {
 | 
						|
                        expect(req.queryParams?.kind).toEqual("guest");
 | 
						|
                    })
 | 
						|
                    .respond(200, {
 | 
						|
                        user_id: "@guest:localhost",
 | 
						|
                        access_token: "secret_token",
 | 
						|
                    });
 | 
						|
 | 
						|
                return httpBackend
 | 
						|
                    .flush(undefined)
 | 
						|
                    .then(() => {
 | 
						|
                        return awaitLoggedIn(matrixChat!);
 | 
						|
                    })
 | 
						|
                    .then(() => {
 | 
						|
                        // we got a sync spinner - let the sync complete
 | 
						|
                        return expectAndAwaitSync();
 | 
						|
                    })
 | 
						|
                    .then(async () => {
 | 
						|
                        // once the sync completes, we should have a home page
 | 
						|
                        await waitFor(() => matrixChat?.container.querySelector(".mx_HomePage"));
 | 
						|
 | 
						|
                        // we simulate a click on the 'login' button by firing off
 | 
						|
                        // the relevant dispatch.
 | 
						|
                        //
 | 
						|
                        // XXX: is it an anti-pattern to access the react-sdk's
 | 
						|
                        // dispatcher in this way? Is it better to find the login
 | 
						|
                        // button and simulate a click? (we might have to arrange
 | 
						|
                        // for it to be shown - it's not always, due to the
 | 
						|
                        // collapsing left panel
 | 
						|
 | 
						|
                        dis.dispatch({ action: "start_login" });
 | 
						|
 | 
						|
                        return awaitLoginComponent(matrixChat);
 | 
						|
                    });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should give us a login page", async function () {
 | 
						|
                // we expect a single <Login> component
 | 
						|
                await screen.findByRole("main");
 | 
						|
                screen.getAllByText("Sign in");
 | 
						|
 | 
						|
                expect(windowLocation?.hash).toEqual("#/login");
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("Token login:", function () {
 | 
						|
        it("logs in successfully", function () {
 | 
						|
            localStorage.setItem("mx_sso_hs_url", "https://homeserver");
 | 
						|
            localStorage.setItem("mx_sso_is_url", "https://idserver");
 | 
						|
            loadApp({
 | 
						|
                queryString: "?loginToken=secretToken",
 | 
						|
            });
 | 
						|
 | 
						|
            return sleep(1)
 | 
						|
                .then(async () => {
 | 
						|
                    // we expect a spinner while we're logging in
 | 
						|
                    await waitForLoadingSpinner();
 | 
						|
 | 
						|
                    httpBackend
 | 
						|
                        .when("POST", "/login")
 | 
						|
                        .check(function (req) {
 | 
						|
                            expect(req.path).toMatch(new RegExp("^https://homeserver/"));
 | 
						|
                            expect(req.data.type).toEqual("m.login.token");
 | 
						|
                            expect(req.data.token).toEqual("secretToken");
 | 
						|
                        })
 | 
						|
                        .respond(200, {
 | 
						|
                            user_id: "@user:localhost",
 | 
						|
                            device_id: "DEVICE_ID",
 | 
						|
                            access_token: "access_token",
 | 
						|
                        });
 | 
						|
 | 
						|
                    return httpBackend.flush(undefined);
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // at this point, MatrixChat should fire onTokenLoginCompleted, which
 | 
						|
                    // makes index.js reload the app. We're not going to attempt to
 | 
						|
                    // simulate the reload - just check that things are left in the
 | 
						|
                    // right state for the reloaded app.
 | 
						|
 | 
						|
                    return tokenLoginCompletePromise;
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    return expectAndAwaitSync().catch((e) => {
 | 
						|
                        throw new Error("Never got /sync after login: did the client start?");
 | 
						|
                    });
 | 
						|
                })
 | 
						|
                .then(() => {
 | 
						|
                    // check that the localstorage has been set up in such a way that
 | 
						|
                    // the reloaded app can pick up where we leave off.
 | 
						|
                    expect(localStorage.getItem("mx_user_id")).toEqual("@user:localhost");
 | 
						|
                    expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
 | 
						|
                    expect(localStorage.getItem("mx_hs_url")).toEqual("https://homeserver");
 | 
						|
                    expect(localStorage.getItem("mx_is_url")).toEqual("https://idserver");
 | 
						|
                });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    // check that we have a Login component, send a 'user:pass' login,
 | 
						|
    // and await the HTTP requests.
 | 
						|
    async function completeLogin(matrixChat: RenderResult): Promise<void> {
 | 
						|
        // When we switch to the login component, it'll hit the login endpoint
 | 
						|
        // for proof of life and to get flows. We'll only give it one option.
 | 
						|
        httpBackend.when("GET", "/login").respond(200, { flows: [{ type: "m.login.password" }] });
 | 
						|
        httpBackend.flush(undefined); // We already would have tried the GET /login request
 | 
						|
 | 
						|
        httpBackend
 | 
						|
            .when("POST", "/login")
 | 
						|
            .check(function (req) {
 | 
						|
                expect(req.data.type).toEqual("m.login.password");
 | 
						|
                expect(req.data.identifier.type).toEqual("m.id.user");
 | 
						|
                expect(req.data.identifier.user).toEqual("user");
 | 
						|
                expect(req.data.password).toEqual("pass");
 | 
						|
            })
 | 
						|
            .respond(200, {
 | 
						|
                user_id: "@user:id",
 | 
						|
                device_id: "DEVICE_ID",
 | 
						|
                access_token: "access_token",
 | 
						|
            });
 | 
						|
 | 
						|
        // Give the component some time to finish processing the login flows before continuing.
 | 
						|
        await waitFor(() => expect(matrixChat?.container.querySelector("#mx_LoginForm_username")).toBeTruthy());
 | 
						|
 | 
						|
        // Enter login details
 | 
						|
        fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_username")!, { target: { value: "user" } });
 | 
						|
        fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_password")!, { target: { value: "pass" } });
 | 
						|
        fireEvent.click(screen.getByText("Sign in", { selector: ".mx_Login_submit" }));
 | 
						|
 | 
						|
        return httpBackend
 | 
						|
            .flush(undefined)
 | 
						|
            .then(() => {
 | 
						|
                // Wait for another trip around the event loop for the UI to update
 | 
						|
                return sleep(1);
 | 
						|
            })
 | 
						|
            .then(() => {
 | 
						|
                return expectAndAwaitSync().catch((e) => {
 | 
						|
                    throw new Error("Never got /sync after login: did the client start?");
 | 
						|
                });
 | 
						|
            })
 | 
						|
            .then(() => {
 | 
						|
                httpBackend.verifyNoOutstandingExpectation();
 | 
						|
            });
 | 
						|
    }
 | 
						|
});
 | 
						|
 | 
						|
async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> {
 | 
						|
    if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in
 | 
						|
 | 
						|
    return new Promise((resolve) => {
 | 
						|
        const onAction = ({ action }: ActionPayload): void => {
 | 
						|
            if (action !== "on_logged_in") {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
            console.log(Date.now() + ": Received on_logged_in action");
 | 
						|
            dis.unregister(dispatcherRef);
 | 
						|
            resolve(sleep(1));
 | 
						|
        };
 | 
						|
        const dispatcherRef = dis.register(onAction);
 | 
						|
        console.log(Date.now() + ": Waiting for on_logged_in action");
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
async function awaitRoomView(matrixChat?: RenderResult): Promise<void> {
 | 
						|
    await waitFor(() => matrixChat?.container.querySelector(".mx_RoomView"));
 | 
						|
}
 | 
						|
 | 
						|
async function awaitLoginComponent(matrixChat?: RenderResult): Promise<void> {
 | 
						|
    await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage"));
 | 
						|
}
 | 
						|
 | 
						|
function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise<void> {
 | 
						|
    dis.dispatch({ action: "start_login" });
 | 
						|
    return awaitLoginComponent(matrixChat);
 | 
						|
}
 |