361 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			361 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2022 The Matrix.org Foundation C.I.C.
 | |
| 
 | |
| 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 { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations";
 | |
| import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo";
 | |
| import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent";
 | |
| import { screen, within } from "@testing-library/react";
 | |
| import userEvent from "@testing-library/user-event";
 | |
| import { MatrixClient } from "matrix-js-sdk/src/matrix";
 | |
| import { Mocked } from "jest-mock";
 | |
| 
 | |
| import { ProxiedModuleApi } from "../../src/modules/ProxiedModuleApi";
 | |
| import { getMockClientWithEventEmitter, mkRoom, stubClient } from "../test-utils";
 | |
| import { setLanguage } from "../../src/languageHandler";
 | |
| import { ModuleRunner } from "../../src/modules/ModuleRunner";
 | |
| import { registerMockModule } from "./MockModule";
 | |
| import defaultDispatcher from "../../src/dispatcher/dispatcher";
 | |
| import { Action } from "../../src/dispatcher/actions";
 | |
| import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
 | |
| import { Container, WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
 | |
| 
 | |
| describe("ProxiedApiModule", () => {
 | |
|     afterEach(() => {
 | |
|         ModuleRunner.instance.reset();
 | |
|     });
 | |
| 
 | |
|     // Note: Remainder is implicitly tested from end-to-end tests of modules.
 | |
| 
 | |
|     describe("translations", () => {
 | |
|         it("should cache translations", () => {
 | |
|             const api = new ProxiedModuleApi();
 | |
|             expect(api.translations).toBeFalsy();
 | |
| 
 | |
|             const translations: TranslationStringsObject = {
 | |
|                 ["custom string"]: {
 | |
|                     en: "custom string",
 | |
|                     fr: "custom french string",
 | |
|                 },
 | |
|             };
 | |
|             api.registerTranslations(translations);
 | |
|             expect(api.translations).toBe(translations);
 | |
|         });
 | |
| 
 | |
|         it("should overwriteAccountAuth", async () => {
 | |
|             const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
 | |
| 
 | |
|             const api = new ProxiedModuleApi();
 | |
|             const accountInfo = {} as unknown as AccountAuthInfo;
 | |
|             const promise = api.overwriteAccountAuth(accountInfo);
 | |
| 
 | |
|             expect(dispatchSpy).toHaveBeenCalledWith(
 | |
|                 expect.objectContaining({
 | |
|                     action: Action.OverwriteLogin,
 | |
|                     credentials: {
 | |
|                         ...accountInfo,
 | |
|                         guest: false,
 | |
|                     },
 | |
|                 }),
 | |
|                 expect.anything(),
 | |
|             );
 | |
| 
 | |
|             defaultDispatcher.fire(Action.OnLoggedIn);
 | |
| 
 | |
|             await expect(promise).resolves.toBeUndefined();
 | |
|         });
 | |
| 
 | |
|         describe("integration", () => {
 | |
|             it("should translate strings using translation system", async () => {
 | |
|                 // Test setup
 | |
|                 stubClient();
 | |
| 
 | |
|                 // Set up a module to pull translations through
 | |
|                 const module = registerMockModule();
 | |
|                 const en = "custom string";
 | |
|                 const de = "custom german string";
 | |
|                 const enVars = "custom variable %(var)s";
 | |
|                 const varVal = "string";
 | |
|                 const deVars = "custom german variable %(var)s";
 | |
|                 const deFull = `custom german variable ${varVal}`;
 | |
|                 expect(module.apiInstance).toBeInstanceOf(ProxiedModuleApi);
 | |
|                 module.apiInstance.registerTranslations({
 | |
|                     [en]: {
 | |
|                         en: en,
 | |
|                         de: de,
 | |
|                     },
 | |
|                     [enVars]: {
 | |
|                         en: enVars,
 | |
|                         de: deVars,
 | |
|                     },
 | |
|                 });
 | |
|                 await setLanguage("de"); // calls `registerCustomTranslations()` for us
 | |
| 
 | |
|                 // See if we can pull the German string out
 | |
|                 expect(module.apiInstance.translateString(en)).toEqual(de);
 | |
|                 expect(module.apiInstance.translateString(enVars, { var: varVal })).toEqual(deFull);
 | |
|             });
 | |
| 
 | |
|             afterEach(async () => {
 | |
|                 await setLanguage("en"); // reset the language
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("openDialog", () => {
 | |
|         it("should open dialog with a custom title and default options", async () => {
 | |
|             class MyDialogContent extends DialogContent {
 | |
|                 trySubmit = async () => ({ result: true });
 | |
|                 render = () => <p>This is my example content.</p>;
 | |
|             }
 | |
| 
 | |
|             const api = new ProxiedModuleApi();
 | |
| 
 | |
|             const resultPromise = api.openDialog<{ result: boolean }, DialogProps, MyDialogContent>(
 | |
|                 "My Dialog Title",
 | |
|                 (props, ref) => <MyDialogContent ref={ref} {...props} />,
 | |
|             );
 | |
| 
 | |
|             const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|             expect(within(dialog).getByRole("heading", { name: "My Dialog Title" })).toBeInTheDocument();
 | |
|             expect(within(dialog).getByText("This is my example content.")).toBeInTheDocument();
 | |
|             expect(within(dialog).getByRole("button", { name: "Cancel" })).toBeInTheDocument();
 | |
| 
 | |
|             await userEvent.click(within(dialog).getByRole("button", { name: "OK" }));
 | |
| 
 | |
|             expect(await resultPromise).toEqual({
 | |
|                 didOkOrSubmit: true,
 | |
|                 model: { result: true },
 | |
|             });
 | |
| 
 | |
|             expect(dialog).not.toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("should open dialog with custom options", async () => {
 | |
|             class MyDialogContent extends DialogContent {
 | |
|                 trySubmit = async () => ({ result: true });
 | |
|                 render = () => <p>This is my example content.</p>;
 | |
|             }
 | |
| 
 | |
|             const api = new ProxiedModuleApi();
 | |
| 
 | |
|             const resultPromise = api.openDialog<{ result: boolean }, DialogProps, MyDialogContent>(
 | |
|                 {
 | |
|                     title: "My Custom Dialog Title",
 | |
|                     actionLabel: "Submit it",
 | |
|                     cancelLabel: "Cancel it",
 | |
|                     canSubmit: false,
 | |
|                 },
 | |
|                 (props, ref) => <MyDialogContent ref={ref} {...props} />,
 | |
|             );
 | |
| 
 | |
|             const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|             expect(within(dialog).getByRole("heading", { name: "My Custom Dialog Title" })).toBeInTheDocument();
 | |
|             expect(within(dialog).getByText("This is my example content.")).toBeInTheDocument();
 | |
|             expect(within(dialog).getByRole("button", { name: "Submit it" })).toBeDisabled();
 | |
| 
 | |
|             await userEvent.click(within(dialog).getByRole("button", { name: "Cancel it" }));
 | |
| 
 | |
|             expect(await resultPromise).toEqual({ didOkOrSubmit: false });
 | |
| 
 | |
|             expect(dialog).not.toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("should update the options from the opened dialog", async () => {
 | |
|             class MyDialogContent extends DialogContent {
 | |
|                 trySubmit = async () => ({ result: true });
 | |
|                 render = () => {
 | |
|                     const onClick = () => {
 | |
|                         this.props.setOptions({
 | |
|                             title: "My New Title",
 | |
|                             actionLabel: "New Action",
 | |
|                             cancelLabel: "New Cancel",
 | |
|                         });
 | |
| 
 | |
|                         // check if delta updates work
 | |
|                         this.props.setOptions({
 | |
|                             canSubmit: false,
 | |
|                         });
 | |
|                     };
 | |
| 
 | |
|                     return (
 | |
|                         <button type="button" onClick={onClick}>
 | |
|                             Change the settings!
 | |
|                         </button>
 | |
|                     );
 | |
|                 };
 | |
|             }
 | |
| 
 | |
|             const api = new ProxiedModuleApi();
 | |
| 
 | |
|             const resultPromise = api.openDialog<{ result: boolean }, DialogProps, MyDialogContent>(
 | |
|                 "My Dialog Title",
 | |
|                 (props, ref) => <MyDialogContent ref={ref} {...props} />,
 | |
|             );
 | |
| 
 | |
|             const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|             expect(within(dialog).getByRole("heading", { name: "My Dialog Title" })).toBeInTheDocument();
 | |
|             expect(within(dialog).getByRole("button", { name: "Cancel" })).toBeInTheDocument();
 | |
|             expect(within(dialog).getByRole("button", { name: "OK" })).toBeEnabled();
 | |
| 
 | |
|             await userEvent.click(within(dialog).getByRole("button", { name: "Change the settings!" }));
 | |
| 
 | |
|             expect(within(dialog).getByRole("heading", { name: "My New Title" })).toBeInTheDocument();
 | |
|             expect(within(dialog).getByRole("button", { name: "New Action" })).toBeDisabled();
 | |
| 
 | |
|             await userEvent.click(within(dialog).getByRole("button", { name: "New Cancel" }));
 | |
| 
 | |
|             expect(await resultPromise).toEqual({
 | |
|                 didOkOrSubmit: false,
 | |
|                 model: undefined,
 | |
|             });
 | |
| 
 | |
|             expect(dialog).not.toBeInTheDocument();
 | |
|         });
 | |
| 
 | |
|         it("should cancel the dialog from within the dialog", async () => {
 | |
|             class MyDialogContent extends DialogContent {
 | |
|                 trySubmit = async () => ({ result: true });
 | |
|                 render = () => (
 | |
|                     <button type="button" onClick={this.props.cancel}>
 | |
|                         No need for action
 | |
|                     </button>
 | |
|                 );
 | |
|             }
 | |
| 
 | |
|             const api = new ProxiedModuleApi();
 | |
| 
 | |
|             const resultPromise = api.openDialog<{ result: boolean }, DialogProps, MyDialogContent>(
 | |
|                 "My Dialog Title",
 | |
|                 (props, ref) => <MyDialogContent ref={ref} {...props} />,
 | |
|             );
 | |
| 
 | |
|             const dialog = await screen.findByRole("dialog");
 | |
| 
 | |
|             await userEvent.click(within(dialog).getByRole("button", { name: "No need for action" }));
 | |
| 
 | |
|             expect(await resultPromise).toEqual({
 | |
|                 didOkOrSubmit: false,
 | |
|                 model: undefined,
 | |
|             });
 | |
| 
 | |
|             expect(dialog).not.toBeInTheDocument();
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("getApps", () => {
 | |
|         it("should return apps from the widget store", () => {
 | |
|             const api = new ProxiedModuleApi();
 | |
|             const app = {} as unknown as IApp;
 | |
|             const apps: IApp[] = [app];
 | |
| 
 | |
|             jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue(apps);
 | |
|             expect(api.getApps("!room:example.com")).toEqual(apps);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("getAppAvatarUrl", () => {
 | |
|         const app = {} as unknown as IApp;
 | |
|         const avatarUrl = "https://example.com/avatar.png";
 | |
| 
 | |
|         let api: ProxiedModuleApi;
 | |
|         let client: Mocked<MatrixClient>;
 | |
| 
 | |
|         beforeEach(() => {
 | |
|             api = new ProxiedModuleApi();
 | |
|             client = getMockClientWithEventEmitter({ mxcUrlToHttp: jest.fn().mockReturnValue(avatarUrl) });
 | |
|         });
 | |
| 
 | |
|         it("should return null if the app has no avatar URL", () => {
 | |
|             expect(api.getAppAvatarUrl(app)).toBeNull();
 | |
|         });
 | |
| 
 | |
|         it("should return the app avatar URL", () => {
 | |
|             expect(api.getAppAvatarUrl({ ...app, avatar_url: avatarUrl })).toBe(avatarUrl);
 | |
|         });
 | |
| 
 | |
|         it("should support optional thumbnail params", () => {
 | |
|             api.getAppAvatarUrl({ ...app, avatar_url: avatarUrl }, 1, 2, "3");
 | |
|             // eslint-disable-next-line no-restricted-properties
 | |
|             expect(client.mxcUrlToHttp).toHaveBeenCalledWith(avatarUrl, 1, 2, "3");
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("isAppInContainer", () => {
 | |
|         const app = {} as unknown as IApp;
 | |
|         const roomId = "!room:example.com";
 | |
| 
 | |
|         let api: ProxiedModuleApi;
 | |
|         let client: MatrixClient;
 | |
| 
 | |
|         beforeEach(() => {
 | |
|             api = new ProxiedModuleApi();
 | |
|             client = stubClient();
 | |
| 
 | |
|             jest.spyOn(WidgetLayoutStore.instance, "isInContainer");
 | |
|         });
 | |
| 
 | |
|         it("should return false if there is no room", () => {
 | |
|             client.getRoom = jest.fn().mockReturnValue(null);
 | |
| 
 | |
|             expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(false);
 | |
|             expect(WidgetLayoutStore.instance.isInContainer).not.toHaveBeenCalled();
 | |
|         });
 | |
| 
 | |
|         it("should return false if the app is not in the container", () => {
 | |
|             jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
 | |
|             expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(false);
 | |
|         });
 | |
| 
 | |
|         it("should return true if the app is in the container", () => {
 | |
|             jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
 | |
|             expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(true);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("moveAppToContainer", () => {
 | |
|         const app = {} as unknown as IApp;
 | |
|         const roomId = "!room:example.com";
 | |
| 
 | |
|         let api: ProxiedModuleApi;
 | |
|         let client: MatrixClient;
 | |
| 
 | |
|         beforeEach(() => {
 | |
|             api = new ProxiedModuleApi();
 | |
|             client = stubClient();
 | |
| 
 | |
|             jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
 | |
|         });
 | |
| 
 | |
|         it("should not move if there is no room", () => {
 | |
|             client.getRoom = jest.fn().mockReturnValue(null);
 | |
|             api.moveAppToContainer(app, Container.Top, roomId);
 | |
|             expect(WidgetLayoutStore.instance.moveToContainer).not.toHaveBeenCalled();
 | |
|         });
 | |
| 
 | |
|         it("should move if there is a room", () => {
 | |
|             const room = mkRoom(client, roomId);
 | |
|             client.getRoom = jest.fn().mockReturnValue(room);
 | |
| 
 | |
|             api.moveAppToContainer(app, Container.Top, roomId);
 | |
|             expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, Container.Top);
 | |
|         });
 | |
|     });
 | |
| });
 |