feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`showError should match snapshot 1`] = `
|
||||
<div
|
||||
id="matrixchat"
|
||||
>
|
||||
<div
|
||||
class="mx_ErrorView cpd-theme-light"
|
||||
>
|
||||
<img
|
||||
alt="Element"
|
||||
class="mx_ErrorView_logo"
|
||||
height="160"
|
||||
src="themes/element/img/logos/element-app-logo.png"
|
||||
/>
|
||||
<div
|
||||
class="mx_ErrorView_container"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Error title
|
||||
</h1>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
msg1
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
msg2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`showIncompatibleBrowser should match snapshot 1`] = `
|
||||
<div
|
||||
id="matrixchat"
|
||||
>
|
||||
<div
|
||||
class="mx_ErrorView cpd-theme-light"
|
||||
>
|
||||
<img
|
||||
alt="Element"
|
||||
class="mx_ErrorView_logo"
|
||||
height="160"
|
||||
src="themes/element/img/logos/element-app-logo.png"
|
||||
/>
|
||||
<div
|
||||
class="mx_ErrorView_container"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Element does not support this browser
|
||||
</h1>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
Element uses some browser features which are not available in your current browser. If you continue, some features may stop working and there is a risk that you may lose data in the future.
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
|
||||
>
|
||||
<span>
|
||||
For the best experience, use
|
||||
<a
|
||||
href="https://google.com/chrome"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Chrome
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
href="https://firefox.com"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Firefox
|
||||
</a>
|
||||
,
|
||||
<a
|
||||
href="https://microsoft.com/edge"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Edge
|
||||
</a>
|
||||
, or
|
||||
<a
|
||||
href="https://apple.com/safari"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Safari
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer mx_ErrorView_buttons"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
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>
|
||||
Learn more
|
||||
</button>
|
||||
<button
|
||||
class="_button_1nw83_8"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_13qwf_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Use Element Desktop instead
|
||||
</h2>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/macos/Element.dmg"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.099 2.4a4.1 4.1 0 0 1-1.057 3.073c-.747.863-1.878 1.36-3.07 1.348-.075-1.081.315-2.146 1.085-2.96.78-.825 1.866-1.346 3.042-1.461m3.767 6.54c-1.37.783-2.214 2.163-2.234 3.657.002 1.69 1.092 3.215 2.768 3.873a9.4 9.4 0 0 1-1.44 2.723c-.848 1.178-1.737 2.329-3.149 2.35-.671.015-1.124-.165-1.596-.351-.493-.195-1.006-.398-1.809-.398-.852 0-1.388.21-1.905.412-.447.174-.88.343-1.49.367-1.343.046-2.37-1.258-3.25-2.425-1.756-2.383-3.124-6.716-1.29-9.664.86-1.437 2.471-2.349 4.241-2.402.763-.015 1.494.258 2.135.497.49.183.929.347 1.287.347.315 0 .74-.157 1.237-.34.78-.288 1.737-.64 2.71-.545 1.514.044 2.917.748 3.785 1.9"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Mac
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
|
||||
/>
|
||||
</svg>
|
||||
Windows (64-bit)
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://packages.element.io/desktop/install/win32/arm64/Element%20Setup.exe"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
|
||||
/>
|
||||
</svg>
|
||||
Windows (ARM 64-bit)
|
||||
</a>
|
||||
<a
|
||||
class="_button_1nw83_8 _has-icon_1nw83_60"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
href="https://element.io/download#linux"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#cpd_LinuxIcon_a)"
|
||||
>
|
||||
<path
|
||||
d="M13.206 20.644a2.7 2.7 0 0 1-1.097.248 2.8 2.8 0 0 1-1.567-.495c-.153.38-.392.723-.7 1a2.8 2.8 0 0 1-1.078.604h6.141a2.8 2.8 0 0 1-.99-.51 2.8 2.8 0 0 1-.709-.847M6.71 16.673a.2.2 0 0 0 .067-.03q.056-.49.13-.94c-.01-.169.017-.338.08-.495.348-1.904.834-3.24 1.504-4.052a.13.13 0 0 1 .18-.018.123.123 0 0 1 .018.174 4 4 0 0 0-.203.272q-.764 1.113-1.199 3.389h.03a.6.6 0 0 1 .166 0c.198.042.508.203.985.67.061-2.72 1.661-4.913 3.642-4.913 1.748 0 3.208 1.715 3.556 3.997a1.7 1.7 0 0 1 .762-.742.8.8 0 0 1 .254-.047 7.5 7.5 0 0 0-.948-2.342 4 4 0 0 0-.2-.272.12.12 0 0 1-.03-.09.12.12 0 0 1 .048-.083.13.13 0 0 1 .092-.028.13.13 0 0 1 .085.045c.508.624.93 1.557 1.245 2.822.678.247.696 1.361.71 2.267 0 .43 0 .877.105.99.104.114.325.09.635-.04.036-.393.053-.742.063-1.036v-1.082c0-2.475-3.266-6.755-3.266-6.755l-.378-3.218c0-2.953-2.733-2.926-2.733-2.926s-2.743-.027-2.743 2.914l-.365 3.23s-3.267 4.277-3.267 6.755v.31s-.012.299 0 .767v.227c.414.154.839.315.976.28m5.388-10.41c.761.068 1.975.247 2.031.613.04.282-.429.822-.523.926-.195.215-.876.92-1.49.92-.615 0-1.296-.705-1.49-.92-.093-.104-.563-.644-.525-.926.046-.374 1.273-.545 1.996-.614"
|
||||
/>
|
||||
<path
|
||||
d="M12.111 8.475c.333 0 .843-.33 1.298-.837.348-.384.475-.666.465-.73-.064-.127-.894-.317-1.778-.401-.856.084-1.687.274-1.75.406.091.275.252.523.467.722.457.51.965.84 1.298.84m.684 10.664c-.292-2.607-.023-3.146.206-3.324a.42.42 0 0 1 .4-.057c.23.11.431.271.587.47.287.304.488.495.674.391.131-.077.32-.495.507-.879.097-.205.196-.425.305-.636-.254-2.33-1.65-4.126-3.355-4.126-1.872 0-3.393 2.166-3.393 4.827v.096c.31.317.68.743 1.133 1.3q.266.328.487.685a2.7 2.7 0 0 1 .29 2.258c.426.312.943.485 1.476.495.341-.002.679-.073.99-.21a2.6 2.6 0 0 1-.16-.46 7 7 0 0 1-.147-.83"
|
||||
/>
|
||||
<path
|
||||
d="M9.651 17.35c-.355-.44-.657-.789-.916-1.071l-.13-.139-.132-.138c-.587-.594-.897-.758-1.06-.785a.2.2 0 0 0-.068 0 .15.15 0 0 0-.114.082v.017a.9.9 0 0 0-.058.406v.327c.034.25-.019.505-.15.723a.4.4 0 0 1-.087.071l-.053.032a.6.6 0 0 1-.107.043H6.75c-.211.037-.554-.084-.988-.248l-.127-.05c-.544-.21-1.179-.452-1.644-.452a.63.63 0 0 0-.553.232c-.34.495.421 1.273 1.036 1.901.378.389.675.693.708.921.059.42-.393.582-.792.725a1.5 1.5 0 0 0-.564.278c-.06.069-.058.116-.045.15.055.191.507.61 2.897 1.285q.398.112.806.178c.49.073.992.01 1.448-.182a2.64 2.64 0 0 0 1.13-.902q.165-.245.271-.522l.05-.121c0-.042.024-.09.037-.131a2.48 2.48 0 0 0-.293-1.98 7 7 0 0 0-.475-.65m9.618-.17c-.212.02-.418.084-.604.186l-.277.117c-.305.113-.61.168-.807-.095a.6.6 0 0 1-.094-.227 3.6 3.6 0 0 1-.056-.592v-.322c0-.72-.026-1.586-.373-1.925a.5.5 0 0 0-.255-.14h-.038a.5.5 0 0 0-.254.035c-.3.12-.56.496-.79.936l-.109.215-.117.248-.104.225c-.223.495-.398.874-.601.99-.376.216-.704-.13-.991-.438a1.5 1.5 0 0 0-.48-.4.2.2 0 0 0-.076-.016q-.048 0-.084.03c-.158.124-.384.69-.112 3.104q.044.4.137.792.052.207.137.404.027.056.056.111c.02.037.033.074.054.111.208.377.51.696.879.928a2.66 2.66 0 0 0 2.504.171c.398-.18.743-.454 1.003-.8a7 7 0 0 0 .45-.675c1.218-2.116 1.241-2.722 1.147-2.896a.15.15 0 0 0-.144-.077"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="cpd_LinuxIcon_a"
|
||||
>
|
||||
<path
|
||||
d="M3.167 2.19H20.5V22H3.165z"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
Linux
|
||||
</a>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Or use our mobile app
|
||||
</h2>
|
||||
<div
|
||||
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<a
|
||||
href="https://apps.apple.com/app/vector/id1083446067"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="Apple App Store"
|
||||
height="64"
|
||||
src="themes/element/img/download/apple.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=im.vector.app"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="Google Play Store"
|
||||
height="64"
|
||||
src="themes/element/img/download/google.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="F-Droid"
|
||||
height="64"
|
||||
src="themes/element/img/download/fdroid.svg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @jest-environment jest-fixed-jsdom
|
||||
* @jest-environment-options {"url": "https://app.element.io/#/room/#room:server"}
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2026 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 fetchMock from "@fetch-mock/jest";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
|
||||
import { loadApp } from "../../../src/vector/app.tsx";
|
||||
import SdkConfig from "../../../src/SdkConfig.ts";
|
||||
import PlatformPeg from "../../../src/PlatformPeg.ts";
|
||||
import { mockPlatformPeg, unmockPlatformPeg } from "../../test-utils";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
const defaultConfig = {
|
||||
default_hs_url: "https://synapse",
|
||||
};
|
||||
const issuer = "https://auth.org/";
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
describe("sso_redirect_options", () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
// Stable stub
|
||||
getRandomValues: (arr: Uint8Array) => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = i;
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
SdkConfig.reset();
|
||||
mockPlatformPeg({ getDefaultDeviceDisplayName: jest.fn(), startSingleSignOn: jest.fn() });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
describe("immediate", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
...defaultConfig,
|
||||
sso_redirect_options: { immediate: true },
|
||||
// Avoid testing dynamic client registration
|
||||
oidc_static_clients: { [issuer]: { client_id: "12345" } },
|
||||
});
|
||||
// Signal we support v1.1 to pass the minimum js-sdk compatibility bar
|
||||
// Signal we support v1.15 to use stable Native OIDC support
|
||||
fetchMock.get("https://synapse/_matrix/client/versions", { versions: ["v1.1", "v1.15"] });
|
||||
});
|
||||
|
||||
it("should redirect for legacy SSO", async () => {
|
||||
fetchMock.getOnce("https://synapse/_matrix/client/v3/login", {
|
||||
flows: [{ stages: ["m.login.sso"] }],
|
||||
});
|
||||
|
||||
const startSingleSignOnSpy = jest.spyOn(PlatformPeg.get()!, "startSingleSignOn");
|
||||
|
||||
await loadApp({}, jest.fn());
|
||||
expect(startSingleSignOnSpy).toHaveBeenCalledWith(expect.any(MatrixClient), "sso", "/room/#room:server");
|
||||
});
|
||||
|
||||
it("should redirect for native OIDC", async () => {
|
||||
const authConfig = { ...makeDelegatedAuthConfig(issuer), response_modes_supported: ["query", "fragment"] };
|
||||
fetchMock.get("https://synapse/_matrix/client/v1/auth_metadata", authConfig);
|
||||
fetchMock.get(`${authConfig.issuer}.well-known/openid-configuration`, authConfig);
|
||||
fetchMock.get(authConfig.jwks_uri!, { keys: [] });
|
||||
|
||||
const startOidcLoginSpy = jest.spyOn(window.location, "href", "set");
|
||||
|
||||
await loadApp({}, jest.fn());
|
||||
expect(startOidcLoginSpy).toHaveBeenCalledWith(
|
||||
"https://auth.org/auth?client_id=12345&redirect_uri=https%3A%2F%2Fapp.element.io%2F%3Fno_universal_links%3Dtrue&response_type=code&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AwKpa6hpi3Y&nonce=38QgU2Pomx&state=10000000100040008000100000000000&code_challenge=awE81eIsGff70JahvrTqWRbGKLI10ooyo_Xm1sxuZvU&code_challenge_method=S256&response_mode=fragment",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
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 fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { getVectorConfig } from "../../../src/vector/getconfig";
|
||||
|
||||
describe("getVectorConfig()", () => {
|
||||
const elementDomain = "app.element.io";
|
||||
const now = 1234567890;
|
||||
const specificConfig = {
|
||||
brand: "specific",
|
||||
};
|
||||
const generalConfig = {
|
||||
brand: "general",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: { href: `https://${elementDomain}`, hostname: elementDomain },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// stable value for cachebuster
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
jest.clearAllMocks();
|
||||
fetchMock.removeRoutes();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("requests specific config for document domain", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json*", specificConfig);
|
||||
fetchMock.getOnce("express:/config.json*", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(specificConfig);
|
||||
});
|
||||
|
||||
it("adds trailing slash to relativeLocation when not an empty string", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", specificConfig);
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig("..")).resolves.toEqual(specificConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config succeeds but is empty", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", {});
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config 404s", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { status: 404 });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config is fetched from a file and is empty", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", 0);
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config returns a non-200 status", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { status: 401 });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config returns an error", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err1" });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("rejects with an error when general config rejects", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
|
||||
fetchMock.getOnce("express:/config.json", { throws: "err-general" });
|
||||
|
||||
await expect(getVectorConfig()).rejects.toBe("err-general");
|
||||
});
|
||||
|
||||
it("rejects with an error when config is invalid JSON", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
|
||||
fetchMock.getOnce("express:/config.json", '{"invalid": "json",}');
|
||||
|
||||
// We can't assert it'll be a SyntaxError as node-fetch behaves differently
|
||||
// https://github.com/wheresrhys/fetch-mock/issues/270
|
||||
await expect(getVectorConfig()).rejects.toThrow("in JSON at position 19");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @jest-environment jest-fixed-jsdom
|
||||
* @jest-environment-options {"url": "https://app.element.io/?loginToken=123&no_universal_links&something_else=value#/home?state=abc&code=xyz"}
|
||||
*/
|
||||
|
||||
/*
|
||||
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 fetchMock from "@fetch-mock/jest";
|
||||
import { waitFor, screen } from "jest-matrix-react";
|
||||
|
||||
import { loadApp, showError, showIncompatibleBrowser } from "../../../src/vector/init.tsx";
|
||||
import SdkConfig from "../../../src/SdkConfig.ts";
|
||||
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
|
||||
import { parseAppUrl } from "../../../src/vector/url_utils.ts";
|
||||
|
||||
function setUpMatrixChatDiv() {
|
||||
document.getElementById("matrixchat")?.remove();
|
||||
const div = document.createElement("div");
|
||||
div.id = "matrixchat";
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
describe("showIncompatibleBrowser", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
await showIncompatibleBrowser(jest.fn());
|
||||
await screen.findByText("Element does not support this browser");
|
||||
expect(document.getElementById("matrixchat")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showError", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
it("should match snapshot", async () => {
|
||||
await showError("Error title", ["msg1", "msg2"]);
|
||||
await screen.findByText("Error title");
|
||||
expect(document.getElementById("matrixchat")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadApp", () => {
|
||||
beforeEach(setUpMatrixChatDiv);
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("https://matrix.org/_matrix/client/versions", { versions: ["v1.6"] });
|
||||
SdkConfig.put({ default_server_config: { "m.homeserver": { base_url: "https://matrix.org" } } });
|
||||
});
|
||||
|
||||
it("should set window.matrixChat to the MatrixChat instance", async () => {
|
||||
await loadApp({});
|
||||
await waitFor(() => expect(window.matrixChat).toBeInstanceOf(MatrixChat));
|
||||
});
|
||||
|
||||
it("should pass onTokenLoginCompleted which strips searchParams & fragment to MatrixChat", async () => {
|
||||
const spy = jest.spyOn(window.history, "replaceState");
|
||||
|
||||
await loadApp({});
|
||||
await waitFor(() => expect(window.matrixChat).toBeInstanceOf(MatrixChat));
|
||||
window.matrixChat!.props.onTokenLoginCompleted(parseAppUrl(window.location).params, "/home");
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(null, "", "https://app.element.io/?something_else=value#/home");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,531 @@
|
||||
/*
|
||||
Copyright 2024-2025 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, type MockedObject } from "jest-mock";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { UpdateCheckStatus } from "../../../../src/BasePlatform";
|
||||
import { Action } from "../../../../src/dispatcher/actions";
|
||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import * as rageshake from "../../../../src/rageshake/rageshake";
|
||||
import { BreadcrumbsStore } from "../../../../src/stores/BreadcrumbsStore";
|
||||
import Modal from "../../../../src/Modal";
|
||||
import DesktopCapturerSourcePicker from "../../../../src/components/views/elements/DesktopCapturerSourcePicker";
|
||||
import ElectronPlatform from "../../../../src/vector/platform/ElectronPlatform";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import ToastStore from "../../../../src/stores/ToastStore.ts";
|
||||
|
||||
jest.mock("../../../../src/rageshake/rageshake", () => ({
|
||||
flush: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("ElectronPlatform", () => {
|
||||
const initialiseValues = jest.fn().mockReturnValue({
|
||||
protocol: "io.element.desktop",
|
||||
sessionId: "session-id",
|
||||
config: { _config: true },
|
||||
supportedSettings: { setting1: false, setting2: true },
|
||||
supportsBadgeOverlay: false,
|
||||
});
|
||||
const defaultUserAgent =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
|
||||
const mockElectron = {
|
||||
on: jest.fn(),
|
||||
send: jest.fn(),
|
||||
initialise: initialiseValues,
|
||||
setSettingValue: jest.fn().mockResolvedValue(undefined),
|
||||
getSettingValue: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as MockedObject<Electron>;
|
||||
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const dispatchFireSpy = jest.spyOn(dispatcher, "fire");
|
||||
const logSpy = jest.spyOn(logger, "log").mockImplementation(() => {});
|
||||
|
||||
const userId = "@alice:server.org";
|
||||
const deviceId = "device-id";
|
||||
|
||||
beforeEach(() => {
|
||||
window.electron = mockElectron;
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(window, "navigator", { value: { userAgent: defaultUserAgent }, writable: true });
|
||||
});
|
||||
|
||||
const getElectronEventHandlerCall = (
|
||||
eventType: string,
|
||||
): [type: string, handler: (...args: any) => void] | undefined =>
|
||||
mockElectron.on.mock.calls.find(([type]) => type === eventType);
|
||||
|
||||
it("flushes rageshake before quitting", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("before-quit")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler();
|
||||
|
||||
expect(logSpy).toHaveBeenCalled();
|
||||
expect(rageshake.flush).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should load config", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await expect(platform.getConfig()).resolves.toEqual({ _config: true });
|
||||
});
|
||||
|
||||
it("should return oidc client state as expected", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.getConfig();
|
||||
expect(platform.getOidcClientState()).toMatchInlineSnapshot(`":element-desktop-ssoid:session-id"`);
|
||||
});
|
||||
|
||||
it("dispatches view settings action on preferences event", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("preferences")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler();
|
||||
|
||||
expect(dispatchFireSpy).toHaveBeenCalledWith(Action.ViewUserSettings);
|
||||
});
|
||||
|
||||
it("creates a modal on openDesktopCapturerSourcePicker", async () => {
|
||||
const plat = new ElectronPlatform();
|
||||
Modal.createDialog = jest.fn();
|
||||
|
||||
// @ts-ignore mock
|
||||
mocked(Modal.createDialog).mockReturnValue({
|
||||
finished: new Promise((r) => r(["source"])),
|
||||
});
|
||||
|
||||
let res: () => void;
|
||||
const waitForIPCSend = new Promise<void>((r) => {
|
||||
res = r;
|
||||
});
|
||||
// @ts-ignore mock
|
||||
jest.spyOn(plat.ipc, "call").mockImplementation(() => {
|
||||
res();
|
||||
});
|
||||
|
||||
const [event, handler] = getElectronEventHandlerCall("openDesktopCapturerSourcePicker")!;
|
||||
handler();
|
||||
|
||||
await waitForIPCSend;
|
||||
|
||||
expect(event).toBeTruthy();
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(DesktopCapturerSourcePicker);
|
||||
// @ts-ignore mock
|
||||
expect(plat.ipc.call).toHaveBeenCalledWith("callDisplayMediaCallback", "source");
|
||||
});
|
||||
|
||||
it("should show a toast when showToast is fired", async () => {
|
||||
new ElectronPlatform();
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
action: Action.ClientStarted,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const spy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
|
||||
|
||||
const [event, handler] = getElectronEventHandlerCall("showToast")!;
|
||||
handler({} as any, { title: "title", description: "description" });
|
||||
|
||||
expect(event).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "title",
|
||||
props: expect.objectContaining({ description: "description" }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("updates", () => {
|
||||
it("dispatches on check updates action", () => {
|
||||
new ElectronPlatform();
|
||||
const [event, handler] = getElectronEventHandlerCall("check_updates")!;
|
||||
// correct event bound
|
||||
expect(event).toBeTruthy();
|
||||
|
||||
handler({}, true);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.Downloading,
|
||||
});
|
||||
});
|
||||
|
||||
it("dispatches on check updates action when update not available", () => {
|
||||
new ElectronPlatform();
|
||||
const [, handler] = getElectronEventHandlerCall("check_updates")!;
|
||||
|
||||
handler({}, false);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.NotAvailable,
|
||||
});
|
||||
});
|
||||
|
||||
it("starts update check", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.startUpdateCheck();
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("check_updates");
|
||||
});
|
||||
|
||||
it("installs update", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.installUpdate();
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("install_update");
|
||||
});
|
||||
});
|
||||
|
||||
it("returns human readable name", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.getHumanReadableName()).toEqual("Electron Platform");
|
||||
});
|
||||
|
||||
describe("getDefaultDeviceDisplayName", () => {
|
||||
it.each([
|
||||
[
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
|
||||
"Element Desktop: macOS",
|
||||
],
|
||||
[
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36",
|
||||
"Element Desktop: Windows",
|
||||
],
|
||||
["Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: Linux"],
|
||||
["Mozilla/5.0 (X11; FreeBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: FreeBSD"],
|
||||
["Mozilla/5.0 (X11; OpenBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: OpenBSD"],
|
||||
["Mozilla/5.0 (X11; SunOS i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: SunOS"],
|
||||
["custom user agent", "Element Desktop: Unknown"],
|
||||
])("%s = %s", (userAgent, result) => {
|
||||
Object.defineProperty(window, "navigator", { value: { userAgent }, writable: true });
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.getDefaultDeviceDisplayName()).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true for needsUrlTooltips", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.needsUrlTooltips()).toBe(true);
|
||||
});
|
||||
|
||||
it("should override browser shortcuts", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.overrideBrowserShortcuts()).toBe(true);
|
||||
});
|
||||
|
||||
it("allows overriding native context menus", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.allowOverridingNativeContextMenus()).toBe(true);
|
||||
});
|
||||
|
||||
it("indicates support for desktop capturer", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsDesktopCapturer()).toBe(true);
|
||||
});
|
||||
|
||||
it("indicates no support for jitsi screensharing", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsJitsiScreensharing()).toBe(false);
|
||||
});
|
||||
|
||||
describe("notifications", () => {
|
||||
it("indicates support for notifications", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("may send notifications", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.maySendNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("pretends to request notification permission", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
const result = await platform.requestNotificationPermission();
|
||||
expect(result).toEqual("granted");
|
||||
});
|
||||
|
||||
it("creates a loud notification", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.loudNotification(new MatrixEvent(), new Room("!room:server", {} as any, userId));
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("loudNotification");
|
||||
});
|
||||
|
||||
it("sets notification count when count is changing", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.setNotificationCount(0);
|
||||
// not called because matches internal notificaiton count
|
||||
expect(mockElectron.send).not.toHaveBeenCalledWith("setBadgeCount", 0);
|
||||
platform.setNotificationCount(1);
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("setBadgeCount", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("spellcheck", () => {
|
||||
it("indicates support for spellcheck settings", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
expect(platform.supportsSpellCheckSettings()).toBe(true);
|
||||
});
|
||||
|
||||
it("gets available spellcheck languages", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.getAvailableSpellCheckLanguages();
|
||||
|
||||
const [channel, { name }] = mockElectron.send.mock.calls[0];
|
||||
expect(channel).toEqual("ipcCall");
|
||||
expect(name).toEqual("getAvailableSpellCheckLanguages");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickle key", () => {
|
||||
it("makes correct ipc call to get pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.getPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("getPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
|
||||
it("makes correct ipc call to create pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.createPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("createPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
|
||||
it("makes correct ipc call to destroy pickle key", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
mockElectron.send.mockClear();
|
||||
platform.destroyPickleKey(userId, deviceId);
|
||||
|
||||
const [, { name, args }] = mockElectron.send.mock.calls[0];
|
||||
expect(name).toEqual("destroyPickleKey");
|
||||
expect(args).toEqual([userId, deviceId]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("versions", () => {
|
||||
it("calls install update", () => {
|
||||
const platform = new ElectronPlatform();
|
||||
platform.installUpdate();
|
||||
|
||||
expect(mockElectron.send).toHaveBeenCalledWith("install_update");
|
||||
});
|
||||
});
|
||||
|
||||
describe("breadcrumbs", () => {
|
||||
it("should send breadcrumb updates over the IPC", () => {
|
||||
const spy = jest.spyOn(BreadcrumbsStore.instance, "on");
|
||||
new ElectronPlatform();
|
||||
const cb = spy.mock.calls[0][1];
|
||||
cb();
|
||||
|
||||
expect(mockElectron.send).toHaveBeenCalledWith(
|
||||
"ipcCall",
|
||||
expect.objectContaining({
|
||||
name: "breadcrumbs",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticated media", () => {
|
||||
it("should respond to relevant ipc requests", async () => {
|
||||
const cli = stubClient();
|
||||
mocked(cli.getAccessToken).mockReturnValue("access_token");
|
||||
mocked(cli.getHomeserverUrl).mockReturnValue("homeserver_url");
|
||||
mocked(cli.getVersions).mockResolvedValue({
|
||||
versions: ["v1.1"],
|
||||
unstable_features: {},
|
||||
});
|
||||
|
||||
new ElectronPlatform();
|
||||
|
||||
const userAccessTokenCall = mockElectron.on.mock.calls.find((call) => call[0] === "userAccessToken");
|
||||
userAccessTokenCall;
|
||||
const userAccessTokenResponse = mockElectron.send.mock.calls.find((call) => call[0] === "userAccessToken");
|
||||
expect(userAccessTokenResponse![1]).toBe("access_token");
|
||||
|
||||
const homeserverUrlCall = mockElectron.on.mock.calls.find((call) => call[0] === "homeserverUrl");
|
||||
homeserverUrlCall;
|
||||
const homeserverUrlResponse = mockElectron.send.mock.calls.find((call) => call[0] === "homeserverUrl");
|
||||
expect(homeserverUrlResponse![1]).toBe("homeserver_url");
|
||||
|
||||
const serverSupportedVersionsCall = mockElectron.on.mock.calls.find(
|
||||
(call) => call[0] === "serverSupportedVersions",
|
||||
);
|
||||
await (serverSupportedVersionsCall as unknown as Promise<unknown>);
|
||||
const serverSupportedVersionsResponse = mockElectron.send.mock.calls.find(
|
||||
(call) => call[0] === "serverSupportedVersions",
|
||||
);
|
||||
expect(serverSupportedVersionsResponse![1]).toEqual({ versions: ["v1.1"], unstable_features: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings", () => {
|
||||
let platform: ElectronPlatform;
|
||||
beforeAll(async () => {
|
||||
window.electron = mockElectron;
|
||||
platform = new ElectronPlatform();
|
||||
await platform.getConfig(); // await init
|
||||
});
|
||||
|
||||
it("supportsSetting should return true for the platform", () => {
|
||||
expect(platform.supportsSetting()).toBe(true);
|
||||
});
|
||||
|
||||
it("supportsSetting should return true for available settings", () => {
|
||||
expect(platform.supportsSetting("setting2")).toBe(true);
|
||||
});
|
||||
|
||||
it("supportsSetting should return false for unavailable settings", () => {
|
||||
expect(platform.supportsSetting("setting1")).toBe(false);
|
||||
});
|
||||
|
||||
it("should read setting value over ipc", async () => {
|
||||
mockElectron.getSettingValue.mockResolvedValue("value");
|
||||
await expect(platform.getSettingValue("setting2")).resolves.toEqual("value");
|
||||
expect(mockElectron.getSettingValue).toHaveBeenCalledWith("setting2");
|
||||
});
|
||||
|
||||
it("should write setting value over ipc", async () => {
|
||||
await platform.setSettingValue("setting2", "newValue");
|
||||
expect(mockElectron.setSettingValue).toHaveBeenCalledWith("setting2", "newValue");
|
||||
});
|
||||
});
|
||||
|
||||
it("should forward call_state dispatcher events via ipc", async () => {
|
||||
new ElectronPlatform();
|
||||
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
action: "call_state",
|
||||
state: "connected",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const ipcMessage = mockElectron.send.mock.calls.find((call) => call[0] === "app_onAction");
|
||||
expect(ipcMessage![1]).toEqual({
|
||||
action: "call_state",
|
||||
state: "connected",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notification overlay badges", () => {
|
||||
beforeEach(() => {
|
||||
initialiseValues.mockReturnValue({
|
||||
protocol: "io.element.desktop",
|
||||
sessionId: "session-id",
|
||||
config: { _config: true },
|
||||
supportsBadgeOverlay: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should send a badge with a notification count", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const ipcMessage = mockElectron.send.mock.lastCall;
|
||||
expect(ipcMessage?.[1]).toEqual(1);
|
||||
expect(ipcMessage?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update badge and skip duplicates", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
platform.setNotificationCount(1); // Test that duplicates do not fire.
|
||||
platform.setNotificationCount(2);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageA, ipcMessageB] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(1);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(2);
|
||||
expect(ipcMessageB?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
});
|
||||
});
|
||||
it("should remove badge when notification count zeros", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setNotificationCount(1);
|
||||
platform.setNotificationCount(0); // Test that duplicates do not fire.
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageB, ipcMessageA] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(1);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(0);
|
||||
expect(ipcMessageB?.[2]).toBeNull();
|
||||
});
|
||||
});
|
||||
it("should show an error badge when the application errors", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setErrorStatus(true);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const ipcMessage = mockElectron.send.mock.calls.find((call) => call[0] === "setBadgeCount");
|
||||
|
||||
expect(ipcMessage?.[1]).toEqual(0);
|
||||
expect(ipcMessage?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
expect(ipcMessage?.[3]).toEqual(true);
|
||||
});
|
||||
});
|
||||
it("should restore after error is resolved", async () => {
|
||||
const platform = new ElectronPlatform();
|
||||
await platform.initialised;
|
||||
platform.setErrorStatus(true);
|
||||
platform.setErrorStatus(false);
|
||||
// Badges are sent asynchronously
|
||||
await waitFor(() => {
|
||||
const [ipcMessageB, ipcMessageA] = mockElectron.send.mock.calls.filter(
|
||||
(call) => call[0] === "setBadgeCount",
|
||||
);
|
||||
|
||||
expect(ipcMessageA?.[1]).toEqual(0);
|
||||
expect(ipcMessageA?.[2].constructor.name).toEqual("ArrayBuffer");
|
||||
expect(ipcMessageA?.[3]).toEqual(true);
|
||||
|
||||
expect(ipcMessageB?.[1]).toEqual(0);
|
||||
expect(ipcMessageB?.[2]).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 { mocked } from "jest-mock";
|
||||
|
||||
import PWAPlatform from "../../../../src/vector/platform/PWAPlatform";
|
||||
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
||||
|
||||
jest.mock("../../../../src/vector/platform/WebPlatform");
|
||||
|
||||
describe("PWAPlatform", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("setNotificationCount", () => {
|
||||
it("should call Navigator::setAppBadge", () => {
|
||||
navigator.setAppBadge = jest.fn().mockResolvedValue(undefined);
|
||||
const platform = new PWAPlatform();
|
||||
expect(navigator.setAppBadge).not.toHaveBeenCalled();
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it("should no-op if the badge count isn't changing", () => {
|
||||
navigator.setAppBadge = jest.fn().mockResolvedValue(undefined);
|
||||
const platform = new PWAPlatform();
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
|
||||
platform.setNotificationCount(123);
|
||||
expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should fall back to WebPlatform::setNotificationCount if no Navigator::setAppBadge", () => {
|
||||
// @ts-ignore
|
||||
navigator.setAppBadge = undefined;
|
||||
const platform = new PWAPlatform();
|
||||
const superMethod = mocked(WebPlatform.prototype.setNotificationCount);
|
||||
expect(superMethod).not.toHaveBeenCalled();
|
||||
platform.setNotificationCount(123);
|
||||
expect(superMethod).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it("should handle Navigator::setAppBadge rejecting gracefully", () => {
|
||||
navigator.setAppBadge = jest.fn().mockRejectedValue(new Error());
|
||||
const platform = new PWAPlatform();
|
||||
expect(() => platform.setNotificationCount(123)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
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 fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import { UpdateCheckStatus } from "../../../../src/BasePlatform";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import WebPlatform from "../../../../src/vector/platform/WebPlatform";
|
||||
import ToastStore from "../../../../src/stores/ToastStore.ts";
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher.ts";
|
||||
import { emitPromise } from "../../../test-utils";
|
||||
import { Action } from "../../../../src/dispatcher/actions.ts";
|
||||
|
||||
describe("WebPlatform", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({
|
||||
...navigator,
|
||||
// @ts-expect-error - mocking readonly object
|
||||
serviceWorker: {
|
||||
register: jest.fn().mockResolvedValue({
|
||||
update: jest.fn(),
|
||||
}),
|
||||
addEventListener: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns human readable name", () => {
|
||||
const platform = new WebPlatform();
|
||||
expect(platform.getHumanReadableName()).toEqual("Web Platform");
|
||||
});
|
||||
|
||||
describe("service worker", () => {
|
||||
it("registers successfully", () => {
|
||||
new WebPlatform();
|
||||
expect(navigator.serviceWorker.register).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles errors", async () => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({
|
||||
serviceWorker: {
|
||||
// @ts-expect-error - mocking readonly object
|
||||
register: undefined,
|
||||
},
|
||||
});
|
||||
new WebPlatform();
|
||||
|
||||
defaultDispatcher.dispatch({ action: Action.ClientStarted });
|
||||
await emitPromise(ToastStore.sharedInstance(), "update");
|
||||
const toasts = ToastStore.sharedInstance().getToasts();
|
||||
expect(toasts).toHaveLength(1);
|
||||
expect(toasts[0].title).toEqual("Failed to load service worker");
|
||||
});
|
||||
});
|
||||
|
||||
it("should call reload on window location object", () => {
|
||||
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
||||
|
||||
const platform = new WebPlatform();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
platform.reload();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call reload to install update", () => {
|
||||
Object.defineProperty(window, "location", { value: { reload: jest.fn() }, writable: true });
|
||||
|
||||
const platform = new WebPlatform();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
platform.installUpdate();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("getDefaultDeviceDisplayName", () => {
|
||||
it.each([
|
||||
[
|
||||
"https://develop.element.io/#/room/!foo:bar",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/105.0.0.0 Safari/537.36",
|
||||
"develop.element.io: Chrome on macOS",
|
||||
],
|
||||
])("%s & %s = %s", (url, userAgent, result) => {
|
||||
jest.spyOn(global, "navigator", "get").mockReturnValue({ userAgent } as Navigator);
|
||||
Object.defineProperty(window, "location", { value: { href: url }, writable: true });
|
||||
const platform = new WebPlatform();
|
||||
expect(platform.getDefaultDeviceDisplayName()).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification support", () => {
|
||||
const mockNotification = {
|
||||
requestPermission: jest.fn(),
|
||||
permission: "notGranted",
|
||||
};
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
window.Notification = mockNotification;
|
||||
mockNotification.permission = "notGranted";
|
||||
});
|
||||
|
||||
it("supportsNotifications returns false when platform does not support notifications", () => {
|
||||
// @ts-ignore
|
||||
window.Notification = undefined;
|
||||
expect(new WebPlatform().supportsNotifications()).toBe(false);
|
||||
});
|
||||
|
||||
it("supportsNotifications returns true when platform supports notifications", () => {
|
||||
expect(new WebPlatform().supportsNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("maySendNotifications returns true when notification permissions are not granted", () => {
|
||||
expect(new WebPlatform().maySendNotifications()).toBe(false);
|
||||
});
|
||||
|
||||
it("maySendNotifications returns true when notification permissions are granted", () => {
|
||||
mockNotification.permission = "granted";
|
||||
expect(new WebPlatform().maySendNotifications()).toBe(true);
|
||||
});
|
||||
|
||||
it("requests notification permissions and returns result", async () => {
|
||||
mockNotification.requestPermission.mockImplementation((callback) => callback("test"));
|
||||
|
||||
const platform = new WebPlatform();
|
||||
const result = await platform.requestNotificationPermission();
|
||||
expect(result).toEqual("test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("app version", () => {
|
||||
const envVersion = process.env.VERSION;
|
||||
const prodVersion = "1.10.13";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = envVersion;
|
||||
});
|
||||
|
||||
it("should return true from canSelfUpdate()", async () => {
|
||||
const platform = new WebPlatform();
|
||||
const result = await platform.canSelfUpdate();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("getAppVersion returns normalized app version", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const version = await platform.getAppVersion();
|
||||
expect(version).toEqual(prodVersion);
|
||||
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = `v${prodVersion}`;
|
||||
const version2 = await platform.getAppVersion();
|
||||
// v prefix removed
|
||||
expect(version2).toEqual(prodVersion);
|
||||
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = `version not like semver`;
|
||||
const notSemverVersion = await platform.getAppVersion();
|
||||
expect(notSemverVersion).toEqual(`version not like semver`);
|
||||
});
|
||||
|
||||
describe("pollForUpdate()", () => {
|
||||
it("should return not available and call showNoUpdate when current version matches most recent version", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should strip v prefix from versions before comparing", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = prodVersion;
|
||||
fetchMock.getOnce("end:/version", `v${prodVersion}`);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
// versions only differ by v prefix, no update
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(
|
||||
"should return ready and call showUpdate when current version " + "differs from most recent version",
|
||||
async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = "0.0.0"; // old version
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Ready });
|
||||
expect(showUpdate).toHaveBeenCalledWith("0.0.0", prodVersion);
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("should return ready without showing update when user registered in last 24", async () => {
|
||||
// @ts-ignore
|
||||
WebPlatform.VERSION = "0.0.0"; // old version
|
||||
jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(true);
|
||||
fetchMock.getOnce("end:/version", prodVersion);
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Ready });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return error when version check fails", async () => {
|
||||
fetchMock.getOnce("end:/version", { throws: "oups" });
|
||||
const platform = new WebPlatform();
|
||||
|
||||
const showUpdate = jest.fn();
|
||||
const showNoUpdate = jest.fn();
|
||||
const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
|
||||
|
||||
expect(result).toEqual({ status: UpdateCheckStatus.Error, detail: "Unknown Error" });
|
||||
expect(showUpdate).not.toHaveBeenCalled();
|
||||
expect(showNoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should return config from config.json", async () => {
|
||||
window.location.hostname = "domain.com";
|
||||
fetchMock.get(/config\.json.*/, { brand: "test" });
|
||||
const platform = new WebPlatform();
|
||||
await expect(platform.getConfig()).resolves.toEqual(expect.objectContaining({ brand: "test" }));
|
||||
});
|
||||
|
||||
it("should re-render favicon when setting error status", () => {
|
||||
const platform = new WebPlatform();
|
||||
const spy = jest.spyOn(platform.favicon, "badge");
|
||||
platform.setErrorStatus(true);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), { bgColor: "#f00" });
|
||||
});
|
||||
|
||||
describe("getOidcCallbackUrl()", () => {
|
||||
it("should not include the 'updated' query param in the redirect URI", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
href: "https://element.example.com/?updated=1.12.12",
|
||||
origin: "https://element.example.com",
|
||||
pathname: "/",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
const platform = new WebPlatform();
|
||||
const url = platform.getOidcCallbackUrl();
|
||||
|
||||
expect(url.searchParams.has("updated")).toBe(false);
|
||||
expect(url.searchParams.get("no_universal_links")).toEqual("true");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2026 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 fetchMock from "@fetch-mock/jest";
|
||||
|
||||
import type { Mocked } from "jest-mock";
|
||||
import type { ConsoleLogger } from "../../../src/rageshake/rageshake";
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import "../../../src/vector/rageshakesetup";
|
||||
import { BugReportEndpointURLLocal } from "../../../src/IConfigOptions";
|
||||
|
||||
const RAGESHAKE_URL = "https://logs.example.org/logtome";
|
||||
|
||||
describe("mxSendRageshake", () => {
|
||||
let prevLogger: ConsoleLogger;
|
||||
beforeEach(() => {
|
||||
fetchMock.mockGlobal();
|
||||
SdkConfig.put({ bug_report_endpoint_url: RAGESHAKE_URL });
|
||||
fetchMock.postOnce(RAGESHAKE_URL, { status: 200, body: {} });
|
||||
|
||||
const mockConsoleLogger = {
|
||||
flush: jest.fn(),
|
||||
consume: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
} as unknown as Mocked<ConsoleLogger>;
|
||||
prevLogger = global.mx_rage_logger;
|
||||
mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n");
|
||||
global.mx_rage_logger = mockConsoleLogger;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.mx_rage_logger = prevLogger;
|
||||
jest.restoreAllMocks();
|
||||
fetchMock.unmockGlobal();
|
||||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it("Does not send a rageshake if the URL is not configured", async () => {
|
||||
SdkConfig.put({ bug_report_endpoint_url: undefined });
|
||||
await window.mxSendRageshake("test");
|
||||
expect(fetchMock).not.toHaveFetched();
|
||||
});
|
||||
|
||||
it.each(["", " ", undefined, null])("Does not send a rageshake if text is '%s'", async (text) => {
|
||||
await window.mxSendRageshake(text as string);
|
||||
expect(fetchMock).not.toHaveFetched();
|
||||
});
|
||||
|
||||
it("Sends a rageshake via URL", async () => {
|
||||
await window.mxSendRageshake("Hello world");
|
||||
expect(fetchMock).toHaveFetched(RAGESHAKE_URL);
|
||||
});
|
||||
|
||||
it("Provides a rageshake locally", async () => {
|
||||
SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal });
|
||||
const urlSpy = jest.spyOn(URL, "createObjectURL");
|
||||
await window.mxSendRageshake("Hello world");
|
||||
expect(fetchMock).not.toHaveFetched(RAGESHAKE_URL);
|
||||
expect(urlSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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 { getInitialScreenAfterLogin, init, onNewScreen } from "../../../src/vector/routing";
|
||||
import type MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
|
||||
|
||||
describe("onNewScreen", () => {
|
||||
it("should replace history if stripping via fields", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room:server?via=abc",
|
||||
replace: jest.fn(),
|
||||
assign: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
onNewScreen("room/!room:server");
|
||||
expect(window.location.assign).not.toHaveBeenCalled();
|
||||
expect(window.location.replace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not replace history if changing rooms", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room1:server?via=abc",
|
||||
replace: jest.fn(),
|
||||
assign: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
onNewScreen("room/!room2:server");
|
||||
expect(window.location.assign).toHaveBeenCalled();
|
||||
expect(window.location.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInitialScreenAfterLogin", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(sessionStorage.__proto__, "getItem").mockClear().mockReturnValue(null);
|
||||
jest.spyOn(sessionStorage.__proto__, "setItem").mockClear();
|
||||
});
|
||||
|
||||
const makeMockLocation = (hash = "") => {
|
||||
const url = new URL("https://test.org");
|
||||
url.hash = hash;
|
||||
return url as unknown as Location;
|
||||
};
|
||||
|
||||
describe("when current url has no hash", () => {
|
||||
it("does not set an initial screen in session storage", () => {
|
||||
getInitialScreenAfterLogin(makeMockLocation());
|
||||
expect(sessionStorage.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined when there is no initial screen in session storage", () => {
|
||||
expect(getInitialScreenAfterLogin(makeMockLocation())).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns initial screen from session storage", () => {
|
||||
const screen = {
|
||||
screen: "/room/!test",
|
||||
};
|
||||
jest.spyOn(sessionStorage.__proto__, "getItem").mockReturnValue(JSON.stringify(screen));
|
||||
expect(getInitialScreenAfterLogin(makeMockLocation())).toEqual(screen);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when current url has a hash", () => {
|
||||
it("sets an initial screen in session storage", () => {
|
||||
const hash = "/room/!test";
|
||||
getInitialScreenAfterLogin(makeMockLocation(hash));
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
"mx_screen_after_login",
|
||||
JSON.stringify({
|
||||
screen: "room/!test",
|
||||
params: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets an initial screen in session storage with params", () => {
|
||||
const hash = "/room/!test?param=test";
|
||||
getInitialScreenAfterLogin(makeMockLocation(hash));
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
"mx_screen_after_login",
|
||||
JSON.stringify({
|
||||
screen: "room/!test",
|
||||
params: { param: "test" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
delete window.matrixChat;
|
||||
});
|
||||
|
||||
it("should call showScreen on MatrixChat on hashchange", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room:server?via=abc",
|
||||
},
|
||||
});
|
||||
|
||||
window.matrixChat = {
|
||||
showScreen: jest.fn(),
|
||||
} as unknown as MatrixChat;
|
||||
|
||||
init();
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
|
||||
expect(window.matrixChat.showScreen).toHaveBeenCalledWith("room/!room:server", { via: "abc" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2020-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 { parseAppUrl, parseQsFromFragment, searchParamsToQueryDict } from "../../../src/vector/url_utils";
|
||||
|
||||
// @ts-ignore
|
||||
const location: Location = {
|
||||
hash: "",
|
||||
search: "",
|
||||
};
|
||||
|
||||
describe("parseQsFromFragment", () => {
|
||||
it("should parse correctly", () => {
|
||||
location.hash = "#/home?foo=bar";
|
||||
expect(parseQsFromFragment(location)).toEqual({
|
||||
location: "/home",
|
||||
params: new URLSearchParams({
|
||||
foo: "bar",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchParamsToQueryDict", () => {
|
||||
it("should handle arrays correctly", () => {
|
||||
const u = new URLSearchParams("a=b&b=c&c=d&a=e&a=f");
|
||||
expect(searchParamsToQueryDict(u)).toEqual({
|
||||
a: ["b", "e", "f"],
|
||||
b: "c",
|
||||
c: "d",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUrlParameters", () => {
|
||||
it("should parse legacy sso parameters from query", () => {
|
||||
const u = new URL("https://app.element.io?loginToken=foobar");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.legacy_sso?.loginToken).toEqual("foobar");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from fragment", () => {
|
||||
const u = new URL("https://app.element.io/#code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_fragment?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_fragment?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from query", () => {
|
||||
const u = new URL("https://app.element.io/?code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_query?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_query?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse guest parameters", () => {
|
||||
const u = new URL("https://app.element.io?foo=bar#/room/!roomId:server?guest_access_token=foobar");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.guest?.guest_access_token).toEqual("foobar");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user