Migrate more tests to vitest (#34355)
* Remove doubled-up I18n Provider. ModalManager already provides this. * Reconfigure svgr into a vite-friendly `?react` resource query * Move InviteDialog test to vitest * Move RoomHeader tests to Vitest * Share serializer between Jest & Vitest * Attempt to stabilise InviteDialog test * Fix async leaks * Migrate more tests to vitest * Migrate MatrixChat test to vitest * Deflake * Iterate * Iterate * Iterate
This commit is contained in:
File diff suppressed because it is too large
Load Diff
-716
@@ -1,716 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens after a session is restored 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens during crypto init 1`] = `
|
||||
<body
|
||||
style="--emoji-font-family: Twemoji;"
|
||||
>
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens while we are checking the sync store 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout shows the lockout page when a second tab opens while we were waiting for the lock ourselves 1`] = `
|
||||
<div>
|
||||
<main
|
||||
class="mx_SessionLockStolenView mx_SplashPage"
|
||||
>
|
||||
<h1>
|
||||
Test is connected in another tab
|
||||
</h1>
|
||||
<h2>
|
||||
Switch to the other tab to connect to Test. This tab can now be closed.
|
||||
</h2>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ConfirmSessionLockTheftView"
|
||||
>
|
||||
<div
|
||||
class="mx_ConfirmSessionLockTheftView_body"
|
||||
>
|
||||
<p>
|
||||
Test is open in another window. Click "Continue" to use Test here and disconnect the other window.
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during startup 3`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
style="position: relative;"
|
||||
>
|
||||
<main
|
||||
aria-live="polite"
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: inherit;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="_glass_sepwu_8"
|
||||
>
|
||||
<div
|
||||
class="mx_Welcome"
|
||||
>
|
||||
<div
|
||||
class="mx_DefaultWelcome"
|
||||
>
|
||||
<a
|
||||
class="mx_DefaultWelcome_logo"
|
||||
href="https://element.io"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="Test"
|
||||
src="themes/element/img/logos/element-logo.svg"
|
||||
/>
|
||||
</a>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Welcome to Test
|
||||
</h1>
|
||||
<div
|
||||
class="mx_DefaultWelcome_buttons"
|
||||
>
|
||||
<a
|
||||
class="_button_1nw83_8"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
href="#/login"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8"
|
||||
data-kind="secondary"
|
||||
data-size="md"
|
||||
href="#/register"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
Create account
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8"
|
||||
data-kind="tertiary"
|
||||
data-size="md"
|
||||
href="#/directory"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
Explore rooms
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="mx_Dropdown_arrow"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> qr login should fire ViewQrLogin action on 'qr_login' route 1`] = `
|
||||
<div
|
||||
aria-label="Sign in with QR code"
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<div
|
||||
class="mx_LoginWithQR"
|
||||
data-testid="login-with-qr"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_main"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_spinner"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginWithQR_buttons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> qr login should open QrLoginDialog on ViewQrLogin action 1`] = `
|
||||
<div
|
||||
aria-label="Sign in with QR code"
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
/>
|
||||
<div
|
||||
class="mx_LoginWithQR"
|
||||
data-testid="login-with-qr"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_main"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginWithQR_spinner"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginWithQR_buttons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> should render spinner while app is loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logout page 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal mx_AuthPage_modal_withBlur"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modalBlur"
|
||||
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
|
||||
/>
|
||||
<main
|
||||
aria-live="polite"
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: inherit;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthHeader"
|
||||
>
|
||||
<aside
|
||||
class="mx_AuthHeaderLogo"
|
||||
>
|
||||
<img
|
||||
alt="Element"
|
||||
src="themes/element/img/logos/element-logo.svg"
|
||||
/>
|
||||
</aside>
|
||||
<div
|
||||
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="mx_Dropdown_arrow"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AuthBody"
|
||||
>
|
||||
<h1>
|
||||
You're signed out
|
||||
</h1>
|
||||
<h2>
|
||||
Sign in
|
||||
</h2>
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
Enter your password to sign in and regain access to your account.
|
||||
</p>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign in
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Forgotten your password?
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<h2>
|
||||
Clear personal data
|
||||
</h2>
|
||||
<p>
|
||||
Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Clear all data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = `
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Leave room
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to leave the room '!room:server.org'?
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="cancel"
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Leave
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a space should launch a confirmation modal 1`] = `
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Leave space
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_content"
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to leave the space '!spaceRoom:server.org'?
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
class="cancel"
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Leave
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import CompleteSecurity from "../../../../../src/components/structures/auth/CompleteSecurity";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { Phase, SetupEncryptionStore } from "../../../../../src/stores/SetupEncryptionStore";
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
|
||||
class MockSetupEncryptionStore extends EventEmitter {
|
||||
public phase: Phase = Phase.Intro;
|
||||
public lostKeys(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public start: () => void = jest.fn();
|
||||
public stop: () => void = jest.fn();
|
||||
}
|
||||
|
||||
describe("CompleteSecurity", () => {
|
||||
beforeEach(() => {
|
||||
const client = stubClient();
|
||||
const deviceIdToDevice = new Map();
|
||||
deviceIdToDevice.set("DEVICE_ID", {
|
||||
deviceId: "DEVICE_ID",
|
||||
userId: "USER_ID",
|
||||
});
|
||||
const userIdToDevices = new Map();
|
||||
userIdToDevices.set("USER_ID", deviceIdToDevice);
|
||||
mocked(client.getCrypto()!.getUserDeviceInfo).mockResolvedValue(userIdToDevices);
|
||||
|
||||
const mockSetupEncryptionStore = new MockSetupEncryptionStore();
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(
|
||||
mockSetupEncryptionStore as SetupEncryptionStore,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("Renders with a cancel button by default", () => {
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders with a cancel button if forceVerification false", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "forceVerification") {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Skip verification for now" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders without a cancel button if forceVerification true", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "force_verification") {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
render(<CompleteSecurity onFinished={() => {}} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Skip verification for now" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders a warning if user hits Reset", async () => {
|
||||
// Given a store and a dialog based on it
|
||||
const store = new SetupEncryptionStore();
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// No recovery methods are available, so only the "Can't confirm?" button should be visible
|
||||
expect(screen.queryByRole("button", { name: "Can't confirm?" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Use another device" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Use recovery key" })).not.toBeInTheDocument();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears
|
||||
expect(screen.getByRole("heading", { name: "You need to reset your digital identity" })).toBeInTheDocument();
|
||||
expect(panel.getByRole("button", { name: "Continue" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Allows verifying with another device if one is available", async () => {
|
||||
// Given a store and a dialog based on it
|
||||
const store = new SetupEncryptionStore();
|
||||
jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => {
|
||||
store.hasDevicesToVerifyAgainst = true;
|
||||
store.phase = Phase.Intro;
|
||||
store.emit("update");
|
||||
});
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// The snapshot should have "Use another device" and "Can't confirm?"
|
||||
// buttons, but no "Use recovery key".
|
||||
expect(panel.asFragment()).toMatchSnapshot();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears, and should have a different
|
||||
// title from when there were no verification methods available.
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Are you sure you want to reset your digital identity?" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Allows verifying with recovery key if one is available", async () => {
|
||||
// Given a store and a dialog based on it
|
||||
const store = new SetupEncryptionStore();
|
||||
jest.spyOn(store, "fetchKeyInfo").mockImplementation(async () => {
|
||||
store.keyInfo = {} as any;
|
||||
store.phase = Phase.Intro;
|
||||
store.emit("update");
|
||||
});
|
||||
jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store);
|
||||
const panel = await act(() => render(<CompleteSecurity onFinished={() => {}} />));
|
||||
|
||||
// The snapshot should have "Use recovery key" and "Can't confirm?"
|
||||
// buttons, but no "Use another device".
|
||||
expect(panel.asFragment()).toMatchSnapshot();
|
||||
|
||||
// When we hit reset
|
||||
await act(async () => panel.getByRole("button", { name: "Can't confirm?" }).click());
|
||||
|
||||
// Then the reset identity dialog appears, and should have a different
|
||||
// title from when there were no verification methods available.
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Are you sure you want to reset your digital identity?" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import E2eSetup from "../../../../../src/components/structures/auth/E2eSetup.tsx";
|
||||
import { InitialCryptoSetupStore } from "../../../../../src/stores/InitialCryptoSetupStore.ts";
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
describe("LeftPanel", () => {
|
||||
it("should call `onCancelled` when the user clicks the cancel button", () => {
|
||||
const mockInitialCryptoSetupStore = {
|
||||
getStatus: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
};
|
||||
jest.spyOn(InitialCryptoSetupStore, "sharedInstance").mockReturnValue(mockInitialCryptoSetupStore as any);
|
||||
|
||||
// We need the setup process to have failed, for the dialog to present a cancel button.
|
||||
mocked(mockInitialCryptoSetupStore.getStatus).mockReturnValue("error");
|
||||
|
||||
const onCancelled = jest.fn();
|
||||
const { getByRole } = render(<E2eSetup onCancelled={onCancelled} />);
|
||||
|
||||
getByRole("button", { name: "Cancel" }).click();
|
||||
expect(onCancelled).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,404 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { render, type RenderResult, screen, waitFor, cleanup } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type MatrixClient, createClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ForgotPassword from "../../../../../src/components/structures/auth/ForgotPassword";
|
||||
import { type ValidatedServerConfig } from "../../../../../src/utils/ValidatedServerConfig";
|
||||
import { clearAllModals, filterConsole, stubClient, waitEnoughCyclesForModal } from "../../../../test-utils";
|
||||
import AutoDiscoveryUtils from "../../../../../src/utils/AutoDiscoveryUtils";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||
createClient: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<ForgotPassword>", () => {
|
||||
const testEmail = "user@example.com";
|
||||
const testSid = "sid42";
|
||||
const testPassword = "cRaZyP4ssw0rd!";
|
||||
let client: MatrixClient;
|
||||
let serverConfig: ValidatedServerConfig;
|
||||
let onComplete: () => void;
|
||||
let onLoginClick: () => void;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const typeIntoField = async (label: string, value: string): Promise<void> => {
|
||||
await userEvent.type(screen.getByLabelText(label), value, { delay: null });
|
||||
};
|
||||
|
||||
const click = async (element: Element): Promise<void> => {
|
||||
await userEvent.click(element, { delay: null });
|
||||
};
|
||||
|
||||
const itShouldCloseTheDialogAndShowThePasswordInput = (): void => {
|
||||
it("should close the dialog and show the password input", async () => {
|
||||
await waitFor(() => expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument());
|
||||
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
filterConsole(
|
||||
// not implemented by js-dom https://github.com/jsdom/jsdom/issues/1937
|
||||
"Not implemented: HTMLFormElement.prototype.requestSubmit",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
mocked(createClient).mockReturnValue(client);
|
||||
|
||||
serverConfig = { hsName: "example.com" } as ValidatedServerConfig;
|
||||
|
||||
onComplete = jest.fn();
|
||||
onLoginClick = jest.fn();
|
||||
|
||||
jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue(serverConfig);
|
||||
jest.spyOn(AutoDiscoveryUtils, "authComponentStateForError");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// clean up modals
|
||||
await clearAllModals();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("when starting a password reset flow", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the email input and mention the homeserver", () => {
|
||||
expect(screen.queryByLabelText("Email address")).toBeInTheDocument();
|
||||
expect(screen.queryByText("example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and updating the server config", () => {
|
||||
beforeEach(() => {
|
||||
serverConfig.hsName = "example2.com";
|
||||
renderResult.rerender(
|
||||
<ForgotPassword serverConfig={serverConfig} onComplete={onComplete} onLoginClick={onLoginClick} />,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the new homeserver server name", () => {
|
||||
expect(screen.queryByText("example2.com")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Sign in instead«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Sign in instead"));
|
||||
});
|
||||
|
||||
it("should call onLoginClick()", () => {
|
||||
expect(onLoginClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and entering a non-email value", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", "not en email");
|
||||
});
|
||||
|
||||
it("should show a message about the wrong format", async () => {
|
||||
await expect(
|
||||
screen.findByText("The email address doesn't appear to be valid."),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting an unknown email", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockResolvedValue(serverConfig);
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
errcode: "M_THREEPID_NOT_FOUND",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show an email not found message", async () => {
|
||||
await expect(screen.findByText("This email address was not found")).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and a connection error occurs", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockRejectedValue({
|
||||
name: "ConnectionError",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show an info about that", async () => {
|
||||
await expect(
|
||||
screen.findByText(
|
||||
"Cannot reach homeserver: Ensure you have a stable internet connection, or get in touch with the server admin",
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the server liveness check fails", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(AutoDiscoveryUtils.validateServerConfigWithStaticUrls).mockRejectedValue({});
|
||||
mocked(AutoDiscoveryUtils.authComponentStateForError).mockReturnValue({
|
||||
serverErrorIsFatal: true,
|
||||
serverIsAlive: false,
|
||||
serverDeadError: "server down",
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should show the server error", async () => {
|
||||
await expect(screen.findByText("server down")).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting an known email", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("Email address", testEmail);
|
||||
mocked(client).requestPasswordEmailToken.mockResolvedValue({
|
||||
sid: testSid,
|
||||
});
|
||||
await click(screen.getByText("Send email"));
|
||||
});
|
||||
|
||||
it("should send the mail and show the check email view", () => {
|
||||
expect(client.requestPasswordEmailToken).toHaveBeenCalledWith(
|
||||
testEmail,
|
||||
expect.any(String),
|
||||
1, // second send attempt
|
||||
);
|
||||
expect(screen.getByText("Check your email to continue")).toBeInTheDocument();
|
||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and clicking »Re-enter email address«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Re-enter email address"));
|
||||
});
|
||||
|
||||
it("go back to the email input", () => {
|
||||
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Resend«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Resend"));
|
||||
});
|
||||
|
||||
it("should should resend the mail and show the tooltip", () => {
|
||||
expect(client.requestPasswordEmailToken).toHaveBeenCalledWith(
|
||||
testEmail,
|
||||
expect.any(String),
|
||||
2, // second send attempt
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("tooltip", { name: "Verification link email resent!" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Next«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Next"));
|
||||
});
|
||||
|
||||
it("should show the password input view", () => {
|
||||
expect(screen.getByText("Reset your password")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and entering different passwords", () => {
|
||||
beforeEach(async () => {
|
||||
await typeIntoField("New Password", testPassword);
|
||||
await typeIntoField("Confirm new password", testPassword + "asd");
|
||||
});
|
||||
|
||||
it("should show an info about that", async () => {
|
||||
await expect(
|
||||
screen.findByText("New passwords must match each other."),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and entering a new password", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockRejectedValue({ httpStatus: 401 });
|
||||
await typeIntoField("New Password", testPassword);
|
||||
await typeIntoField("Confirm new password", testPassword);
|
||||
});
|
||||
|
||||
describe("and submitting it running into rate limiting", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockRejectedValue({
|
||||
message: "rate limit reached",
|
||||
httpStatus: 429,
|
||||
data: {
|
||||
retry_after_ms: (13 * 60 + 37) * 1000,
|
||||
},
|
||||
});
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should show the rate limit error message", () => {
|
||||
expect(
|
||||
screen.getByText("Too many attempts in a short time. Retry after 13:37."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and confirm the email link and submitting the new password", () => {
|
||||
beforeEach(async () => {
|
||||
// fake link confirmed by resolving client.setPassword instead of raising an error
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should send the new password (once)", async () => {
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
false,
|
||||
);
|
||||
|
||||
// it should not retry to set the password
|
||||
await waitFor(() => expect(client.setPassword).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe("and submitting it", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Reset password"));
|
||||
await waitEnoughCyclesForModal();
|
||||
});
|
||||
|
||||
it("should send the new password and show the click validation link dialog", async () => {
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
false,
|
||||
);
|
||||
await expect(
|
||||
screen.findByText("Verify your email to continue"),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText(testEmail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and dismissing the dialog by clicking the background", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null });
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||
});
|
||||
|
||||
describe("and dismissing the dialog", () => {
|
||||
beforeEach(async () => {
|
||||
await click(await screen.findByLabelText("Close dialog"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
itShouldCloseTheDialogAndShowThePasswordInput();
|
||||
});
|
||||
|
||||
describe("and clicking »Re-enter email address«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(await screen.findByText("Re-enter email address"));
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should close the dialog and go back to the email input", async () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("and validating the link from the mail", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(client.setPassword).mockResolvedValue({});
|
||||
await click(screen.getByText("Reset password"));
|
||||
// flush promises for the modal to disappear
|
||||
await waitEnoughCyclesForModal();
|
||||
await waitEnoughCyclesForModal();
|
||||
});
|
||||
|
||||
it("should display the confirm reset view and now show the dialog", async () => {
|
||||
await expect(
|
||||
screen.findByText("Your password has been reset."),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and clicking »Remove other devices« and »Reset password«", () => {
|
||||
beforeEach(async () => {
|
||||
await click(screen.getByText("Remove other devices"));
|
||||
await click(screen.getByText("Reset password"));
|
||||
});
|
||||
|
||||
it("should show the removing warning dialog", async () => {
|
||||
await expect(
|
||||
screen.findByText(
|
||||
"Removing your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
// confirm dialog
|
||||
await click(screen.getByText("Continue"));
|
||||
|
||||
// expect setPassword with logoutDevices = true
|
||||
expect(client.setPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
client_secret: expect.any(String),
|
||||
sid: testSid,
|
||||
},
|
||||
},
|
||||
testPassword,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,466 +0,0 @@
|
||||
/*
|
||||
Copyright 2019-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { mocked, type MockedObject } from "jest-mock-vitest-adapter";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
import {
|
||||
OAUTH_AWARE_PREFERRED_FLOW_FIELD,
|
||||
IdentityProviderBrand,
|
||||
type ValidatedAuthMetadata,
|
||||
OAuth2Error,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import * as Matrix from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils";
|
||||
import Login from "../../../../../src/components/structures/auth/Login";
|
||||
import type BasePlatform from "../../../../../src/BasePlatform";
|
||||
import * as registerClientUtils from "../../../../../src/utils/oauth/registerClient";
|
||||
import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth";
|
||||
import { ModuleApi } from "../../../../../src/modules/Api.ts";
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
const oauthStaticClientsConfig = {
|
||||
"https://staticallyregisteredissuer.org/": {
|
||||
client_id: "static-clientId-123",
|
||||
},
|
||||
};
|
||||
|
||||
describe("Login", function () {
|
||||
let platform: MockedObject<BasePlatform>;
|
||||
|
||||
const mockClient = mocked({
|
||||
login: jest.fn().mockResolvedValue({}),
|
||||
loginFlows: jest.fn(),
|
||||
} as unknown as Matrix.MatrixClient);
|
||||
|
||||
beforeEach(function () {
|
||||
SdkConfig.put({
|
||||
brand: "test-brand",
|
||||
disable_custom_urls: true,
|
||||
oidc_static_clients: oauthStaticClientsConfig,
|
||||
});
|
||||
mockClient.login.mockClear().mockResolvedValue({
|
||||
access_token: "TOKEN",
|
||||
device_id: "IAMADEVICE",
|
||||
user_id: "@user:server",
|
||||
});
|
||||
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||
jest.spyOn(Matrix, "createClient").mockImplementation((opts) => {
|
||||
mockClient.idBaseUrl = opts.idBaseUrl;
|
||||
mockClient.baseUrl = opts.baseUrl;
|
||||
return mockClient;
|
||||
});
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
platform = mockPlatformPeg({
|
||||
startSingleSignOn: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
SdkConfig.reset(); // we touch the config, so clean up
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
function getRawComponent(
|
||||
hsUrl = "https://matrix.org",
|
||||
isUrl = "https://vector.im",
|
||||
delegatedAuthentication?: ValidatedAuthMetadata,
|
||||
) {
|
||||
return (
|
||||
<Login
|
||||
serverConfig={mkServerConfig(hsUrl, isUrl, delegatedAuthentication)}
|
||||
onLoggedIn={() => {}}
|
||||
onRegisterClick={() => {}}
|
||||
onServerConfigChange={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getComponent(hsUrl?: string, isUrl?: string, delegatedAuthentication?: ValidatedAuthMetadata) {
|
||||
return render(getRawComponent(hsUrl, isUrl, delegatedAuthentication));
|
||||
}
|
||||
|
||||
it("should show form with change server link", async () => {
|
||||
SdkConfig.put({
|
||||
brand: "test-brand",
|
||||
disable_custom_urls: false,
|
||||
});
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
|
||||
expect(container.querySelector(".mx_ServerPicker_change")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show register button", async () => {
|
||||
const onRegisterClick = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Login
|
||||
serverConfig={mkServerConfig("https://matrix.org", "https://vector.im")}
|
||||
onLoggedIn={() => {}}
|
||||
onRegisterClick={onRegisterClick}
|
||||
onServerConfigChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(getByText("Create an account"));
|
||||
expect(onRegisterClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hide register button", async () => {
|
||||
const { queryByText } = render(
|
||||
<Login
|
||||
serverConfig={mkServerConfig("https://matrix.org", "https://vector.im")}
|
||||
onLoggedIn={() => {}}
|
||||
onServerConfigChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(queryByText("Create an account")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show form without change server link when custom URLs disabled", async () => {
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
expect(container.querySelectorAll(".mx_ServerPicker_change")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should show SSO button if that flow is available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.sso" }] });
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show both SSO button and username+password if both are available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }, { type: "m.login.sso" }] });
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show multiple SSO buttons if multiple identity_providers are available", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
identity_providers: [
|
||||
{
|
||||
id: "a",
|
||||
name: "Provider 1",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
name: "Provider 2",
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
name: "Provider 3",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should show single SSO button if identity_providers is null", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle serverConfig updates correctly", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(platform.startSingleSignOn.mock.calls[0][0].baseUrl).toBe("https://matrix.org");
|
||||
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(platform.startSingleSignOn.mock.calls[1][0].baseUrl).toBe("https://server2");
|
||||
});
|
||||
|
||||
it("should handle updating to a server with no supported flows", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// update the mock for the new server with no supported flows
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "just something weird",
|
||||
},
|
||||
],
|
||||
});
|
||||
// render with a new server
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("This homeserver doesn't offer any login flows that are supported by this client."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// no sso button because server2 doesnt support sso
|
||||
expect(container.querySelector(".mx_SSOButton")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show single Continue button if OIDC MSC3824 compatibility is given by server", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]: true,
|
||||
},
|
||||
{
|
||||
type: "m.login.password",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
|
||||
expect(ssoButtons.length).toBe(1);
|
||||
expect(ssoButtons[0].textContent).toBe("Continue");
|
||||
|
||||
// no password form visible
|
||||
expect(container.querySelector("form")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should show branded SSO buttons", async () => {
|
||||
const idpsWithIcons = Object.values(IdentityProviderBrand).map((brand) => ({
|
||||
id: brand,
|
||||
brand,
|
||||
name: `Provider ${brand}`,
|
||||
}));
|
||||
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
identity_providers: [
|
||||
...idpsWithIcons,
|
||||
{
|
||||
id: "foo",
|
||||
name: "Provider foo",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, getByTestId } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
for (const idp of idpsWithIcons) {
|
||||
const ssoButton = getByTestId(`idp-${idp.id}`);
|
||||
expect(ssoButton).toBeTruthy();
|
||||
expect(ssoButton.childNodes[0]).toHaveAccessibleName(idp.brand);
|
||||
}
|
||||
|
||||
const ssoButtons = container.querySelectorAll(".mx_SSOButton");
|
||||
expect(ssoButtons.length).toBe(idpsWithIcons.length + 1);
|
||||
});
|
||||
|
||||
it("should display an error when homeserver doesn't offer any supported login flows", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "just something weird",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("This homeserver doesn't offer any login flows that are supported by this client."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a connection error when getting login flows fails", async () => {
|
||||
mockClient.loginFlows.mockRejectedValue("oups");
|
||||
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
expect(
|
||||
screen.getByText("There was a problem communicating with the homeserver, please try again later."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display an error when homeserver fails liveliness check", async () => {
|
||||
fetchMock.removeRoutes();
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
status: 0,
|
||||
});
|
||||
getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error displayed
|
||||
expect(screen.getByText("Cannot reach homeserver")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should reset liveliness error when server config changes", async () => {
|
||||
fetchMock.removeRoutes();
|
||||
// matrix.org is not alive
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
status: 400,
|
||||
});
|
||||
// but server2 is
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
const { rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error displayed
|
||||
expect(screen.getByText("Cannot reach homeserver")).toBeInTheDocument();
|
||||
|
||||
rerender(getRawComponent("https://server2"));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// error cleared
|
||||
expect(screen.queryByText("Cannot reach homeserver")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("OIDC native flow", () => {
|
||||
const hsUrl = "https://matrix.org";
|
||||
const isUrl = "https://vector.im";
|
||||
const issuer = "https://test.com/";
|
||||
const delegatedAuth = makeDelegatedAuthMetadata(issuer);
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, "error");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
it("should attempt to register oauth client", async () => {
|
||||
// dont mock, spy so we can check config values were correctly passed
|
||||
jest.spyOn(registerClientUtils, "getOAuthClientId");
|
||||
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// tried to register
|
||||
expect(fetchMock).toHaveFetched(delegatedAuth.registration_endpoint);
|
||||
// called with values from config
|
||||
expect(registerClientUtils.getOAuthClientId).toHaveBeenCalledWith(delegatedAuth, oauthStaticClientsConfig);
|
||||
});
|
||||
|
||||
it("should fallback to normal login when client registration fails", async () => {
|
||||
fetchMock.post(delegatedAuth.registration_endpoint!, { status: 500 });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// tried to register
|
||||
expect(fetchMock).toHaveFetched(delegatedAuth.registration_endpoint);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Failed to get OAuth2 native flow",
|
||||
new Error(OAuth2Error.DynamicRegistrationFailed),
|
||||
);
|
||||
|
||||
// continued with normal setup
|
||||
expect(mockClient.loginFlows).toHaveBeenCalled();
|
||||
// normal password login rendered
|
||||
expect(screen.getByLabelText("Username")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// short term during active development, UI will be added in next PRs
|
||||
it("should show continue button when oauth native flow is correctly configured", async () => {
|
||||
fetchMock.post(delegatedAuth.registration_endpoint!, { client_id: "abc123" });
|
||||
getComponent(hsUrl, isUrl, delegatedAuth);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
// did not continue with matrix login
|
||||
expect(mockClient.loginFlows).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Continue")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Module API", () => {
|
||||
afterEach(() => {
|
||||
ModuleApi.instance.customComponents.registerLoginComponent(undefined as any);
|
||||
});
|
||||
|
||||
it("should use registered module renderer", async () => {
|
||||
ModuleApi.instance.customComponents.registerLoginComponent(() => <>Test component</>);
|
||||
const { getByText } = getComponent();
|
||||
expect(getByText("Test component")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { act, render, type RenderResult } from "jest-matrix-react";
|
||||
import React, { type ComponentProps } from "react";
|
||||
import EventEmitter from "events";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { LoginSplashView } from "../../../../../src/components/structures/auth/LoginSplashView";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
describe("<LoginSplashView />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
beforeEach(() => {
|
||||
matrixClient = new EventEmitter() as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof LoginSplashView>> = {}): RenderResult {
|
||||
const defaultProps = {
|
||||
matrixClient,
|
||||
onLogoutClick: () => {},
|
||||
syncError: null,
|
||||
};
|
||||
return render(<LoginSplashView {...defaultProps} {...props} />);
|
||||
}
|
||||
|
||||
it("Renders a spinner", () => {
|
||||
const rendered = getComponent();
|
||||
expect(rendered.getByTestId("spinner")).toBeInTheDocument();
|
||||
expect(rendered.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Renders an error message", () => {
|
||||
const rendered = getComponent({ syncError: new Error("boohoo") });
|
||||
expect(rendered.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Calls onLogoutClick", () => {
|
||||
const onLogoutClick = jest.fn();
|
||||
const rendered = getComponent({ onLogoutClick });
|
||||
expect(onLogoutClick).not.toHaveBeenCalled();
|
||||
rendered.getByRole("button", { name: "Logout" }).click();
|
||||
expect(onLogoutClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Shows migration progress", async () => {
|
||||
const rendered = getComponent();
|
||||
|
||||
act(() => {
|
||||
matrixClient.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, 5, 10);
|
||||
});
|
||||
rendered.getByText("Hang tight.", { exact: false });
|
||||
|
||||
// Wait for the animation to update
|
||||
await act(() => sleep(500));
|
||||
|
||||
const progress = rendered.getByRole("progressbar");
|
||||
expect(progress.getAttribute("value")).toEqual("5");
|
||||
expect(progress.getAttribute("max")).toEqual("10");
|
||||
});
|
||||
});
|
||||
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { createClient, type MatrixClient, MatrixError, type ValidatedAuthMetadata } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, type MockedObject } from "jest-mock-vitest-adapter";
|
||||
import fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import SdkConfig, { DEFAULTS } from "../../../../../src/SdkConfig";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mkServerConfig,
|
||||
mockPlatformPeg,
|
||||
unmockPlatformPeg,
|
||||
} from "../../../../test-utils";
|
||||
import Registration from "../../../../../src/components/structures/auth/Registration";
|
||||
import { makeDelegatedAuthMetadata } from "../../../../test-utils/auth";
|
||||
import { startOAuthLogin } from "../../../../../src/utils/oauth/authorize";
|
||||
|
||||
jest.mock("../../../../../src/utils/oauth/authorize", () => ({
|
||||
startOAuthLogin: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||
createClient: jest.fn(),
|
||||
}));
|
||||
|
||||
/** The matrix versions our mock server claims to support */
|
||||
const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"];
|
||||
|
||||
describe("Registration", function () {
|
||||
let mockClient!: MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(function () {
|
||||
SdkConfig.put({
|
||||
...DEFAULTS,
|
||||
disable_custom_urls: true,
|
||||
});
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
registerRequest: jest.fn(),
|
||||
loginFlows: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }),
|
||||
});
|
||||
mockClient.registerRequest.mockRejectedValueOnce(
|
||||
new MatrixError(
|
||||
{
|
||||
flows: [{ stages: [] }],
|
||||
},
|
||||
401,
|
||||
),
|
||||
);
|
||||
mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] });
|
||||
mocked(createClient).mockImplementation((opts) => {
|
||||
mockClient.idBaseUrl = opts.idBaseUrl;
|
||||
mockClient.baseUrl = opts.baseUrl;
|
||||
return mockClient;
|
||||
});
|
||||
fetchMock.catch(404);
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
mockPlatformPeg({
|
||||
startSingleSignOn: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jest.restoreAllMocks();
|
||||
SdkConfig.reset(); // we touch the config, so clean up
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
defaultDeviceDisplayName: "test-device-display-name",
|
||||
onLoggedIn: jest.fn(),
|
||||
onLoginClick: jest.fn(),
|
||||
onServerConfigChange: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultHsUrl = "https://matrix.org";
|
||||
const defaultIsUrl = "https://vector.im";
|
||||
|
||||
function getRawComponent(
|
||||
hsUrl = defaultHsUrl,
|
||||
isUrl = defaultIsUrl,
|
||||
authConfig?: ValidatedAuthMetadata,
|
||||
mobileRegister?: boolean,
|
||||
) {
|
||||
return (
|
||||
<Registration
|
||||
{...defaultProps}
|
||||
serverConfig={mkServerConfig(hsUrl, isUrl, authConfig)}
|
||||
mobileRegister={mobileRegister}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getComponent(
|
||||
hsUrl?: string,
|
||||
isUrl?: string,
|
||||
authConfig?: ValidatedAuthMetadata,
|
||||
mobileRegister?: boolean,
|
||||
) {
|
||||
return render(getRawComponent(hsUrl, isUrl, authConfig, mobileRegister));
|
||||
}
|
||||
|
||||
it("should show server picker", async function () {
|
||||
const { container } = getComponent();
|
||||
expect(container.querySelector(".mx_ServerPicker")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show form when custom URLs disabled", async function () {
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
expect(container.querySelector("form")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should show SSO options if those are available", async () => {
|
||||
mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.sso" }] });
|
||||
const { container } = getComponent();
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
const ssoButton = container.querySelector(".mx_SSOButton");
|
||||
expect(ssoButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle serverConfig updates correctly", async () => {
|
||||
mockClient.loginFlows.mockResolvedValue({
|
||||
flows: [
|
||||
{
|
||||
type: "m.login.sso",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { container, rerender } = render(getRawComponent());
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(mockClient.baseUrl).toBe("https://matrix.org");
|
||||
|
||||
fetchMock.get("https://server2/_matrix/client/versions", {
|
||||
unstable_features: {},
|
||||
versions: SERVER_SUPPORTED_MATRIX_VERSIONS,
|
||||
});
|
||||
rerender(getRawComponent("https://server2"));
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(container.querySelector(".mx_SSOButton")!);
|
||||
expect(mockClient.baseUrl).toBe("https://server2");
|
||||
});
|
||||
|
||||
describe("when delegated authentication is configured and enabled", () => {
|
||||
const authConfig = makeDelegatedAuthMetadata();
|
||||
const clientId = "test-client-id";
|
||||
authConfig.prompt_values_supported = ["create"];
|
||||
|
||||
beforeEach(() => {
|
||||
// mock a statically registered client to avoid dynamic registration
|
||||
SdkConfig.put({
|
||||
oidc_static_clients: {
|
||||
[authConfig.issuer]: {
|
||||
client_id: clientId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should display oidc-native continue button", async () => {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, authConfig);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
// no form
|
||||
expect(container.querySelector("form")).toBeFalsy();
|
||||
|
||||
expect(await screen.findByText("Continue")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should start OIDC login flow as registration on button click", async () => {
|
||||
getComponent(defaultHsUrl, defaultIsUrl, authConfig);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
|
||||
fireEvent.click(await screen.findByText("Continue"));
|
||||
|
||||
expect(startOAuthLogin).toHaveBeenCalledWith(
|
||||
authConfig,
|
||||
clientId,
|
||||
defaultHsUrl,
|
||||
defaultIsUrl,
|
||||
// isRegistration
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when is mobile registeration", () => {
|
||||
it("should not show server picker", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
expect(container.querySelector(".mx_ServerPicker")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should show username field with autocaps disabled", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector("#mx_RegistrationForm_username")).toHaveAttribute(
|
||||
"autocapitalize",
|
||||
"none",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show password and confirm password fields in separate rows", async function () {
|
||||
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
|
||||
|
||||
await waitFor(() => expect(container.querySelector("#mx_RegistrationForm_username")).toBeTruthy());
|
||||
// when password and confirm password fields are in separate rows there should be 4 rather than 3
|
||||
expect(container.querySelectorAll(".mx_AuthBody_fieldRow")).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-376
@@ -1,376 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`CompleteSecurity Allows verifying with another device if one is available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
style="position: relative;"
|
||||
>
|
||||
<main
|
||||
aria-live="polite"
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: inherit;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_border _glass_sepwu_8"
|
||||
>
|
||||
<div
|
||||
class="mx_CompleteSecurityBody"
|
||||
>
|
||||
<h1
|
||||
class="mx_CompleteSecurity_header"
|
||||
>
|
||||
<div
|
||||
aria-label="Skip verification for now"
|
||||
class="mx_AccessibleButton mx_CompleteSecurity_skip"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_CompleteSecurity_body"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Confirm your digital identity
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span>
|
||||
Verify this device to set up secure messaging
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/help#encryption-device-verification"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.5 20q-.625 0-1.062-.437A1.45 1.45 0 0 1 2 18.5q0-.625.438-1.062A1.45 1.45 0 0 1 3.5 17H4V6q0-.824.588-1.412A1.93 1.93 0 0 1 6 4h14q.424 0 .712.287Q21 4.576 21 5t-.288.713A.97.97 0 0 1 20 6H6v11h4.5q.624 0 1.063.438.437.437.437 1.062t-.437 1.063A1.45 1.45 0 0 1 10.5 20zM15 20a.97.97 0 0 1-.713-.288A.97.97 0 0 1 14 19V9q0-.424.287-.713A.97.97 0 0 1 15 8h6q.424 0 .712.287Q22 8.576 22 9v10q0 .424-.288.712A.97.97 0 0 1 21 20zm1-3h4v-7h-4z"
|
||||
/>
|
||||
</svg>
|
||||
Use another device
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Can't confirm?
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove this device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`CompleteSecurity Allows verifying with recovery key if one is available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AuthPage"
|
||||
>
|
||||
<div
|
||||
class="mx_AuthPage_modal"
|
||||
style="position: relative;"
|
||||
>
|
||||
<main
|
||||
aria-live="polite"
|
||||
class="mx_AuthPage_modalContent"
|
||||
style="display: flex; z-index: 1; border-radius: inherit;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_border _glass_sepwu_8"
|
||||
>
|
||||
<div
|
||||
class="mx_CompleteSecurityBody"
|
||||
>
|
||||
<h1
|
||||
class="mx_CompleteSecurity_header"
|
||||
>
|
||||
<div
|
||||
aria-label="Skip verification for now"
|
||||
class="mx_AccessibleButton mx_CompleteSecurity_skip"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_CompleteSecurity_body"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<div
|
||||
class="mx_EncryptionCard_header"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Confirm your digital identity
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_EncryptionCard_emphasisedContent"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span>
|
||||
Verify this device to set up secure messaging
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/help#encryption-device-verification"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_EncryptionCard_buttons"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Use recovery key
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Can't confirm?
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Remove this device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="mx_AuthFooter"
|
||||
role="contentinfo"
|
||||
>
|
||||
<a
|
||||
href="https://element.io/blog"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://mastodon.matrix.org/@Element"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/element-hq/element-web"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by Matrix
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`<LoginSplashView /> Renders a spinner 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginSplashView_splashButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LoginSplashView /> Renders an error message 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MatrixChat_splash"
|
||||
>
|
||||
<div
|
||||
class="mx_LoginSplashView_syncError"
|
||||
>
|
||||
<div>
|
||||
Unable to connect to Homeserver. Retrying…
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<svg
|
||||
aria-label="Loading…"
|
||||
class="_icon_1855a_18"
|
||||
data-testid="spinner"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LoginSplashView_splashButtons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
Reference in New Issue
Block a user