Conform more of the codebase with noImplicitAny and strictNullChecks (#25174

* Conform more of the codebase with `noImplicitAny` and `strictNullChecks`

* Fix tests

* Update src/vector/app.tsx
This commit is contained in:
Michael Telatynski 2023-04-25 09:36:17 +01:00 committed by GitHub
parent a2da1eb79d
commit f5b8bccb65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 265 additions and 211 deletions

View File

@ -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",
},
},
],

View File

@ -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",

29
src/@types/jitsi-meet.d.ts vendored Normal file
View File

@ -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;

View File

@ -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<IProps> = ({ onAccept }) => {
android = [];
}
let mobileHeader = <h2 id="step2_heading">{_t("Use %(brand)s on mobile", { brand })}</h2>;
let mobileHeader: ReactNode = <h2 id="step2_heading">{_t("Use %(brand)s on mobile", { brand })}</h2>;
if (!android.length && !ios) {
mobileHeader = null;
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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<ReactElement> {
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 <React.Fragment />;
}
const defaultDeviceName = snakedConfig.get("default_device_display_name") ?? platform.getDefaultDeviceDisplayName();
const defaultDeviceName =
snakedConfig.get("default_device_display_name") ?? platform?.getDefaultDeviceDisplayName();
return (
<MatrixChat
@ -146,7 +144,7 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
}
async function verifyServerConfig(): Promise<IConfigOptions> {
let validatedConfig;
let validatedConfig: ValidatedServerConfig;
try {
logger.log("Verifying homeserver configuration");
@ -197,7 +195,7 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
}
}
let discoveryResult = null;
let discoveryResult: ClientConfig | undefined;
if (wkConfig) {
logger.log("Config uses a default_server_config - validating object");
discoveryResult = await AutoDiscovery.fromDiscoveryConfig(wkConfig);

View File

@ -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<IConfigOptions> {
export async function getVectorConfig(relativeLocation = ""): Promise<IConfigOptions | undefined> {
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<IConfigOpt
}
}
async function getConfig(configJsonFilename: string): Promise<IConfigOptions> {
async function getConfig(configJsonFilename: string): Promise<IConfigOptions | undefined> {
const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString());
const res = await fetch(url, {

View File

@ -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<keyof ModernizrStatic>;
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);
});

View File

@ -67,7 +67,7 @@ export async function loadConfig(): Promise<void> {
// 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<void> {
export async function loadLanguage(): Promise<void> {
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<voi
);
}
export async function showIncompatibleBrowser(onAccept): Promise<void> {
export async function showIncompatibleBrowser(onAccept: () => void): Promise<void> {
const CompatibilityView = (
await import(
/* webpackChunkName: "compatibility-view" */

View File

@ -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<string | void> => {
@ -102,12 +110,12 @@ const setupCompleted = (async (): Promise<string | void> => {
}
// Set this up as early as possible because Element will be hitting it almost immediately.
let widgetApiReady: Promise<void>;
let widgetApiReady: Promise<void> | undefined;
if (parentUrl && widgetId) {
const parentOrigin = new URL(qsParam("parentUrl")).origin;
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
widgetApiReady = new Promise<void>((resolve) => widgetApi.once("ready", resolve));
widgetApiReady = new Promise<void>((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<string | void> => {
action: WidgetApiAction,
handler: (request: IWidgetApiRequestData) => Promise<void>,
): void => {
widgetApi.on(`action:${action}`, async (ev: CustomEvent<IWidgetApiRequest>) => {
widgetApi!.on(`action:${action}`, async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
await setupCompleted;
@ -138,7 +146,7 @@ const setupCompleted = (async (): Promise<string | void> => {
}
}
await widgetApi.transport.reply(ev.detail, response);
await widgetApi!.transport.reply(ev.detail, response);
});
};
@ -149,7 +157,7 @@ const setupCompleted = (async (): Promise<string | void> => {
if (force === true) {
meetApi?.dispose();
notifyHangup();
meetApi = null;
meetApi = undefined;
closeConference();
} else {
meetApi?.executeCommand("hangup");
@ -212,9 +220,10 @@ const setupCompleted = (async (): Promise<string | void> => {
// We've reached the point where we have to wait for the config, so do that then parse it.
const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{});
const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {};
skipOurWelcomeScreen =
new SnakedObject<IConfigOptions["jitsi_widget"]>(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<string | void> => {
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<void> => joinConference();
document.getElementById("joinButton")!.onclick = (): Promise<void> => 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<void> {
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<void> {
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<ExternalAPIEventCallbacks["errorOccurred"]>[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);

View File

@ -31,17 +31,17 @@ function renderConfigError(message: string): void {
}
async function initPage(): Promise<void> {
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<void> {
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<void> {
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;
}
}

View File

@ -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<IConfigOptions> {
public async getConfig(): Promise<IConfigOptions | undefined> {
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 {

View File

@ -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<any>();
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;
}

View File

@ -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<IEventAndProfile[]> {
public async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> {
return this.ipc.call("loadFileEvents", args);
}

View File

@ -30,7 +30,7 @@ import Favicon from "../../favicon";
export default abstract class VectorBasePlatform extends BasePlatform {
protected _favicon: Favicon;
public async getConfig(): Promise<IConfigOptions> {
public async getConfig(): Promise<IConfigOptions | undefined> {
return getVectorConfig();
}

View File

@ -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<string> {
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<UpdateStatus> => {
return this.getMostRecentVersion().then(
(mostRecentVersion) => {
const currentVersion = getNormalizedAppVersion(process.env.VERSION);
const currentVersion = getNormalizedAppVersion(WebPlatform.VERSION);
if (currentVersion !== mostRecentVersion) {
if (this.shouldShowUpdate(mostRecentVersion)) {

View File

@ -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);

View File

@ -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<any, any, any> | undefined;
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;
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(
<MatrixChat
onNewScreen={onNewScreen}
config={config}
config={config!}
serverConfig={config.validated_server_config}
realQueryParams={params}
startingFragmentQueryParams={fragParts.params}
enableGuest={true}
onTokenLoginCompleted={resolve}
initialScreenAfterLogin={getScreenFromLocation(windowLocation)}
initialScreenAfterLogin={getScreenFromLocation(windowLocation!)}
makeRegistrationUrl={(): string => {
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<any> {
let syncRequest = null;
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: ["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<any, any, any>): Promise<void> {
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(); // 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<void> {
await screen.findByRole("progressbar");
}
async function awaitLoggedIn(matrixChat: RenderResult<any, any, any>): Promise<void> {
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 }): void => {
const onAction = ({ action }: ActionPayload): void => {
if (action !== "on_logged_in") {
return;
}
@ -673,19 +672,19 @@ async function awaitLoggedIn(matrixChat: RenderResult<any, any, any>): Promise<v
});
}
async function awaitRoomView(matrixChat?: RenderResult<any, any, any>): Promise<void> {
async function awaitRoomView(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_RoomView"));
}
async function awaitLoginComponent(matrixChat?: RenderResult<any, any, any>): Promise<void> {
async function awaitLoginComponent(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_AuthPage"));
}
async function awaitWelcomeComponent(matrixChat?: RenderResult<any, any, any>): Promise<void> {
async function awaitWelcomeComponent(matrixChat?: RenderResult): Promise<void> {
await waitFor(() => matrixChat?.container.querySelector(".mx_Welcome"));
}
function moveFromWelcomeToLogin(matrixChat?: RenderResult<any, any, any>): Promise<void> {
function moveFromWelcomeToLogin(matrixChat?: RenderResult): Promise<void> {
dis.dispatch({ action: "start_login" });
return awaitLoginComponent(matrixChat);
}

View File

@ -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");
});

View File

@ -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");
});
});

View File

@ -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);
});

View File

@ -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();

View File

@ -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();

View File

@ -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: "",

View File

@ -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"