Force verification even for refreshed clients (#44)
* Force verification even for refreshed cients Set a flag on login to remember that the device needs to be verified so that we don't forget if the user refreshes the page, but still allow user with an existing unverified session to stay logged in. * Hopefully make matrixchat tests pass? Much, much tweaking to make the matrixchat tests pass again. Should hopefully make them a bit more solid in general with judicious use of waitFor rather than flushPromises(). Also lots of fun to stop the state bleeding between tests. * Manual yarn.lock manipulation to hopefully resolve infinite package sadness * Make final test pass(?) Mock out the createClient method to return the same client, because we've mocked the peg to always return that client, so if we let the code make another one having still overridden the peg, everything becomes cursed. Also mock out the autodiscovery stuff rather than relying on fetch-mock. * another waitFor * death to flushPromises * Put the logged in dispatch back Actually it breaks all sorts of other things too, having fixed all the MatrixChat tests (although this is useful anyway). * Try displaying the screen in onClientStarted instead * Put post login screen back in logged in but move ready transition to avoid flash of main UI * Rejig more in the hope it does the right thing * Make hook work before push rules are fetched * Add test for unskippable verification * Add test for use case selection * Fix test * Add playwright test for unskippable verification * Remove console log * Add log message to log line * Add tsdoc * Use useTypedEventEmitter * Remove commented code * Use catch instead of empty then on unawaited promises or in one case just await it because the caller was async anyway * Add new mock
This commit is contained in:
parent
5d6c19c939
commit
67cb8b7590
11 changed files with 392 additions and 160 deletions
|
@ -198,6 +198,7 @@
|
|||
"axe-core": "4.10.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^9.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
|
|
@ -6,20 +6,85 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page } from "playwright-core";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { doTokenRegistration } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
|
||||
const username = "user1234";
|
||||
const password = "p4s5W0rD";
|
||||
|
||||
// Pre-generated dummy signing keys to create an account that has signing keys set.
|
||||
// Note the signatures are specific to the username and must be valid or the HS will reject the keys.
|
||||
const DEVICE_SIGNING_KEYS_BODY = {
|
||||
master_key: {
|
||||
keys: {
|
||||
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg": "6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg",
|
||||
},
|
||||
signatures: {
|
||||
"@user1234:localhost": {
|
||||
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
|
||||
"mvwqsYiGa2gPH6ueJsiJnceHMrZhf1pqIMGxkvKisN3ucz8sU7LwyzndbYaLkUKEDx1JuOKFfZ9Mb3mqc7PMBQ",
|
||||
"ed25519:SRHVWTNVBH":
|
||||
"HVGmVIzsJe3d+Un/6S9tXPsU7YA8HjZPdxogVzdjEFIU8OjLyElccvjupow0rVWgkEqU8sO21LIHw9cWRZEmDw",
|
||||
},
|
||||
},
|
||||
usage: ["master"],
|
||||
user_id: "@user1234:localhost",
|
||||
},
|
||||
self_signing_key: {
|
||||
keys: {
|
||||
"ed25519:eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8": "eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8",
|
||||
},
|
||||
signatures: {
|
||||
"@user1234:localhost": {
|
||||
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
|
||||
"M2rt5xs+23egbVUwUcZuU7pMpn0chBNC5rpdyZGayfU3FDlx1DbopbakIcl5v4uOSGMbqUotyzkE6CchB+dgDw",
|
||||
},
|
||||
},
|
||||
usage: ["self_signing"],
|
||||
user_id: "@user1234:localhost",
|
||||
},
|
||||
user_signing_key: {
|
||||
keys: {
|
||||
"ed25519:h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4": "h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4",
|
||||
},
|
||||
signatures: {
|
||||
"@user1234:localhost": {
|
||||
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
|
||||
"5ZMJ7SG2qr76vU2nITKap88AxLZ/RZQmF/mBcAcVZ9Bknvos3WQp8qN9jKuiqOHCq/XpPORA6XBmiDIyPqTFAA",
|
||||
},
|
||||
},
|
||||
usage: ["user_signing"],
|
||||
user_id: "@user1234:localhost",
|
||||
},
|
||||
auth: {
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: "@user1234:localhost" },
|
||||
password: password,
|
||||
},
|
||||
};
|
||||
|
||||
async function login(page: Page, homeserver: HomeserverInstance) {
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
}
|
||||
|
||||
test.describe("Login", () => {
|
||||
test.describe("Password login", () => {
|
||||
test.use({ startHomeserverOpts: "consent" });
|
||||
|
||||
const username = "user1234";
|
||||
const password = "p4s5W0rD";
|
||||
let creds: Credentials;
|
||||
|
||||
test.beforeEach(async ({ homeserver }) => {
|
||||
await homeserver.registerUser(username, password);
|
||||
creds = await homeserver.registerUser(username, password);
|
||||
});
|
||||
|
||||
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
|
||||
|
@ -65,17 +130,97 @@ test.describe("Login", () => {
|
|||
|
||||
test("Follows the original link after login", async ({ page, homeserver }) => {
|
||||
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
await login(page, homeserver);
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
||||
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("verification after login", () => {
|
||||
test("Shows verification prompt after login if signing keys are set up, skippable by default", async ({
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
}
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("with force_verification off", () => {
|
||||
test.use({
|
||||
config: {
|
||||
force_verification: false,
|
||||
},
|
||||
});
|
||||
|
||||
test("Shows skippable verification prompt after login if signing keys are set up", async ({
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
}) => {
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
}
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("with force_verification on", () => {
|
||||
test.use({
|
||||
config: {
|
||||
force_verification: true,
|
||||
},
|
||||
});
|
||||
|
||||
test("Shows unskippable verification prompt after login if signing keys are set up", async ({
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
}) => {
|
||||
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
}
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
|
||||
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
|
||||
expect(h1.locator(".mx_CompleteSecurity_skip")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
||||
|
|
|
@ -824,6 +824,8 @@ async function doSetLoggedIn(
|
|||
}
|
||||
checkSessionLock();
|
||||
|
||||
// We are now logged in, so fire this. We have yet to start the client but the
|
||||
// client_started dispatch is for that.
|
||||
dis.fire(Action.OnLoggedIn);
|
||||
|
||||
const clientPegOpts: MatrixClientPegAssignOpts = {};
|
||||
|
@ -846,6 +848,12 @@ async function doSetLoggedIn(
|
|||
// Run the migrations after the MatrixClientPeg has been assigned
|
||||
SettingsStore.runMigrations(isFreshLogin);
|
||||
|
||||
if (isFreshLogin && !credentials.guest) {
|
||||
// For newly registered users, set a flag so that we force them to verify,
|
||||
// (we don't want to force users with existing sessions to verify though)
|
||||
localStorage.setItem("must_verify_device", "true");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
|||
integrations_rest_url: "https://scalar.vector.im/api",
|
||||
uisi_autorageshake_app: "element-auto-uisi",
|
||||
show_labs_settings: false,
|
||||
force_verification: false,
|
||||
|
||||
jitsi: {
|
||||
preferred_domain: "meet.element.io",
|
||||
|
|
|
@ -166,6 +166,12 @@ interface IProps {
|
|||
initialScreenAfterLogin?: IScreen;
|
||||
// displayname, if any, to set on the device when logging in/registering.
|
||||
defaultDeviceDisplayName?: string;
|
||||
|
||||
// Used by tests, this function is called when session initialisation starts
|
||||
// with a promise that resolves or rejects once the initialiation process
|
||||
// has finished, so that tests can wait for this to avoid them executing over
|
||||
// each other.
|
||||
initPromiseCallback?: (p: Promise<void>) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -309,7 +315,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
* Kick off a call to {@link initSession}, and handle any errors
|
||||
*/
|
||||
private startInitSession = (): void => {
|
||||
this.initSession().catch((err) => {
|
||||
const initProm = this.initSession();
|
||||
if (this.props.initPromiseCallback) {
|
||||
this.props.initPromiseCallback(initProm);
|
||||
}
|
||||
|
||||
initProm.catch((err) => {
|
||||
// TODO: show an error screen, rather than a spinner of doom
|
||||
logger.error("Error initialising Matrix session", err);
|
||||
});
|
||||
|
@ -881,7 +892,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
break;
|
||||
case "client_started":
|
||||
this.onClientStarted();
|
||||
// No need to make this handler async to wait for the result of this
|
||||
this.onClientStarted().catch((e) => {
|
||||
logger.error("Exception in onClientStarted", e);
|
||||
});
|
||||
break;
|
||||
case "send_event":
|
||||
this.onSendEvent(payload.room_id, payload.event);
|
||||
|
@ -1320,6 +1334,25 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user must go through the device verification process before they
|
||||
* can use the app.
|
||||
* @returns true if the user must verify
|
||||
*/
|
||||
private async shouldForceVerification(): Promise<boolean> {
|
||||
if (!SdkConfig.get("force_verification")) return false;
|
||||
const mustVerifyFlag = localStorage.getItem("must_verify_device");
|
||||
if (!mustVerifyFlag) return false;
|
||||
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
if (client.isGuest()) return false;
|
||||
|
||||
const crypto = client.getCrypto();
|
||||
const crossSigningReady = await crypto?.isCrossSigningReady();
|
||||
|
||||
return !crossSigningReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
|
@ -1328,30 +1361,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.themeWatcher.recheck();
|
||||
StorageManager.tryPersistStorage();
|
||||
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered() && SettingsStore.getValue("FTUE.useCaseSelection") === null) {
|
||||
this.setStateForNewView({ view: Views.USE_CASE_SELECTION });
|
||||
|
||||
// Listen to changes in settings and hide the use case screen if appropriate - this is necessary because
|
||||
// account settings can still be changing at this point in app init (due to the initial sync being cached,
|
||||
// then subsequent syncs being received from the server)
|
||||
//
|
||||
// This seems unlikely for something that should happen directly after registration, but if a user does
|
||||
// their initial login on another device/browser than they registered on, we want to avoid asking this
|
||||
// question twice
|
||||
//
|
||||
// initPosthogAnalyticsToast pioneered this technique, we’re just reusing it here.
|
||||
SettingsStore.watchSetting(
|
||||
"FTUE.useCaseSelection",
|
||||
null,
|
||||
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
|
||||
if (newValue !== null && this.state.view === Views.USE_CASE_SELECTION) {
|
||||
this.onShowPostLoginScreen();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return this.onShowPostLoginScreen();
|
||||
}
|
||||
await this.onShowPostLoginScreen();
|
||||
}
|
||||
|
||||
private async onShowPostLoginScreen(useCase?: UseCase): Promise<void> {
|
||||
|
@ -1557,9 +1567,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setState({
|
||||
ready: true,
|
||||
});
|
||||
});
|
||||
|
||||
cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
|
||||
|
@ -1702,9 +1709,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
* setting up anything that requires the client to be started.
|
||||
* @private
|
||||
*/
|
||||
private onClientStarted(): void {
|
||||
private async onClientStarted(): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
|
||||
const shouldForceVerification = await this.shouldForceVerification();
|
||||
// XXX: Don't replace the screen if it's already one of these: postLoginSetup
|
||||
// changes to these screens in certain circumstances so we shouldn't clobber it.
|
||||
// We should probably have one place where we decide what the next screen is after
|
||||
// login.
|
||||
if (![Views.COMPLETE_SECURITY, Views.E2E_SETUP].includes(this.state.view)) {
|
||||
if (shouldForceVerification) {
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
}
|
||||
}
|
||||
|
||||
if (cli.isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
|
||||
cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
|
||||
|
@ -1722,6 +1740,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (PosthogAnalytics.instance.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
|
||||
this.initPosthogAnalyticsToast();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
ready: true,
|
||||
});
|
||||
}
|
||||
|
||||
public showScreen(screen: string, params?: { [key: string]: any }): void {
|
||||
|
@ -2013,7 +2035,33 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// complete security / e2e setup has finished
|
||||
private onCompleteSecurityE2eSetupFinished = (): void => {
|
||||
this.onLoggedIn();
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered() && SettingsStore.getValue("FTUE.useCaseSelection") === null) {
|
||||
this.setStateForNewView({ view: Views.USE_CASE_SELECTION });
|
||||
|
||||
// Listen to changes in settings and hide the use case screen if appropriate - this is necessary because
|
||||
// account settings can still be changing at this point in app init (due to the initial sync being cached,
|
||||
// then subsequent syncs being received from the server)
|
||||
//
|
||||
// This seems unlikely for something that should happen directly after registration, but if a user does
|
||||
// their initial login on another device/browser than they registered on, we want to avoid asking this
|
||||
// question twice
|
||||
//
|
||||
// initPosthogAnalyticsToast pioneered this technique, we’re just reusing it here.
|
||||
SettingsStore.watchSetting(
|
||||
"FTUE.useCaseSelection",
|
||||
null,
|
||||
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
|
||||
if (newValue !== null && this.state.view === Views.USE_CASE_SELECTION) {
|
||||
this.onShowPostLoginScreen();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// This is async but we makign this function async to wait for it isn't useful
|
||||
this.onShowPostLoginScreen().catch((e) => {
|
||||
logger.error("Exception showing post-login screen", e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private getFragmentAfterLogin(): string {
|
||||
|
|
|
@ -83,7 +83,7 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
const forceVerification = SdkConfig.get("force_verification") ?? false;
|
||||
const forceVerification = SdkConfig.get("force_verification");
|
||||
|
||||
let skipButton;
|
||||
if (!forceVerification && (phase === Phase.Intro || phase === Phase.ConfirmReset)) {
|
||||
|
|
|
@ -47,6 +47,7 @@ export default class Welcome extends React.PureComponent<IProps> {
|
|||
className={classNames("mx_Welcome", {
|
||||
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
|
||||
})}
|
||||
data-testid="mx_welcome_screen"
|
||||
>
|
||||
<EmbeddedPage className="mx_WelcomePage" url={pageUrl} replaceMap={replaceMap} />
|
||||
<LanguageSelector />
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Notifier, NotifierEvent } from "../Notifier";
|
|||
import DMRoomMap from "../utils/DMRoomMap";
|
||||
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||
import { useSettingValue } from "./useSettings";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
import { useEventEmitter, useTypedEventEmitter } from "./useEventEmitter";
|
||||
|
||||
export interface UserOnboardingContext {
|
||||
hasAvatar: boolean;
|
||||
|
@ -77,14 +77,27 @@ function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: Matri
|
|||
}
|
||||
|
||||
function useShowNotificationsPrompt(): boolean {
|
||||
const [value, setValue] = useState<boolean>(Notifier.shouldShowPrompt());
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const [value, setValue] = useState<boolean>(client.pushRules ? Notifier.shouldShowPrompt() : true);
|
||||
|
||||
const updateValue = useCallback(() => {
|
||||
setValue(client.pushRules ? Notifier.shouldShowPrompt() : true);
|
||||
}, [client]);
|
||||
|
||||
useEventEmitter(Notifier, NotifierEvent.NotificationHiddenChange, () => {
|
||||
setValue(Notifier.shouldShowPrompt());
|
||||
updateValue();
|
||||
});
|
||||
|
||||
const setting = useSettingValue("notificationsEnabled");
|
||||
useEffect(() => {
|
||||
setValue(Notifier.shouldShowPrompt());
|
||||
}, [setting]);
|
||||
updateValue();
|
||||
}, [setting, updateValue]);
|
||||
|
||||
// shouldShowPrompt is dependent on the client having push rules. There isn't an event for the client
|
||||
// fetching its push rules, but we'll know it has them by the time it sync, so we update this on sync.
|
||||
useTypedEventEmitter(client, ClientEvent.Sync, updateValue);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// fake-indexeddb needs this and the tests crash without it
|
||||
// https://github.com/dumbmatter/fakeIndexedDB?tab=readme-ov-file#jsdom-often-used-with-jest
|
||||
import "core-js/stable/structured-clone";
|
||||
import "fake-indexeddb/auto";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { fireEvent, render, RenderResult, screen, waitFor, within } from "@testing-library/react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
@ -17,7 +21,7 @@ import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { OidcError } from "matrix-js-sdk/src/oidc/error";
|
||||
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
|
||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||
import { defer, IDeferred, sleep } from "matrix-js-sdk/src/utils";
|
||||
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import MatrixChat from "../../../src/components/structures/MatrixChat";
|
||||
|
@ -51,11 +55,13 @@ import * as Lifecycle from "../../../src/Lifecycle";
|
|||
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../src/BasePlatform";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
||||
import { MatrixClientPeg, MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||
import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
|
||||
import { UIFeature } from "../../../src/settings/UIFeature";
|
||||
import AutoDiscoveryUtils from "../../../src/utils/AutoDiscoveryUtils";
|
||||
import { ValidatedServerConfig } from "../../../src/utils/ValidatedServerConfig";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||
completeAuthorizationCodeGrant: jest.fn(),
|
||||
|
@ -72,12 +78,17 @@ describe("<MatrixChat />", () => {
|
|||
const userId = "@alice:server.org";
|
||||
const deviceId = "qwertyui";
|
||||
const accessToken = "abc123";
|
||||
const refreshToken = "def456";
|
||||
let bootstrapDeferred: IDeferred<void>;
|
||||
// reused in createClient mock below
|
||||
const getMockClientMethods = () => ({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }),
|
||||
startClient: jest.fn(),
|
||||
startClient: function () {
|
||||
// @ts-ignore
|
||||
this.emit(ClientEvent.Sync, SyncState.Prepared, null);
|
||||
},
|
||||
stopClient: jest.fn(),
|
||||
setCanResetTimelineCallback: jest.fn(),
|
||||
isInitialSyncComplete: jest.fn(),
|
||||
|
@ -110,13 +121,24 @@ describe("<MatrixChat />", () => {
|
|||
getAccountData: jest.fn(),
|
||||
doesServerSupportUnstableFeature: jest.fn(),
|
||||
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
|
||||
getProfileInfo: jest.fn(),
|
||||
getProfileInfo: jest.fn().mockResolvedValue({
|
||||
displayname: "Ernie",
|
||||
}),
|
||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
userHasCrossSigningKeys: jest.fn(),
|
||||
setGlobalBlacklistUnverifiedDevices: jest.fn(),
|
||||
setGlobalErrorOnUnknownDevices: jest.fn(),
|
||||
getCrypto: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue({
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
isCrossSigningReady: jest.fn().mockReturnValue(false),
|
||||
getUserDeviceInfo: jest.fn().mockReturnValue(new Map()),
|
||||
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)),
|
||||
getVersion: jest.fn().mockReturnValue("1"),
|
||||
setDeviceIsolationMode: jest.fn(),
|
||||
}),
|
||||
// This needs to not finish immediately because we need to test the screen appears
|
||||
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
||||
secretStorage: {
|
||||
isStored: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
|
@ -137,22 +159,8 @@ describe("<MatrixChat />", () => {
|
|||
isNameResolvable: true,
|
||||
warning: "",
|
||||
};
|
||||
const defaultProps: ComponentProps<typeof MatrixChat> = {
|
||||
config: {
|
||||
brand: "Test",
|
||||
help_url: "help_url",
|
||||
help_encryption_url: "help_encryption_url",
|
||||
element_call: {},
|
||||
feedback: {
|
||||
existing_issues_url: "https://feedback.org/existing",
|
||||
new_issue_url: "https://feedback.org/new",
|
||||
},
|
||||
validated_server_config: serverConfig,
|
||||
},
|
||||
onNewScreen: jest.fn(),
|
||||
onTokenLoginCompleted: jest.fn(),
|
||||
realQueryParams: {},
|
||||
};
|
||||
let initPromise: Promise<void> | undefined;
|
||||
let defaultProps: ComponentProps<typeof MatrixChat>;
|
||||
const getComponent = (props: Partial<ComponentProps<typeof MatrixChat>> = {}) =>
|
||||
render(<MatrixChat {...defaultProps} {...props} />);
|
||||
|
||||
|
@ -184,10 +192,6 @@ describe("<MatrixChat />", () => {
|
|||
// need to wait for different elements depending on which flow
|
||||
// without security setup we go to a loading page
|
||||
if (withoutSecuritySetup) {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
await screen.findByText("Logout");
|
||||
// initial sync
|
||||
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
|
||||
// wait for logged in view to load
|
||||
await screen.findByLabelText("User menu");
|
||||
|
||||
|
@ -207,39 +211,64 @@ describe("<MatrixChat />", () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockClient = getMockClientWithEventEmitter(getMockClientMethods());
|
||||
fetchMock.get("https://test.com/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
fetchMock.catch({
|
||||
status: 404,
|
||||
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
defaultProps = {
|
||||
config: {
|
||||
brand: "Test",
|
||||
help_url: "help_url",
|
||||
help_encryption_url: "help_encryption_url",
|
||||
element_call: {},
|
||||
feedback: {
|
||||
existing_issues_url: "https://feedback.org/existing",
|
||||
new_issue_url: "https://feedback.org/new",
|
||||
},
|
||||
validated_server_config: serverConfig,
|
||||
},
|
||||
onNewScreen: jest.fn(),
|
||||
onTokenLoginCompleted: jest.fn(),
|
||||
realQueryParams: {},
|
||||
initPromiseCallback: (p: Promise<void>) => (initPromise = p),
|
||||
};
|
||||
|
||||
initPromise = undefined;
|
||||
mockClient = getMockClientWithEventEmitter(getMockClientMethods());
|
||||
jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient);
|
||||
|
||||
jest.spyOn(StorageAccess, "idbLoad").mockReset();
|
||||
jest.spyOn(StorageAccess, "idbSave").mockResolvedValue(undefined);
|
||||
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
|
||||
jest.spyOn(defaultDispatcher, "fire").mockClear();
|
||||
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
|
||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(
|
||||
{} as ValidatedServerConfig,
|
||||
);
|
||||
|
||||
bootstrapDeferred = defer();
|
||||
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
resetJsDomAfterEach();
|
||||
afterEach(async () => {
|
||||
// Wait for the promise that MatrixChat gives us to complete so that we know
|
||||
// it's finished running its login code. We either need to do this or make the
|
||||
// login code abort halfway through once the test finishes testing whatever it
|
||||
// needs to test. If we do nothing, the login code will just continue running
|
||||
// and interfere with the subsequent tests.
|
||||
await initPromise;
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore
|
||||
DMRoomMap.setShared(null);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
|
||||
// emit a loggedOut event so that all of the Store singletons forget about their references to the mock client
|
||||
defaultDispatcher.dispatch({ action: Action.OnLoggedOut });
|
||||
// (must be sync otherwise the next test will start before it happens)
|
||||
defaultDispatcher.dispatch({ action: Action.OnLoggedOut }, true);
|
||||
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
resetJsDomAfterEach();
|
||||
|
||||
it("should render spinner while app is loading", () => {
|
||||
const { container } = getComponent();
|
||||
|
||||
|
@ -298,7 +327,7 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
expect(within(dialog).getByText(errorMessage)).toBeInTheDocument();
|
||||
// just check we're back on welcome page
|
||||
expect(document.querySelector(".mx_Welcome")!).toBeInTheDocument();
|
||||
await expect(await screen.findByTestId("mx_welcome_screen")).toBeInTheDocument();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -395,9 +424,7 @@ describe("<MatrixChat />", () => {
|
|||
const onTokenLoginCompleted = jest.fn();
|
||||
getComponent({ realQueryParams, onTokenLoginCompleted });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(onTokenLoginCompleted).toHaveBeenCalled();
|
||||
await waitFor(() => expect(onTokenLoginCompleted).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
describe("when login fails", () => {
|
||||
|
@ -461,17 +488,12 @@ describe("<MatrixChat />", () => {
|
|||
jest.spyOn(StorageAccess, "idbLoad").mockImplementation(
|
||||
async (_table: string, key: string | string[]) => (key === "mx_access_token" ? accessToken : null),
|
||||
);
|
||||
loginClient.getProfileInfo.mockResolvedValue({
|
||||
displayname: "Ernie",
|
||||
});
|
||||
});
|
||||
|
||||
it("should persist login credentials", async () => {
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(localStorage.getItem("mx_hs_url")).toEqual(homeserverUrl);
|
||||
await waitFor(() => expect(localStorage.getItem("mx_hs_url")).toEqual(homeserverUrl));
|
||||
expect(localStorage.getItem("mx_user_id")).toEqual(userId);
|
||||
expect(localStorage.getItem("mx_has_access_token")).toEqual("true");
|
||||
expect(localStorage.getItem("mx_device_id")).toEqual(deviceId);
|
||||
|
@ -480,34 +502,17 @@ describe("<MatrixChat />", () => {
|
|||
it("should store clientId and issuer in session storage", async () => {
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(localStorage.getItem("mx_oidc_client_id")).toEqual(clientId);
|
||||
await waitFor(() => expect(localStorage.getItem("mx_oidc_client_id")).toEqual(clientId));
|
||||
expect(localStorage.getItem("mx_oidc_token_issuer")).toEqual(issuer);
|
||||
});
|
||||
|
||||
it("should set logged in and start MatrixClient", async () => {
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
"setLoggedIn: mxid: " +
|
||||
userId +
|
||||
" deviceId: " +
|
||||
deviceId +
|
||||
" guest: " +
|
||||
false +
|
||||
" hs: " +
|
||||
homeserverUrl +
|
||||
" softLogout: " +
|
||||
false,
|
||||
" freshLogin: " + true,
|
||||
);
|
||||
|
||||
// client successfully started
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" });
|
||||
await waitFor(() =>
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" }),
|
||||
);
|
||||
|
||||
// check we get to logged in view
|
||||
await waitForSyncAndLoad(loginClient, true);
|
||||
|
@ -545,8 +550,9 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
describe("with an existing session", () => {
|
||||
const mockidb: Record<string, Record<string, string>> = {
|
||||
acccount: {
|
||||
account: {
|
||||
mx_access_token: accessToken,
|
||||
mx_refresh_token: refreshToken,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -579,21 +585,12 @@ describe("<MatrixChat />", () => {
|
|||
it("should render welcome page after login", async () => {
|
||||
getComponent();
|
||||
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
const logoutButton = await screen.findByText("Logout");
|
||||
|
||||
expect(logoutButton).toBeInTheDocument();
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
|
||||
// initial sync
|
||||
mockClient.emit(ClientEvent.Sync, SyncState.Prepared, null);
|
||||
|
||||
// wait for logged in view to load
|
||||
await screen.findByLabelText("User menu");
|
||||
// let things settle
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
||||
expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument();
|
||||
const h1Element = screen.getByRole("heading", { level: 1 });
|
||||
expect(h1Element).toHaveTextContent(`Welcome Ernie`);
|
||||
});
|
||||
|
||||
describe("clean up drafts", () => {
|
||||
|
@ -888,7 +885,7 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
// stuff that happens in onloggedout
|
||||
expect(defaultDispatcher.fire).toHaveBeenCalledWith(Action.OnLoggedOut, true);
|
||||
expect(logoutClient.clearStores).toHaveBeenCalled();
|
||||
await waitFor(() => expect(logoutClient.clearStores).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("should do post-logout cleanup", async () => {
|
||||
|
@ -897,12 +894,22 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
// stuff that happens in onloggedout
|
||||
expect(defaultDispatcher.fire).toHaveBeenCalledWith(Action.OnLoggedOut, true);
|
||||
expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled();
|
||||
await waitFor(() => expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled());
|
||||
expect(logoutClient.clearStores).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unskippable verification", () => {
|
||||
it("should show the complete security screen if unskippable verification is enabled", async () => {
|
||||
defaultProps.config.force_verification = true;
|
||||
localStorage.setItem("must_verify_device", "true");
|
||||
getComponent();
|
||||
|
||||
await screen.findByRole("heading", { name: "Unable to verify this device", level: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a soft-logged-out session", () => {
|
||||
|
@ -985,10 +992,6 @@ describe("<MatrixChat />", () => {
|
|||
user_id: userId,
|
||||
});
|
||||
loginClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||
|
||||
loginClient.getProfileInfo.mockResolvedValue({
|
||||
displayname: "Ernie",
|
||||
});
|
||||
});
|
||||
|
||||
it("should render login page", async () => {
|
||||
|
@ -1056,7 +1059,9 @@ describe("<MatrixChat />", () => {
|
|||
},
|
||||
});
|
||||
|
||||
loginClient.isRoomEncrypted.mockImplementation((roomId) => roomId === encryptedRoom.roomId);
|
||||
loginClient.isRoomEncrypted.mockImplementation((roomId) => {
|
||||
return roomId === encryptedRoom.roomId;
|
||||
});
|
||||
});
|
||||
|
||||
it("should go straight to logged in view when user is not in any encrypted rooms", async () => {
|
||||
|
@ -1085,10 +1090,8 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
expect(loginClient.userHasCrossSigningKeys).toHaveBeenCalled();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// set up keys screen is rendered
|
||||
expect(screen.getByText("Setting up keys")).toBeInTheDocument();
|
||||
await expect(await screen.findByText("Setting up keys")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1117,6 +1120,17 @@ describe("<MatrixChat />", () => {
|
|||
// set up keys screen is rendered
|
||||
expect(screen.getByText("Setting up keys")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should go to use case selection if user just registered", async () => {
|
||||
loginClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
|
||||
MatrixClientPeg.setJustRegisteredUserId(userId);
|
||||
|
||||
await getComponentAndLogin();
|
||||
|
||||
bootstrapDeferred.resolve();
|
||||
|
||||
await expect(await screen.findByRole("heading", { name: "You're in", level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1180,9 +1194,7 @@ describe("<MatrixChat />", () => {
|
|||
const onTokenLoginCompleted = jest.fn();
|
||||
getComponent({ realQueryParams, onTokenLoginCompleted });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(onTokenLoginCompleted).toHaveBeenCalled();
|
||||
await waitFor(() => expect(onTokenLoginCompleted).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
describe("when login fails", () => {
|
||||
|
@ -1228,10 +1240,8 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// just check we called the clearStorage function
|
||||
expect(loginClient.clearStores).toHaveBeenCalled();
|
||||
await waitFor(() => expect(loginClient.clearStores).toHaveBeenCalled());
|
||||
expect(localStorage.getItem("mx_sso_hs_url")).toBe(null);
|
||||
expect(localStorageClearSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -1239,9 +1249,7 @@ describe("<MatrixChat />", () => {
|
|||
it("should persist login credentials", async () => {
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(localStorage.getItem("mx_hs_url")).toEqual(serverConfig.hsUrl);
|
||||
await waitFor(() => expect(localStorage.getItem("mx_hs_url")).toEqual(serverConfig.hsUrl));
|
||||
expect(localStorage.getItem("mx_user_id")).toEqual(userId);
|
||||
expect(localStorage.getItem("mx_has_access_token")).toEqual("true");
|
||||
expect(localStorage.getItem("mx_device_id")).toEqual(deviceId);
|
||||
|
@ -1251,8 +1259,7 @@ describe("<MatrixChat />", () => {
|
|||
const sessionStorageSetSpy = jest.spyOn(sessionStorage.__proto__, "setItem");
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
expect(sessionStorageSetSpy).toHaveBeenCalledWith("mx_fresh_login", "true");
|
||||
await waitFor(() => expect(sessionStorageSetSpy).toHaveBeenCalledWith("mx_fresh_login", "true"));
|
||||
});
|
||||
|
||||
it("should override hsUrl in creds when login response wellKnown differs from config", async () => {
|
||||
|
@ -1268,9 +1275,7 @@ describe("<MatrixChat />", () => {
|
|||
loginClient.login.mockResolvedValue(loginResponseWithWellKnown);
|
||||
getComponent({ realQueryParams });
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(localStorage.getItem("mx_hs_url")).toEqual(hsUrlFromWk);
|
||||
await waitFor(() => expect(localStorage.getItem("mx_hs_url")).toEqual(hsUrlFromWk));
|
||||
});
|
||||
|
||||
it("should continue to post login setup when no session is found in local storage", async () => {
|
||||
|
@ -1319,8 +1324,10 @@ describe("<MatrixChat />", () => {
|
|||
screen: "start_sso",
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(ssoClient.getSsoLoginUrl).toHaveBeenCalledWith("http://localhost/", "sso", undefined, undefined);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(ssoClient.getSsoLoginUrl).toHaveBeenCalledWith("http://localhost/", "sso", undefined, undefined),
|
||||
);
|
||||
expect(window.localStorage.getItem(SSO_HOMESERVER_URL_KEY)).toEqual("matrix.example.com");
|
||||
expect(window.localStorage.getItem(SSO_ID_SERVER_URL_KEY)).toEqual("ident.example.com");
|
||||
expect(hrefSetter).toHaveBeenCalledWith("http://my-sso-url");
|
||||
|
@ -1332,8 +1339,10 @@ describe("<MatrixChat />", () => {
|
|||
screen: "start_cas",
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(ssoClient.getSsoLoginUrl).toHaveBeenCalledWith("http://localhost/", "cas", undefined, undefined);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(ssoClient.getSsoLoginUrl).toHaveBeenCalledWith("http://localhost/", "cas", undefined, undefined),
|
||||
);
|
||||
expect(window.localStorage.getItem(SSO_HOMESERVER_URL_KEY)).toEqual("matrix.example.com");
|
||||
expect(window.localStorage.getItem(SSO_ID_SERVER_URL_KEY)).toEqual("ident.example.com");
|
||||
expect(hrefSetter).toHaveBeenCalledWith("http://my-sso-url");
|
||||
|
@ -1397,7 +1406,6 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
const client = getMockClientWithEventEmitter(getMockClientMethods());
|
||||
jest.spyOn(MatrixJs, "createClient").mockReturnValue(client);
|
||||
client.getProfileInfo.mockResolvedValue({ displayname: "Ernie" });
|
||||
|
||||
const rendered = getComponent({});
|
||||
await waitForSyncAndLoad(client, true);
|
||||
|
|
|
@ -117,6 +117,7 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
|
|||
>
|
||||
<div
|
||||
class="mx_Welcome"
|
||||
data-testid="mx_welcome_screen"
|
||||
>
|
||||
<div
|
||||
class="mx_WelcomePage mx_WelcomePage_loggedIn"
|
||||
|
|
|
@ -3976,6 +3976,11 @@ core-js@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea"
|
||||
integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==
|
||||
|
||||
core-js@^3.38.1:
|
||||
version "3.38.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e"
|
||||
integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
|
@ -9594,6 +9599,7 @@ which@^2.0.1:
|
|||
isexe "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
name wrap-ansi-cjs
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
|
Loading…
Reference in a new issue