Add support for rendering a custom wrapper around Element (#25537)

Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
This commit is contained in:
maheichyk 2023-08-30 09:52:41 +03:00 committed by GitHub
parent 2ad80c8fcf
commit 3fd6b62254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 29 deletions

View File

@ -70,7 +70,7 @@
}, },
"dependencies": { "dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@matrix-org/react-sdk-module-api": "^2.0.0", "@matrix-org/react-sdk-module-api": "^2.1.0",
"gfm.css": "^1.1.2", "gfm.css": "^1.1.2",
"jsrsasign": "^10.5.25", "jsrsasign": "^10.5.25",
"katex": "^0.16.0", "katex": "^0.16.0",

View File

@ -33,6 +33,8 @@ import { createClient } from "matrix-js-sdk/src/matrix";
import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject"; import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject";
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat"; import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";
import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
import { ModuleRunner } from "matrix-react-sdk/src/modules/ModuleRunner";
import { parseQs } from "./url_utils"; import { parseQs } from "./url_utils";
import VectorBasePlatform from "./platform/VectorBasePlatform"; import VectorBasePlatform from "./platform/VectorBasePlatform";
@ -109,17 +111,22 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location); const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location);
const wrapperOpts: WrapperOpts = { Wrapper: React.Fragment };
ModuleRunner.instance.invoke(WrapperLifecycle.Wrapper, wrapperOpts);
return ( return (
<MatrixChat <wrapperOpts.Wrapper>
onNewScreen={onNewScreen} <MatrixChat
config={config} onNewScreen={onNewScreen}
realQueryParams={params} config={config}
startingFragmentQueryParams={fragParams} realQueryParams={params}
enableGuest={!config.disable_guests} startingFragmentQueryParams={fragParams}
onTokenLoginCompleted={onTokenLoginCompleted} enableGuest={!config.disable_guests}
initialScreenAfterLogin={initialScreenAfterLogin} onTokenLoginCompleted={onTokenLoginCompleted}
defaultDeviceDisplayName={defaultDeviceName} initialScreenAfterLogin={initialScreenAfterLogin}
/> defaultDeviceDisplayName={defaultDeviceName}
/>
</wrapperOpts.Wrapper>
); );
} }

View File

@ -34,7 +34,7 @@ import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads";
import "../jest-mocks"; import "../jest-mocks";
import WebPlatform from "../../src/vector/platform/WebPlatform"; import WebPlatform from "../../src/vector/platform/WebPlatform";
import { parseQs, parseQsFromFragment } from "../../src/vector/url_utils"; import { parseQs, parseQsFromFragment } from "../../src/vector/url_utils";
import { cleanLocalstorage, deleteIndexedDB } from "../test-utils"; import { cleanLocalstorage, deleteIndexedDB, waitForLoadingSpinner, waitForWelcomeComponent } from "../test-utils";
const DEFAULT_HS_URL = "http://my_server"; const DEFAULT_HS_URL = "http://my_server";
const DEFAULT_IS_URL = "http://my_is"; const DEFAULT_IS_URL = "http://my_is";
@ -189,7 +189,7 @@ describe("loading:", function () {
.then(async () => { .then(async () => {
// at this point, we're trying to do a guest registration; // at this point, we're trying to do a guest registration;
// we expect a spinner // we expect a spinner
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/register") .when("POST", "/register")
@ -202,7 +202,7 @@ describe("loading:", function () {
}) })
.then(() => { .then(() => {
// Wait for another trip around the event loop for the UI to update // Wait for another trip around the event loop for the UI to update
return awaitWelcomeComponent(matrixChat); return waitForWelcomeComponent(matrixChat);
}) })
.then(() => { .then(() => {
return waitFor(() => expect(windowLocation?.hash).toEqual("#/welcome")); return waitFor(() => expect(windowLocation?.hash).toEqual("#/welcome"));
@ -222,7 +222,7 @@ describe("loading:", function () {
.then(async () => { .then(async () => {
// at this point, we're trying to do a guest registration; // at this point, we're trying to do a guest registration;
// we expect a spinner // we expect a spinner
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/register") .when("POST", "/register")
@ -391,7 +391,7 @@ describe("loading:", function () {
.then(async () => { .then(async () => {
// at this point, we're trying to do a guest registration; // at this point, we're trying to do a guest registration;
// we expect a spinner // we expect a spinner
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/register") .when("POST", "/register")
@ -427,7 +427,7 @@ describe("loading:", function () {
.then(async () => { .then(async () => {
// at this point, we're trying to do a guest registration; // at this point, we're trying to do a guest registration;
// we expect a spinner // we expect a spinner
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/register") .when("POST", "/register")
@ -468,7 +468,7 @@ describe("loading:", function () {
.then(async () => { .then(async () => {
// at this point, we're trying to do a guest registration; // at this point, we're trying to do a guest registration;
// we expect a spinner // we expect a spinner
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/register") .when("POST", "/register")
@ -562,7 +562,7 @@ describe("loading:", function () {
return sleep(1) return sleep(1)
.then(async () => { .then(async () => {
// we expect a spinner while we're logging in // we expect a spinner while we're logging in
await assertAtLoadingSpinner(); await waitForLoadingSpinner();
httpBackend httpBackend
.when("POST", "/login") .when("POST", "/login")
@ -650,11 +650,6 @@ describe("loading:", function () {
} }
}); });
// assert that we are on the loading page
async function assertAtLoadingSpinner(): Promise<void> {
await screen.findByRole("progressbar");
}
async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> { async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> {
if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in
@ -680,10 +675,6 @@ async function awaitLoginComponent(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage")); await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage"));
} }
async function awaitWelcomeComponent(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_Welcome"));
}
function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise<void> { function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise<void> {
dis.dispatch({ action: "start_login" }); dis.dispatch({ action: "start_login" });
return awaitLoginComponent(matrixChat); return awaitLoginComponent(matrixChat);

View File

@ -0,0 +1,87 @@
/*
Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
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 SdkConfig from "matrix-react-sdk/src/SdkConfig";
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import fetchMock from "fetch-mock-jest";
import { render, RenderResult, screen } from "@testing-library/react";
import { ModuleRunner } from "matrix-react-sdk/src/modules/ModuleRunner";
import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
import WebPlatform from "../../src/vector/platform/WebPlatform";
import { loadApp } from "../../src/vector/app";
import { waitForLoadingSpinner, waitForWelcomeComponent } from "../test-utils";
fetchMock.config.overwriteRoutes = true;
describe("Wrapper", () => {
beforeEach(async () => {
SdkConfig.reset();
PlatformPeg.set(new WebPlatform());
fetchMock.get("https://matrix-client.matrix.org/_matrix/client/versions", {
unstable_features: {},
versions: ["v1.1"],
});
fetchMock.get("https://matrix.org/.well-known/matrix/client", {
"m.homeserver": {
base_url: "https://matrix-client.matrix.org",
},
});
fetchMock.get("/version", "1.10.13");
});
it("wrap a matrix chat with header and footer", async () => {
SdkConfig.put({
default_server_config: {
"m.homeserver": {
base_url: "https://matrix-client.matrix.org",
},
},
});
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts) => {
if (lifecycleEvent === WrapperLifecycle.Wrapper) {
(opts as WrapperOpts).Wrapper = ({ children }) => {
return (
<>
<div data-testid="wrapper-header">Header</div>
<div data-testid="wrapper-matrix-chat">{children}</div>
<div data-testid="wrapper-footer">Footer</div>
</>
);
};
}
});
const matrixChatResult: RenderResult = render(await loadApp({}));
// at this point, we're trying to do a guest registration;
// we expect a spinner
await waitForLoadingSpinner();
await waitForWelcomeComponent(matrixChatResult);
// Are not semantic elements because Element has a footer already.
const header = screen.getByTestId("wrapper-header");
const matrixChat = screen.getByTestId("wrapper-matrix-chat");
const footer = screen.getByTestId("wrapper-footer");
expect(header.nextSibling).toBe(matrixChat);
expect(matrixChat.nextSibling).toBe(footer);
});
});

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { RenderResult, screen, waitFor } from "@testing-library/react";
export function cleanLocalstorage(): void { export function cleanLocalstorage(): void {
window.localStorage.clear(); window.localStorage.clear();
} }
@ -47,3 +49,12 @@ export function deleteIndexedDB(dbName: string): Promise<void> {
throw e; throw e;
}); });
} }
// wait for loading page
export async function waitForLoadingSpinner(): Promise<void> {
await screen.findByRole("progressbar");
}
export async function waitForWelcomeComponent(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_Welcome"));
}

View File

@ -1664,7 +1664,7 @@
version "3.2.14" version "3.2.14"
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"
"@matrix-org/react-sdk-module-api@^2.0.0", "@matrix-org/react-sdk-module-api@^2.1.0": "@matrix-org/react-sdk-module-api@^2.1.0":
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.1.0.tgz#ca9d67853512fda1df2786810b90be31dd8dc7b1" resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.1.0.tgz#ca9d67853512fda1df2786810b90be31dd8dc7b1"
integrity sha512-SARD5BsmZYv1hvuezLfBUafJ9+rPLbk5WO0S3vZgkLH3jJQrk7f/65qBB5fLKF2ljprfZ1GTpuBeq04wn7Tnmg== integrity sha512-SARD5BsmZYv1hvuezLfBUafJ9+rPLbk5WO0S3vZgkLH3jJQrk7f/65qBB5fLKF2ljprfZ1GTpuBeq04wn7Tnmg==