diff --git a/.eslintrc.js b/.eslintrc.js index 6fd40eb9f..72cadf813 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,7 @@ module.exports = { // We don't need super strict typing in test utilities "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/ban-ts-comment": "off", }, }, ], diff --git a/package.json b/package.json index 6a4fcfbb2..dd664704b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@types/react": "17.0.58" }, "dependencies": { - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.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": "^0.0.4", "gfm.css": "^1.1.2", "jsrsasign": "^10.5.25", @@ -104,6 +104,7 @@ "@svgr/webpack": "^5.5.0", "@testing-library/react": "^12.1.5", "@types/jest": "^29.0.0", + "@types/jitsi-meet": "^2.0.2", "@types/jsrsasign": "^10.5.4", "@types/modernizr": "^3.5.3", "@types/node": "^16", diff --git a/src/@types/jitsi-meet.d.ts b/src/@types/jitsi-meet.d.ts new file mode 100644 index 000000000..a5680debd --- /dev/null +++ b/src/@types/jitsi-meet.d.ts @@ -0,0 +1,29 @@ +/* +Copyright 2023 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 "jitsi-meet"; + +declare module "jitsi-meet" { + interface ExternalAPIEventCallbacks { + errorOccurred: (e: { error: Error & { isFatal?: boolean } }) => void; + } + + interface JitsiMeetExternalAPI { + executeCommand(command: "setTileView", value: boolean): void; + } +} + +export as namespace Jitsi; diff --git a/src/async-components/structures/CompatibilityView.tsx b/src/async-components/structures/CompatibilityView.tsx index 02bcffa44..dde0aea25 100644 --- a/src/async-components/structures/CompatibilityView.tsx +++ b/src/async-components/structures/CompatibilityView.tsx @@ -17,10 +17,10 @@ limitations under the License. import * as React from "react"; import { _t } from "matrix-react-sdk/src/languageHandler"; import SdkConfig from "matrix-react-sdk/src/SdkConfig"; - // directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk // PostCSS variables will be accessible. import "../../../res/css/structures/ErrorView.pcss"; +import { ReactNode } from "react"; interface IProps { onAccept(): void; @@ -91,7 +91,7 @@ const CompatibilityView: React.FC = ({ onAccept }) => { android = []; } - let mobileHeader =

{_t("Use %(brand)s on mobile", { brand })}

; + let mobileHeader: ReactNode =

{_t("Use %(brand)s on mobile", { brand })}

; if (!android.length && !ios) { mobileHeader = null; } diff --git a/src/components/views/auth/VectorAuthPage.tsx b/src/components/views/auth/VectorAuthPage.tsx index 560fe424a..e04dfcefd 100644 --- a/src/components/views/auth/VectorAuthPage.tsx +++ b/src/components/views/auth/VectorAuthPage.tsx @@ -20,7 +20,7 @@ import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import VectorAuthFooter from "./VectorAuthFooter"; export default class VectorAuthPage extends React.PureComponent { - private static welcomeBackgroundUrl; + private static welcomeBackgroundUrl?: string; // cache the url as a static to prevent it changing without refreshing private static getWelcomeBackgroundUrl(): string { diff --git a/src/favicon.ts b/src/favicon.ts index 5a9516d24..9f4fbec39 100644 --- a/src/favicon.ts +++ b/src/favicon.ts @@ -72,14 +72,14 @@ export default class Favicon { // get height and width of the favicon this.canvas.height = this.baseImage.height > 0 ? this.baseImage.height : 32; this.canvas.width = this.baseImage.width > 0 ? this.baseImage.width : 32; - this.context = this.canvas.getContext("2d"); + this.context = this.canvas.getContext("2d")!; this.ready(); }; - this.baseImage.setAttribute("src", lastIcon.getAttribute("href")); + this.baseImage.setAttribute("src", lastIcon.getAttribute("href")!); } else { this.canvas.height = this.baseImage.height = 32; this.canvas.width = this.baseImage.width = 32; - this.context = this.canvas.getContext("2d"); + this.context = this.canvas.getContext("2d")!; this.ready(); } } @@ -239,7 +239,7 @@ export default class Favicon { const icons: HTMLLinkElement[] = []; const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link"); for (const link of links) { - if (/(^|\s)icon(\s|$)/i.test(link.getAttribute("rel"))) { + if (link.hasAttribute("rel") && /(^|\s)icon(\s|$)/i.test(link.getAttribute("rel")!)) { icons.push(link); } } diff --git a/src/vector/app.tsx b/src/vector/app.tsx index 6614948ab..2b140a5ba 100644 --- a/src/vector/app.tsx +++ b/src/vector/app.tsx @@ -25,7 +25,7 @@ import React, { ReactElement } from "react"; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg"; import { UserFriendlyError } from "matrix-react-sdk/src/languageHandler"; import AutoDiscoveryUtils from "matrix-react-sdk/src/utils/AutoDiscoveryUtils"; -import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; +import { AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/autodiscovery"; import * as Lifecycle from "matrix-react-sdk/src/Lifecycle"; import SdkConfig, { parseSsoRedirectOptions } from "matrix-react-sdk/src/SdkConfig"; import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions"; @@ -33,6 +33,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import { createClient } from "matrix-js-sdk/src/matrix"; import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject"; import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat"; +import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig"; +import { QueryDict, encodeParams } from "matrix-js-sdk/src/utils"; import { parseQs } from "./url_utils"; import VectorBasePlatform from "./platform/VectorBasePlatform"; @@ -55,24 +57,19 @@ window.matrixLogger = logger; // If we're in electron, we should never pass through a file:// URL otherwise // the identity server will try to 302 the browser to it, which breaks horribly. // so in that instance, hardcode to use app.element.io for now instead. -function makeRegistrationUrl(params: object): string { - let url; +function makeRegistrationUrl(params: QueryDict): string { + let url: string; if (window.location.protocol === "vector:") { url = "https://app.element.io/#/register"; } else { url = window.location.protocol + "//" + window.location.host + window.location.pathname + "#/register"; } - const keys = Object.keys(params); - for (let i = 0; i < keys.length; ++i) { - if (i === 0) { - url += "?"; - } else { - url += "&"; - } - const k = keys[i]; - url += k + "=" + encodeURIComponent(params[k]); + const encodedParams = encodeParams(params); + if (encodedParams) { + url += "?" + encodedParams; } + return url; } @@ -117,18 +114,19 @@ export async function loadApp(fragParams: {}): Promise { if (!hasPossibleToken && !isReturningFromSso && autoRedirect) { logger.log("Bypassing app load to redirect to SSO"); const tempCli = createClient({ - baseUrl: config.validated_server_config.hsUrl, - idBaseUrl: config.validated_server_config.isUrl, + baseUrl: config.validated_server_config!.hsUrl, + idBaseUrl: config.validated_server_config!.isUrl, }); - PlatformPeg.get().startSingleSignOn(tempCli, "sso", `/${getScreenFromLocation(window.location).screen}`); + PlatformPeg.get()!.startSingleSignOn(tempCli, "sso", `/${getScreenFromLocation(window.location).screen}`); // We return here because startSingleSignOn() will asynchronously redirect us. We don't // care to wait for it, and don't want to show any UI while we wait (not even half a welcome // page). As such, just don't even bother loading the MatrixChat component. - return; + return ; } - const defaultDeviceName = snakedConfig.get("default_device_display_name") ?? platform.getDefaultDeviceDisplayName(); + const defaultDeviceName = + snakedConfig.get("default_device_display_name") ?? platform?.getDefaultDeviceDisplayName(); return ( { } async function verifyServerConfig(): Promise { - let validatedConfig; + let validatedConfig: ValidatedServerConfig; try { logger.log("Verifying homeserver configuration"); @@ -197,7 +195,7 @@ async function verifyServerConfig(): Promise { } } - let discoveryResult = null; + let discoveryResult: ClientConfig | undefined; if (wkConfig) { logger.log("Config uses a default_server_config - validating object"); discoveryResult = await AutoDiscovery.fromDiscoveryConfig(wkConfig); diff --git a/src/vector/getconfig.ts b/src/vector/getconfig.ts index 4633b57d8..17a897809 100644 --- a/src/vector/getconfig.ts +++ b/src/vector/getconfig.ts @@ -18,16 +18,16 @@ import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions"; // Load the config file. First try to load up a domain-specific config of the // form "config.$domain.json" and if that fails, fall back to config.json. -export async function getVectorConfig(relativeLocation = ""): Promise { +export async function getVectorConfig(relativeLocation = ""): Promise { if (relativeLocation !== "" && !relativeLocation.endsWith("/")) relativeLocation += "/"; - const specificConfigPromise = getConfig(`${relativeLocation}config.${document.domain}.json`); + const specificConfigPromise = getConfig(`${relativeLocation}config.${window.location.hostname}.json`); const generalConfigPromise = getConfig(relativeLocation + "config.json"); try { const configJson = await specificConfigPromise; // 404s succeed with an empty json config, so check that there are keys - if (Object.keys(configJson).length === 0) { + if (!configJson || Object.keys(configJson).length === 0) { throw new Error(); // throw to enter the catch } return configJson; @@ -36,7 +36,7 @@ export async function getVectorConfig(relativeLocation = ""): Promise { +async function getConfig(configJsonFilename: string): Promise { const url = new URL(configJsonFilename, window.location.href); url.searchParams.set("cachebuster", Date.now().toString()); const res = await fetch(url, { diff --git a/src/vector/index.ts b/src/vector/index.ts index dcffa5e2c..56d87e4fd 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -71,7 +71,7 @@ function checkBrowserFeatures(): boolean { // ES2019: http://262.ecma-international.org/10.0/#sec-object.fromentries window.Modernizr.addTest("objectfromentries", () => typeof window.Object?.fromEntries === "function"); - const featureList = Object.keys(window.Modernizr); + const featureList = Object.keys(window.Modernizr) as Array; let featureComplete = true; for (const feature of featureList) { @@ -240,7 +240,7 @@ start().catch((err) => { logger.error(err); // show the static error in an iframe to not lose any context / console data // with some basic styling to make the iframe full page - delete document.body.style.height; + document.body.style.removeProperty("height"); const iframe = document.createElement("iframe"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - typescript seems to only like the IE syntax for iframe sandboxing @@ -254,5 +254,5 @@ start().catch((err) => { iframe.style.right = "0"; iframe.style.bottom = "0"; iframe.style.border = "0"; - document.getElementById("matrixchat").appendChild(iframe); + document.getElementById("matrixchat")?.appendChild(iframe); }); diff --git a/src/vector/init.tsx b/src/vector/init.tsx index 6ab4e4cac..a255c7d7f 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -67,7 +67,7 @@ export async function loadConfig(): Promise { // granular settings are loaded correctly and to avoid duplicating the override logic for the theme. // // Note: this isn't called twice for some wrappers, like the Jitsi wrapper. - const platformConfig = await PlatformPeg.get().getConfig(); + const platformConfig = await PlatformPeg.get()?.getConfig(); if (platformConfig) { SdkConfig.put(platformConfig); } else { @@ -119,7 +119,7 @@ export function loadOlm(): Promise { export async function loadLanguage(): Promise { const prefLang = SettingsStore.getValue("language", null, /*excludeDefault=*/ true); - let langs = []; + let langs: string[] = []; if (!prefLang) { languageHandler.getLanguagesFromBrowser().forEach((l) => { @@ -163,7 +163,7 @@ export async function showError(title: string, messages?: string[]): Promise { +export async function showIncompatibleBrowser(onAccept: () => void): Promise { const CompatibilityView = ( await import( /* webpackChunkName: "compatibility-view" */ diff --git a/src/vector/jitsi/index.ts b/src/vector/jitsi/index.ts index 77379fe20..f50fa0d8d 100644 --- a/src/vector/jitsi/index.ts +++ b/src/vector/jitsi/index.ts @@ -30,6 +30,14 @@ import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions"; import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject"; import { ElementWidgetCapabilities } from "matrix-react-sdk/src/stores/widgets/ElementWidgetCapabilities"; +import type { + JitsiMeetExternalAPIConstructor, + ExternalAPIEventCallbacks, + JitsiMeetExternalAPI as _JitsiMeetExternalAPI, + AudioMuteStatusChangedEvent, + LogEvent, + VideoMuteStatusChangedEvent, +} from "jitsi-meet"; import { getVectorConfig } from "../getconfig"; // We have to trick webpack into loading our CSS for us. @@ -40,7 +48,7 @@ const JITSI_OPENIDTOKEN_JWT_AUTH = "openidtoken-jwt"; // Dev note: we use raw JS without many dependencies to reduce bundle size. // We do not need all of React to render a Jitsi conference. -declare let JitsiMeetExternalAPI: any; +declare let JitsiMeetExternalAPI: JitsiMeetExternalAPIConstructor; let inConference = false; @@ -60,8 +68,8 @@ let isVideoChannel: boolean; let supportsScreensharing: boolean; let language: string; -let widgetApi: WidgetApi; -let meetApi: any; // JitsiMeetExternalAPI +let widgetApi: WidgetApi | undefined; +let meetApi: _JitsiMeetExternalAPI | undefined; let skipOurWelcomeScreen = false; const setupCompleted = (async (): Promise => { @@ -102,12 +110,12 @@ const setupCompleted = (async (): Promise => { } // Set this up as early as possible because Element will be hitting it almost immediately. - let widgetApiReady: Promise; + let widgetApiReady: Promise | undefined; if (parentUrl && widgetId) { const parentOrigin = new URL(qsParam("parentUrl")).origin; widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin); - widgetApiReady = new Promise((resolve) => widgetApi.once("ready", resolve)); + widgetApiReady = new Promise((resolve) => widgetApi!.once("ready", resolve)); widgetApi.requestCapabilities(VideoConferenceCapabilities); // jitsi cannot work in a popup if auth token is provided because widgetApi is not available there @@ -122,7 +130,7 @@ const setupCompleted = (async (): Promise => { action: WidgetApiAction, handler: (request: IWidgetApiRequestData) => Promise, ): void => { - widgetApi.on(`action:${action}`, async (ev: CustomEvent) => { + widgetApi!.on(`action:${action}`, async (ev: CustomEvent) => { ev.preventDefault(); await setupCompleted; @@ -138,7 +146,7 @@ const setupCompleted = (async (): Promise => { } } - await widgetApi.transport.reply(ev.detail, response); + await widgetApi!.transport.reply(ev.detail, response); }); }; @@ -149,7 +157,7 @@ const setupCompleted = (async (): Promise => { if (force === true) { meetApi?.dispose(); notifyHangup(); - meetApi = null; + meetApi = undefined; closeConference(); } else { meetApi?.executeCommand("hangup"); @@ -212,9 +220,10 @@ const setupCompleted = (async (): Promise => { // We've reached the point where we have to wait for the config, so do that then parse it. const instanceConfig = new SnakedObject((await configPromise) ?? {}); - const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {}; - skipOurWelcomeScreen = - new SnakedObject(jitsiConfig).get("skip_built_in_welcome_screen") ?? false; + const jitsiConfig = instanceConfig.get("jitsi_widget"); + if (jitsiConfig) { + skipOurWelcomeScreen = new SnakedObject(jitsiConfig).get("skip_built_in_welcome_screen") ?? false; + } // Either reveal the prejoin screen, or skip straight to Jitsi depending on the config. // We don't set up the call yet though as this might lead to failure without the widget API. @@ -232,12 +241,12 @@ const setupCompleted = (async (): Promise => { enableJoinButton(); // always enable the button } catch (e) { logger.error("Error setting up Jitsi widget", e); - document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; + document.getElementById("widgetActionContainer")!.innerText = "Failed to load Jitsi widget"; } })(); function enableJoinButton(): void { - document.getElementById("joinButton").onclick = (): Promise => joinConference(); + document.getElementById("joinButton")!.onclick = (): Promise => joinConference(); } function switchVisibleContainers(): void { @@ -251,9 +260,9 @@ function switchVisibleContainers(): void { } function toggleConferenceVisibility(inConference: boolean): void { - document.getElementById("jitsiContainer").style.visibility = inConference ? "unset" : "hidden"; + document.getElementById("jitsiContainer")!.style.visibility = inConference ? "unset" : "hidden"; // Video rooms have a separate UI for joining, so they should never show our join button - document.getElementById("joinButtonContainer").style.visibility = + document.getElementById("joinButtonContainer")!.style.visibility = inConference || isVideoChannel ? "hidden" : "unset"; } @@ -311,7 +320,7 @@ async function notifyHangup(errorMessage?: string): Promise { function closeConference(): void { switchVisibleContainers(); - document.getElementById("jitsiContainer").innerHTML = ""; + document.getElementById("jitsiContainer")!.innerHTML = ""; if (skipOurWelcomeScreen) { skipToJitsiSplashScreen(); @@ -349,17 +358,17 @@ function mapLanguage(language: string): string { // and a non-nullish input specifies the label of a specific device to use. // Same for video inputs. async function joinConference(audioInput?: string | null, videoInput?: string | null): Promise { - let jwt; + let jwt: string | undefined; if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { // See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification - const openIdToken: IOpenIDCredentials = await widgetApi.requestOpenIDConnectToken(); + const openIdToken = await widgetApi?.requestOpenIDConnectToken(); logger.log("Got OpenID Connect token"); - if (!openIdToken.access_token) { + if (!openIdToken?.access_token) { // eslint-disable-line camelcase // We've failing to get a token, don't try to init conference logger.warn("Expected to have an OpenID credential, cannot initialize widget."); - document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; + document.getElementById("widgetActionContainer")!.innerText = "Failed to load Jitsi widget"; return; } jwt = createJWTToken(openIdToken); @@ -376,7 +385,7 @@ async function joinConference(audioInput?: string | null, videoInput?: string | const options = { width: "100%", height: "100%", - parentNode: document.querySelector("#jitsiContainer"), + parentNode: document.querySelector("#jitsiContainer") ?? undefined, roomName: conferenceId, devices: { audioInput, @@ -434,13 +443,13 @@ async function joinConference(audioInput?: string | null, videoInput?: string | // (regardless of video on or off) meetApi.on("videoConferenceJoined", onVideoConferenceJoined); meetApi.on("videoConferenceLeft", onVideoConferenceLeft); - meetApi.on("readyToClose", closeConference); + meetApi.on("readyToClose", closeConference as ExternalAPIEventCallbacks["readyToClose"]); meetApi.on("errorOccurred", onErrorOccurred); meetApi.on("audioMuteStatusChanged", onAudioMuteStatusChanged); meetApi.on("videoMuteStatusChanged", onVideoMuteStatusChanged); - ["videoConferenceJoined", "participantJoined", "participantLeft"].forEach((event) => { - meetApi.on(event, updateParticipants); + (["videoConferenceJoined", "participantJoined", "participantLeft"] as const).forEach((event) => { + meetApi!.on(event, updateParticipants); }); // Patch logs into rageshakes @@ -456,9 +465,9 @@ const onVideoConferenceJoined = (): void => { // We can't just use these commands immediately after creating the // iframe, because there's *another* bug where they can crash Jitsi by // racing with its startup process. - if (displayName) meetApi.executeCommand("displayName", displayName); + if (displayName) meetApi?.executeCommand("displayName", displayName); // This doesn't have a userInfo equivalent, so has to be set via commands - if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl); + if (avatarUrl) meetApi?.executeCommand("avatarUrl", avatarUrl); if (widgetApi) { // ignored promise because we don't care if it works @@ -468,30 +477,30 @@ const onVideoConferenceJoined = (): void => { } // Video rooms should start in tile mode - if (isVideoChannel) meetApi.executeCommand("setTileView", true); + if (isVideoChannel) meetApi?.executeCommand("setTileView", true); }; const onVideoConferenceLeft = (): void => { notifyHangup(); - meetApi = null; + meetApi = undefined; }; -const onErrorOccurred = ({ error }): void => { +const onErrorOccurred = ({ error }: Parameters[0]): void => { if (error.isFatal) { // We got disconnected. Since Jitsi Meet might send us back to the // prejoin screen, we're forced to act as if we hung up entirely. notifyHangup(error.message); - meetApi = null; + meetApi = undefined; closeConference(); } }; -const onAudioMuteStatusChanged = ({ muted }): void => { +const onAudioMuteStatusChanged = ({ muted }: AudioMuteStatusChangedEvent): void => { const action = muted ? ElementWidgetActions.MuteAudio : ElementWidgetActions.UnmuteAudio; widgetApi?.transport.send(action, {}); }; -const onVideoMuteStatusChanged = ({ muted }): void => { +const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void => { if (muted) { // Jitsi Meet always sends a "video muted" event directly before // hanging up, which we need to ignore by padding the timeout here, @@ -507,8 +516,9 @@ const onVideoMuteStatusChanged = ({ muted }): void => { const updateParticipants = (): void => { widgetApi?.transport.send(ElementWidgetActions.CallParticipants, { - participants: meetApi.getParticipantsInfo(), + participants: meetApi?.getParticipantsInfo(), }); }; -const onLog = ({ logLevel, args }): void => (parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args); +const onLog = ({ logLevel, args }: LogEvent): void => + (parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args); diff --git a/src/vector/mobile_guide/index.ts b/src/vector/mobile_guide/index.ts index 0a0a351ee..5f9344074 100644 --- a/src/vector/mobile_guide/index.ts +++ b/src/vector/mobile_guide/index.ts @@ -31,17 +31,17 @@ function renderConfigError(message: string): void { } async function initPage(): Promise { - document.getElementById("back_to_element_button").onclick = onBackToElementClick; + document.getElementById("back_to_element_button")!.onclick = onBackToElementClick; const config = await getVectorConfig(".."); // We manually parse the config similar to how validateServerConfig works because // calling that function pulls in roughly 4mb of JS we don't use. - const wkConfig = config["default_server_config"]; // overwritten later under some conditions - const serverName = config["default_server_name"]; - const defaultHsUrl = config["default_hs_url"]; - const defaultIsUrl = config["default_is_url"]; + const wkConfig = config?.["default_server_config"]; // overwritten later under some conditions + const serverName = config?.["default_server_name"]; + const defaultHsUrl = config?.["default_hs_url"]; + const defaultIsUrl = config?.["default_is_url"]; const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i); if (incompatibleOptions.length > 1) { @@ -54,13 +54,13 @@ async function initPage(): Promise { return renderConfigError("Invalid configuration: no default server specified."); } - let hsUrl = ""; - let isUrl = ""; + let hsUrl: string | undefined; + let isUrl: string | undefined; - if (wkConfig && wkConfig["m.homeserver"]) { + if (typeof wkConfig?.["m.homeserver"]?.["base_url"] === "string") { hsUrl = wkConfig["m.homeserver"]["base_url"]; - if (wkConfig["m.identity_server"]) { + if (typeof wkConfig["m.identity_server"]?.["base_url"] === "string") { isUrl = wkConfig["m.identity_server"]["base_url"]; } } @@ -96,17 +96,19 @@ async function initPage(): Promise { if (isUrl && !isUrl.endsWith("/")) isUrl += "/"; if (hsUrl !== "https://matrix.org/") { - (document.getElementById("configure_element_button") as HTMLAnchorElement).href = - "https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl) + "&is_url=" + encodeURIComponent(isUrl); - document.getElementById("step1_heading").innerHTML = "1: Install the app"; - document.getElementById("step2_container").style.display = "block"; - document.getElementById("hs_url").innerText = hsUrl; + let url = "https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl); if (isUrl) { - document.getElementById("custom_is").style.display = "block"; - document.getElementById("is_url").style.display = "block"; - document.getElementById("is_url").innerText = isUrl; + document.getElementById("custom_is")!.style.display = "block"; + document.getElementById("is_url")!.style.display = "block"; + document.getElementById("is_url")!.innerText = isUrl; + url += "&is_url=" + encodeURIComponent(isUrl ?? ""); } + + (document.getElementById("configure_element_button") as HTMLAnchorElement).href = url; + document.getElementById("step1_heading")!.innerHTML = "1: Install the app"; + document.getElementById("step2_container")!.style.display = "block"; + document.getElementById("hs_url")!.innerText = hsUrl; } } diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx index 537ce4cc0..b18b1abe4 100644 --- a/src/vector/platform/ElectronPlatform.tsx +++ b/src/vector/platform/ElectronPlatform.tsx @@ -79,7 +79,7 @@ function platformFriendlyName(): string { function onAction(payload: ActionPayload): void { // Whitelist payload actions, no point sending most across if (["call_state"].includes(payload.action)) { - window.electron.send("app_onAction", payload); + window.electron!.send("app_onAction", payload); } } @@ -105,6 +105,10 @@ export default class ElectronPlatform extends VectorBasePlatform { public constructor() { super(); + if (!window.electron) { + throw new Error("Cannot instantiate ElectronPlatform, window.electron is not set"); + } + dis.register(onAction); /* IPC Call `check_updates` returns: @@ -135,12 +139,12 @@ export default class ElectronPlatform extends VectorBasePlatform { const key = `DOWNLOAD_TOAST_${id}`; const onAccept = (): void => { - window.electron.send("userDownloadAction", { id, open: true }); + window.electron!.send("userDownloadAction", { id, open: true }); ToastStore.sharedInstance().dismissToast(key); }; const onDismiss = (): void => { - window.electron.send("userDownloadAction", { id }); + window.electron!.send("userDownloadAction", { id }); }; ToastStore.sharedInstance().addOrReplaceToast({ @@ -164,7 +168,7 @@ export default class ElectronPlatform extends VectorBasePlatform { BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); } - public async getConfig(): Promise { + public async getConfig(): Promise { return this.ipc.call("getConfig"); } @@ -212,7 +216,7 @@ export default class ElectronPlatform extends VectorBasePlatform { if (this.notificationCount === count) return; super.setNotificationCount(count); - window.electron.send("setBadgeCount", count); + window.electron!.send("setBadgeCount", count); } public supportsNotifications(): boolean { @@ -252,7 +256,7 @@ export default class ElectronPlatform extends VectorBasePlatform { } public loudNotification(ev: MatrixEvent, room: Room): void { - window.electron.send("loudNotification"); + window.electron!.send("loudNotification"); } public needsUrlTooltips(): boolean { @@ -288,14 +292,14 @@ export default class ElectronPlatform extends VectorBasePlatform { public startUpdateCheck(): void { super.startUpdateCheck(); - window.electron.send("check_updates"); + window.electron!.send("check_updates"); } public installUpdate(): void { // IPC to the main process to install the update, since quitAndInstall // doesn't fire the before-quit event so the main process needs to know // it should exit. - window.electron.send("install_update"); + window.electron!.send("install_update"); } public getDefaultDeviceDisplayName(): string { diff --git a/src/vector/platform/IPCManager.ts b/src/vector/platform/IPCManager.ts index 868f528d3..2ad4a2556 100644 --- a/src/vector/platform/IPCManager.ts +++ b/src/vector/platform/IPCManager.ts @@ -33,6 +33,9 @@ export class IPCManager { private readonly sendChannel: ElectronChannel = "ipcCall", private readonly recvChannel: ElectronChannel = "ipcReply", ) { + if (!window.electron) { + throw new Error("Cannot instantiate ElectronPlatform, window.electron is not set"); + } window.electron.on(this.recvChannel, this.onIpcReply); } @@ -42,7 +45,7 @@ export class IPCManager { const deferred = defer(); this.pendingIpcCalls[ipcCallId] = deferred; // Maybe add a timeout to these? Probably not necessary. - window.electron.send(this.sendChannel, { id: ipcCallId, name, args }); + window.electron!.send(this.sendChannel, { id: ipcCallId, name, args }); return deferred.promise; } diff --git a/src/vector/platform/SeshatIndexManager.ts b/src/vector/platform/SeshatIndexManager.ts index 3526bcc46..dd77d5800 100644 --- a/src/vector/platform/SeshatIndexManager.ts +++ b/src/vector/platform/SeshatIndexManager.ts @@ -19,6 +19,7 @@ import BaseEventIndexManager, { IEventAndProfile, IIndexStats, ISearchArgs, + ILoadArgs, } from "matrix-react-sdk/src/indexing/BaseEventIndexManager"; import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search"; @@ -75,7 +76,7 @@ export class SeshatIndexManager extends BaseEventIndexManager { return this.ipc.call("removeCrawlerCheckpoint", checkpoint); } - public async loadFileEvents(args): Promise { + public async loadFileEvents(args: ILoadArgs): Promise { return this.ipc.call("loadFileEvents", args); } diff --git a/src/vector/platform/VectorBasePlatform.ts b/src/vector/platform/VectorBasePlatform.ts index cbb3b9eeb..fccb6119c 100644 --- a/src/vector/platform/VectorBasePlatform.ts +++ b/src/vector/platform/VectorBasePlatform.ts @@ -30,7 +30,7 @@ import Favicon from "../../favicon"; export default abstract class VectorBasePlatform extends BasePlatform { protected _favicon: Favicon; - public async getConfig(): Promise { + public async getConfig(): Promise { return getVectorConfig(); } diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 938fc8fd8..75eae03e8 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -40,6 +40,8 @@ function getNormalizedAppVersion(version: string): string { } export default class WebPlatform extends VectorBasePlatform { + private static readonly VERSION = process.env.VERSION!; // baked in by Webpack + public constructor() { super(); // Register service worker if available on this platform @@ -101,7 +103,7 @@ export default class WebPlatform extends VectorBasePlatform { } public getAppVersion(): Promise { - return Promise.resolve(getNormalizedAppVersion(process.env.VERSION)); + return Promise.resolve(getNormalizedAppVersion(WebPlatform.VERSION)); } public startUpdater(): void { @@ -113,7 +115,7 @@ export default class WebPlatform extends VectorBasePlatform { // // Ideally, loading an old copy would be impossible with the // cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/ - console.log("startUpdater, current version is " + getNormalizedAppVersion(process.env.VERSION)); + console.log("startUpdater, current version is " + getNormalizedAppVersion(WebPlatform.VERSION)); this.pollForUpdate((version: string, newVersion: string) => { const query = parseQs(location); if (query.updated) { @@ -144,7 +146,7 @@ export default class WebPlatform extends VectorBasePlatform { ): Promise => { return this.getMostRecentVersion().then( (mostRecentVersion) => { - const currentVersion = getNormalizedAppVersion(process.env.VERSION); + const currentVersion = getNormalizedAppVersion(WebPlatform.VERSION); if (currentVersion !== mostRecentVersion) { if (this.shouldShowUpdate(mostRecentVersion)) { diff --git a/src/vector/routing.ts b/src/vector/routing.ts index 92b2c7a59..04d455f51 100644 --- a/src/vector/routing.ts +++ b/src/vector/routing.ts @@ -22,7 +22,7 @@ import MatrixChatType from "matrix-react-sdk/src/components/structures/MatrixCha import { parseQsFromFragment } from "./url_utils"; -let lastLocationHashSet: string = null; +let lastLocationHashSet: string | null = null; export function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } { const fragparts = parseQsFromFragment(location); diff --git a/test/app-tests/loading-test.tsx b/test/app-tests/loading-test.tsx index b57695df1..e5805e66e 100644 --- a/test/app-tests/loading-test.tsx +++ b/test/app-tests/loading-test.tsx @@ -28,6 +28,8 @@ 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"; @@ -38,26 +40,21 @@ const DEFAULT_HS_URL = "http://my_server"; const DEFAULT_IS_URL = "http://my_is"; describe("loading:", function () { - let parentDiv; - let httpBackend; + let httpBackend: MockHttpBackend; // an Object simulating the window.location let windowLocation: Location | undefined; // the mounted MatrixChat - let matrixChat: RenderResult | undefined; + let matrixChat: RenderResult | undefined; // a promise which resolves when the MatrixChat calls onTokenLoginCompleted let tokenLoginCompletePromise: Promise | undefined; beforeEach(function () { httpBackend = new MockHttpBackend(); + // @ts-ignore window.fetch = httpBackend.fetchFn; - 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 = undefined; matrixChat = undefined; @@ -80,8 +77,13 @@ describe("loading:", function () { * 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?): void { - opts = opts || {}; + function loadApp( + opts: { + queryString?: string; + uriFragment?: string; + config?: IConfigOptions; + } = {}, + ): void { const queryString = opts.queryString || ""; const uriFragment = opts.uriFragment || ""; @@ -93,7 +95,7 @@ describe("loading:", function () { }, } as Location; - function onNewScreen(screen): void { + function onNewScreen(screen: string): void { console.log(Date.now() + " newscreen " + screen); const hash = "#/" + screen; windowLocation!.hash = hash; @@ -102,7 +104,7 @@ describe("loading:", function () { // Parse the given window.location and return parameters that can be used when calling // MatrixChat.showScreen(screen, params) - function getScreenFromLocation(location): { screen: string; params: QueryDict } { + function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } { const fragparts = parseQsFromFragment(location); return { screen: fragparts.location.substring(1), @@ -112,22 +114,20 @@ describe("loading:", function () { const fragParts = parseQsFromFragment(windowLocation); - const config = Object.assign( - { - 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, - embeddedPages: { - homeUrl: "data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==", - }, + 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 || {}, - ); + ...(opts.config ?? {}), + } as IConfigOptions; PlatformPeg.set(new WebPlatform()); @@ -137,18 +137,17 @@ describe("loading:", function () { matrixChat = render( { throw new Error("Not implemented"); }} />, - parentDiv, ); }); } @@ -157,15 +156,15 @@ describe("loading:", function () { // http requests until we do. // // returns a promise resolving to the received request - async function expectAndAwaitSync(opts?): Promise { - let syncRequest = null; + async function expectAndAwaitSync(opts?: { isGuest?: boolean }): Promise { + let syncRequest: (typeof MockHttpBackend.prototype.requests)[number] | null = null; httpBackend.when("GET", "/_matrix/client/versions").respond(200, { versions: ["r0.3.0"], unstable_features: { "m.lazy_load_members": true, }, }); - const isGuest = opts && opts.isGuest; + const isGuest = opts?.isGuest; if (!isGuest) { // the call to create the LL filter httpBackend.when("POST", "/filter").respond(200, { filter_id: "llfid" }); @@ -183,7 +182,7 @@ describe("loading:", function () { if (syncRequest) { return syncRequest; } - await httpBackend.flush(); + await httpBackend.flush(undefined); } throw new Error("Gave up waiting for /sync"); } @@ -201,11 +200,11 @@ describe("loading:", function () { httpBackend .when("POST", "/register") .check(function (req) { - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(403, "Guest access is disabled"); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { // Wait for another trip around the event loop for the UI to update @@ -234,11 +233,11 @@ describe("loading:", function () { httpBackend .when("POST", "/register") .check(function (req) { - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(403, "Guest access is disabled"); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { // Wait for another trip around the event loop for the UI to update @@ -403,14 +402,14 @@ describe("loading:", function () { httpBackend .when("POST", "/register") .check(function (req) { - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(200, { user_id: "@guest:localhost", access_token: "secret_token", }); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { return awaitLoggedIn(matrixChat!); @@ -440,14 +439,14 @@ describe("loading:", function () { .when("POST", "/register") .check(function (req) { expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true); - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(200, { user_id: "@guest:localhost", access_token: "secret_token", }); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { return awaitLoggedIn(matrixChat!); @@ -480,14 +479,14 @@ describe("loading:", function () { httpBackend .when("POST", "/register") .check(function (req) { - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(200, { user_id: "@guest:localhost", access_token: "secret_token", }); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { return awaitLoggedIn(matrixChat!); @@ -513,7 +512,7 @@ describe("loading:", function () { httpBackend .when("POST", "/register") .check(function (req) { - expect(req.queryParams.kind).toEqual("guest"); + expect(req.queryParams?.kind).toEqual("guest"); }) .respond(200, { user_id: "@guest:localhost", @@ -521,7 +520,7 @@ describe("loading:", function () { }); return httpBackend - .flush() + .flush(undefined) .then(() => { return awaitLoggedIn(matrixChat!); }) @@ -584,7 +583,7 @@ describe("loading:", function () { access_token: "access_token", }); - return httpBackend.flush(); + return httpBackend.flush(undefined); }) .then(() => { // at this point, MatrixChat should fire onTokenLoginCompleted, which @@ -607,11 +606,11 @@ describe("loading:", function () { // check that we have a Login component, send a 'user:pass' login, // and await the HTTP requests. - async function completeLogin(matrixChat: RenderResult): Promise { + async function completeLogin(matrixChat: RenderResult): Promise { // 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(); // We already would have tried the GET /login request + httpBackend.flush(undefined); // We already would have tried the GET /login request // Give the component some time to finish processing the login flows before // continuing. @@ -635,7 +634,7 @@ describe("loading:", function () { fireEvent.click(screen.getByText("Sign in", { selector: ".mx_Login_submit" })); return httpBackend - .flush() + .flush(undefined) .then(() => { // Wait for another trip around the event loop for the UI to update return sleep(1); @@ -656,11 +655,11 @@ async function assertAtLoadingSpinner(): Promise { await screen.findByRole("progressbar"); } -async function awaitLoggedIn(matrixChat: RenderResult): Promise { +async function awaitLoggedIn(matrixChat: RenderResult): Promise { if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in return new Promise((resolve) => { - const onAction = ({ action }): void => { + const onAction = ({ action }: ActionPayload): void => { if (action !== "on_logged_in") { return; } @@ -673,19 +672,19 @@ async function awaitLoggedIn(matrixChat: RenderResult): Promise): Promise { +async function awaitRoomView(matrixChat?: RenderResult): Promise { await waitFor(() => matrixChat?.container.querySelector(".mx_RoomView")); } -async function awaitLoginComponent(matrixChat?: RenderResult): Promise { +async function awaitLoginComponent(matrixChat?: RenderResult): Promise { await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage")); } -async function awaitWelcomeComponent(matrixChat?: RenderResult): Promise { +async function awaitWelcomeComponent(matrixChat?: RenderResult): Promise { await waitFor(() => matrixChat?.container.querySelector(".mx_Welcome")); } -function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise { +function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise { dis.dispatch({ action: "start_login" }); return awaitLoginComponent(matrixChat); } diff --git a/test/unit-tests/favicon-test.ts b/test/unit-tests/favicon-test.ts index 8e2f45864..4e9739db0 100644 --- a/test/unit-tests/favicon-test.ts +++ b/test/unit-tests/favicon-test.ts @@ -29,7 +29,7 @@ describe("Favicon", () => { it("should create a link element if one doesn't yet exist", () => { const favicon = new Favicon(); expect(favicon).toBeTruthy(); - const link = window.document.querySelector("link"); + const link = window.document.querySelector("link")!; expect(link.rel).toContain("icon"); }); diff --git a/test/unit-tests/vector/getconfig-test.ts b/test/unit-tests/vector/getconfig-test.ts index aac830457..753785417 100644 --- a/test/unit-tests/vector/getconfig-test.ts +++ b/test/unit-tests/vector/getconfig-test.ts @@ -21,7 +21,6 @@ import { getVectorConfig } from "../../../src/vector/getconfig"; fetchMock.config.overwriteRoutes = true; describe("getVectorConfig()", () => { - const prevDocumentDomain = document.domain; const elementDomain = "app.element.io"; const now = 1234567890; const specificConfig = { @@ -32,7 +31,10 @@ describe("getVectorConfig()", () => { }; beforeEach(() => { - document.domain = elementDomain; + Object.defineProperty(window, "location", { + value: { href: `https://${elementDomain}`, hostname: elementDomain }, + writable: true, + }); // stable value for cachebuster jest.spyOn(Date, "now").mockReturnValue(now); @@ -41,7 +43,6 @@ describe("getVectorConfig()", () => { }); afterAll(() => { - document.domain = prevDocumentDomain; jest.spyOn(Date, "now").mockRestore(); }); @@ -107,6 +108,6 @@ describe("getVectorConfig()", () => { // We can't assert it'll be a SyntaxError as node-fetch behaves differently // https://github.com/wheresrhys/fetch-mock/issues/270 - await expect(getVectorConfig()).rejects.toThrow("Unexpected token } in JSON at position 19"); + await expect(getVectorConfig()).rejects.toThrow("in JSON at position 19"); }); }); diff --git a/test/unit-tests/vector/platform/ElectronPlatform-test.ts b/test/unit-tests/vector/platform/ElectronPlatform-test.ts index 24de2e180..653c5cf3f 100644 --- a/test/unit-tests/vector/platform/ElectronPlatform-test.ts +++ b/test/unit-tests/vector/platform/ElectronPlatform-test.ts @@ -47,8 +47,7 @@ describe("ElectronPlatform", () => { beforeEach(() => { window.electron = mockElectron; jest.clearAllMocks(); - delete window.navigator; - window.navigator = { userAgent: defaultUserAgent } as unknown as Navigator; + Object.defineProperty(window, "navigator", { value: { userAgent: defaultUserAgent }, writable: true }); }); const getElectronEventHandlerCall = (eventType: string): [type: string, handler: Function] | undefined => @@ -56,7 +55,7 @@ describe("ElectronPlatform", () => { it("flushes rageshake before quitting", () => { new ElectronPlatform(); - const [event, handler] = getElectronEventHandlerCall("before-quit"); + const [event, handler] = getElectronEventHandlerCall("before-quit")!; // correct event bound expect(event).toBeTruthy(); @@ -68,7 +67,7 @@ describe("ElectronPlatform", () => { it("dispatches view settings action on preferences event", () => { new ElectronPlatform(); - const [event, handler] = getElectronEventHandlerCall("preferences"); + const [event, handler] = getElectronEventHandlerCall("preferences")!; // correct event bound expect(event).toBeTruthy(); @@ -80,7 +79,7 @@ describe("ElectronPlatform", () => { describe("updates", () => { it("dispatches on check updates action", () => { new ElectronPlatform(); - const [event, handler] = getElectronEventHandlerCall("check_updates"); + const [event, handler] = getElectronEventHandlerCall("check_updates")!; // correct event bound expect(event).toBeTruthy(); @@ -93,7 +92,7 @@ describe("ElectronPlatform", () => { it("dispatches on check updates action when update not available", () => { new ElectronPlatform(); - const [, handler] = getElectronEventHandlerCall("check_updates"); + const [, handler] = getElectronEventHandlerCall("check_updates")!; handler({}, false); expect(dispatchSpy).toHaveBeenCalledWith({ @@ -138,8 +137,7 @@ describe("ElectronPlatform", () => { ["Mozilla/5.0 (X11; SunOS i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: SunOS"], ["custom user agent", "Element Desktop: Unknown"], ])("%s = %s", (userAgent, result) => { - delete window.navigator; - window.navigator = { userAgent } as unknown as Navigator; + Object.defineProperty(window, "navigator", { value: { userAgent }, writable: true }); const platform = new ElectronPlatform(); expect(platform.getDefaultDeviceDisplayName()).toEqual(result); }); diff --git a/test/unit-tests/vector/platform/WebPlatform-test.ts b/test/unit-tests/vector/platform/WebPlatform-test.ts index 6f7c332c2..af35137f9 100644 --- a/test/unit-tests/vector/platform/WebPlatform-test.ts +++ b/test/unit-tests/vector/platform/WebPlatform-test.ts @@ -33,7 +33,6 @@ describe("WebPlatform", () => { }); it("registers service worker", () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - mocking readonly object navigator.serviceWorker = { register: jest.fn() }; new WebPlatform(); @@ -41,10 +40,7 @@ describe("WebPlatform", () => { }); it("should call reload on window location object", () => { - delete window.location; - window.location = { - reload: jest.fn(), - } as unknown as Location; + Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true }); const platform = new WebPlatform(); expect(window.location.reload).not.toHaveBeenCalled(); @@ -53,10 +49,7 @@ describe("WebPlatform", () => { }); it("should call reload to install update", () => { - delete window.location; - window.location = { - reload: jest.fn(), - } as unknown as Location; + Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true }); const platform = new WebPlatform(); expect(window.location.reload).not.toHaveBeenCalled(); @@ -73,10 +66,8 @@ describe("WebPlatform", () => { "develop.element.io: Chrome on macOS", ], ])("%s & %s = %s", (url, userAgent, result) => { - delete window.navigator; - window.navigator = { userAgent } as unknown as Navigator; - delete window.location; - window.location = { href: url } as unknown as Location; + Object.defineProperty(window, "navigator", { value: { userAgent }, writable: true }); + Object.defineProperty(window, "location", { value: { href: url }, writable: true }); const platform = new WebPlatform(); expect(platform.getDefaultDeviceDisplayName()).toEqual(result); }); @@ -88,14 +79,12 @@ describe("WebPlatform", () => { permission: "notGranted", }; beforeEach(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.Notification = mockNotification; mockNotification.permission = "notGranted"; }); it("supportsNotifications returns false when platform does not support notifications", () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.Notification = undefined; expect(new WebPlatform().supportsNotifications()).toBe(false); @@ -132,7 +121,8 @@ describe("WebPlatform", () => { }); afterAll(() => { - process.env.VERSION = envVersion; + // @ts-ignore + WebPlatform.VERSION = envVersion; }); it("should return true from canSelfUpdate()", async () => { @@ -142,18 +132,21 @@ describe("WebPlatform", () => { }); it("getAppVersion returns normalized app version", async () => { - process.env.VERSION = prodVersion; + // @ts-ignore + WebPlatform.VERSION = prodVersion; const platform = new WebPlatform(); const version = await platform.getAppVersion(); expect(version).toEqual(prodVersion); - process.env.VERSION = `v${prodVersion}`; + // @ts-ignore + WebPlatform.VERSION = `v${prodVersion}`; const version2 = await platform.getAppVersion(); // v prefix removed expect(version2).toEqual(prodVersion); - process.env.VERSION = `version not like semver`; + // @ts-ignore + WebPlatform.VERSION = `version not like semver`; const notSemverVersion = await platform.getAppVersion(); expect(notSemverVersion).toEqual(`version not like semver`); }); @@ -163,7 +156,8 @@ describe("WebPlatform", () => { "should return not available and call showNoUpdate when current version " + "matches most recent version", async () => { - process.env.VERSION = prodVersion; + // @ts-ignore + WebPlatform.VERSION = prodVersion; fetchMock.getOnce("/version", prodVersion); const platform = new WebPlatform(); @@ -178,7 +172,8 @@ describe("WebPlatform", () => { ); it("should strip v prefix from versions before comparing", async () => { - process.env.VERSION = prodVersion; + // @ts-ignore + WebPlatform.VERSION = prodVersion; fetchMock.getOnce("/version", `v${prodVersion}`); const platform = new WebPlatform(); @@ -195,7 +190,8 @@ describe("WebPlatform", () => { it( "should return ready and call showUpdate when current version " + "differs from most recent version", async () => { - process.env.VERSION = "0.0.0"; // old version + // @ts-ignore + WebPlatform.VERSION = "0.0.0"; // old version fetchMock.getOnce("/version", prodVersion); const platform = new WebPlatform(); @@ -210,7 +206,8 @@ describe("WebPlatform", () => { ); it("should return ready without showing update when user registered in last 24", async () => { - process.env.VERSION = "0.0.0"; // old version + // @ts-ignore + WebPlatform.VERSION = "0.0.0"; // old version jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(true); fetchMock.getOnce("/version", prodVersion); const platform = new WebPlatform(); diff --git a/test/unit-tests/vector/routing-test.ts b/test/unit-tests/vector/routing-test.ts index 28676a1c8..3b8df5302 100644 --- a/test/unit-tests/vector/routing-test.ts +++ b/test/unit-tests/vector/routing-test.ts @@ -18,24 +18,28 @@ import { onNewScreen } from "../../../src/vector/routing"; describe("onNewScreen", () => { it("should replace history if stripping via fields", () => { - delete window.location; - window.location = { - hash: "#/room/!room:server?via=abc", - replace: jest.fn(), - assign: jest.fn(), - } as unknown as Location; + Object.defineProperty(window, "location", { + value: { + hash: "#/room/!room:server?via=abc", + replace: jest.fn(), + assign: jest.fn(), + }, + writable: true, + }); onNewScreen("room/!room:server"); expect(window.location.assign).not.toHaveBeenCalled(); expect(window.location.replace).toHaveBeenCalled(); }); it("should not replace history if changing rooms", () => { - delete window.location; - window.location = { - hash: "#/room/!room1:server?via=abc", - replace: jest.fn(), - assign: jest.fn(), - } as unknown as Location; + Object.defineProperty(window, "location", { + value: { + hash: "#/room/!room1:server?via=abc", + replace: jest.fn(), + assign: jest.fn(), + }, + writable: true, + }); onNewScreen("room/!room2:server"); expect(window.location.assign).toHaveBeenCalled(); expect(window.location.replace).not.toHaveBeenCalled(); diff --git a/test/unit-tests/vector/url_utils-test.ts b/test/unit-tests/vector/url_utils-test.ts index 374b6c6d2..4b907de55 100644 --- a/test/unit-tests/vector/url_utils-test.ts +++ b/test/unit-tests/vector/url_utils-test.ts @@ -17,7 +17,6 @@ limitations under the License. import { parseQsFromFragment, parseQs } from "../../../src/vector/url_utils"; describe("url_utils.ts", function () { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const location: Location = { hash: "", diff --git a/yarn.lock b/yarn.lock index 2c25279fd..da364f632 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1544,9 +1544,9 @@ resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-2.0.0.tgz#913eb5faa5d43c7a4ee9bda68de1aa1dcc49a11d" integrity sha512-xRYFwsf6Jx7KTCpwU91mVhPA8q/c+oOVyK98NnexyK/IcQS7BMFAns5GZX9b6ZEy38u30KoxeN6mxvxi+ysQgg== -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": - version "3.2.12" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": + 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" "@matrix-org/react-sdk-module-api@^0.0.4": version "0.0.4" @@ -2104,6 +2104,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jitsi-meet@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/jitsi-meet/-/jitsi-meet-2.0.2.tgz#4670e6dd47f2762cda5af53b73ab0a6e39ec0205" + integrity sha512-rnb5znCdZs7T2VgA16wyu5UHIbq+WR2HH233GPkS6HQ9m7Sh5jiXChZ41jo0tCTtNuSCziPIb5sI+6OPKK8h+Q== + "@types/jsdom@^20.0.0": version "20.0.1" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808"