Compare commits

..

18 Commits

Author SHA1 Message Date
Michael Telatynski
a52d4aed9f Specify node-version for setup-node action 2022-10-13 08:52:52 +01:00
Michael Telatynski
66798c75b5 Trim /version file response (#23473) 2022-10-13 05:52:38 +01:00
Michael Telatynski
2ef6abbfb8 Move from browser-request to fetch (#23427) 2022-10-12 18:59:10 +01:00
Michael Telatynski
326a1a9056 Make ErrorView & CompatibilityView scrollable (#23468) 2022-10-12 18:14:27 +01:00
Michael Telatynski
341b0b469e Make bash scripts exit on error rather than continue (#23467) 2022-10-12 16:15:51 +01:00
Kerry
0e8e472138 Device manager - tweak string formatting of default device name (#23457)
* tweak string formatting of default device name

* cheaters path to beating quality gate

* more electronplatform test coverage

* remove test that throw errors

* cover some more window.electrons

* more coverage

* empty line
2022-10-12 15:35:52 +02:00
RiotRobot
44eeb6fddc Reset matrix-react-sdk back to develop branch 2022-10-11 18:04:37 +01:00
RiotRobot
8383c021f1 Reset matrix-js-sdk back to develop branch 2022-10-11 18:04:19 +01:00
RiotRobot
7afa416ed5 Merge branch 'master' into develop
# Conflicts:
#	package.json
#	yarn.lock
2022-10-11 18:03:36 +01:00
RiotRobot
d1f7454dc0 Reset matrix-react-sdk back to develop branch 2022-10-11 14:10:12 +01:00
RiotRobot
3bf3237774 Reset matrix-js-sdk back to develop branch 2022-10-11 14:09:53 +01:00
RiotRobot
f0e6673fec Merge branch 'master' into develop 2022-10-11 14:09:34 +01:00
Šimon Brandner
8891698745 Add Element Call participant limit (#23431) 2022-10-07 22:00:38 +02:00
Šimon Brandner
ec4cc52b7e Add Element Call brand (#23443) 2022-10-07 19:32:12 +02:00
renovate[bot]
fb686ec5b6 Update dependency jest-mock to v29 (#23421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:18:56 +01:00
renovate[bot]
1d86a75a29 Update typescript-eslint monorepo to v5.39.0 (#23420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:18:33 +01:00
renovate[bot]
ee5b20e12e Update jest monorepo to v29.1.1 (#23418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 17:57:36 +01:00
renovate[bot]
c8184e2497 Update babel monorepo to v7.19.3 (#23417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 16:34:12 +00:00
25 changed files with 1001 additions and 668 deletions

View File

@@ -18,6 +18,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
- name: Install Dependencies
run: "./scripts/layered.sh"

View File

@@ -22,6 +22,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
- name: Install Dependencies
run: "./scripts/layered.sh"

View File

@@ -19,6 +19,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
- name: Install Dependencies
run: "./scripts/layered.sh"
@@ -39,6 +40,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
# Does not need branch matching as only analyses this layer
- name: Install Deps
@@ -56,6 +58,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
# Needs branch matching as it inherits .stylelintrc.js from matrix-react-sdk
- name: Install Dependencies
@@ -73,6 +76,7 @@ jobs:
- uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
- name: Install Deps
run: "scripts/layered.sh"

View File

@@ -21,6 +21,7 @@ jobs:
uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: 16
- name: Install Dependencies
run: "./scripts/layered.sh"

View File

@@ -45,7 +45,9 @@
"preferred_domain": "meet.element.io"
},
"element_call": {
"url": "https://call.element.io"
"url": "https://call.element.io",
"participant_limit": 8,
"brand": "Element Call"
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
}

View File

@@ -323,6 +323,10 @@ The VoIP and Jitsi options are:
and may be removed at any time without notice. Defaults to `https://call.element.io`.
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
- `participant_limit`: The maximum number of users who can join a call; if
this number is exceeded, the user will not be able to join a given call.
- `brand`: Optional name for the app. Defaults to `Element Call`. This is
used throughout the application in various strings/locations.
## Bug reporting

View File

@@ -61,8 +61,8 @@
"gfm.css": "^1.1.2",
"jsrsasign": "^10.5.25",
"katex": "^0.16.0",
"matrix-js-sdk": "20.1.0",
"matrix-react-sdk": "3.58.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop",
"matrix-widget-api": "^1.1.1",
"prop-types": "^15.7.2",
"react": "17.0.2",
@@ -119,18 +119,19 @@
"eslint-plugin-react-hooks": "^4.3.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fake-indexeddb": "^3.1.2",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^5.1.0",
"fs-extra": "^0.30.0",
"html-webpack-plugin": "^4.5.2",
"jest": "^29.0.0",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^29.0.0",
"jest-mock": "^27.5.1",
"jest-mock": "^29.0.0",
"jest-raw-loader": "^1.0.1",
"jest-sonar-reporter": "^2.0.0",
"json-loader": "^0.5.7",
"loader-utils": "^1.4.0",
"matrix-mock-request": "^2.0.0",
"matrix-mock-request": "^2.5.0",
"matrix-react-test-utils": "^0.2.3",
"matrix-web-i18n": "^1.3.0",
"mini-css-extract-plugin": "^1",
@@ -188,7 +189,6 @@
"\\.(gif|png|ttf|woff2)$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/svg.js",
"\\$webapp/i18n/languages.json": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/languages.json",
"^browser-request$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/browser-request.js",
"^react$": "<rootDir>/node_modules/react",
"^react-dom$": "<rootDir>/node_modules/react-dom",
"^matrix-js-sdk$": "<rootDir>/node_modules/matrix-js-sdk/src",

View File

@@ -36,10 +36,12 @@ limitations under the License.
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
width: 100%;
min-height: 100%;
height: auto;
color: #000;
width: 100%;
height: 100%;
overflow: auto;
padding: 0 20px;
box-sizing: border-box;
.mx_ErrorView_container {
max-width: 680px;

View File

@@ -6,7 +6,7 @@
# the branch the current checkout is on, use that branch. Otherwise,
# use develop.
set -ex
set -x
GIT_CLONE_ARGS=("$@")
[ -z "$defbranch" ] && defbranch="develop"

View File

@@ -25,6 +25,8 @@
# all phonenumber.js-supported country flags (as SVGs) into
# PNGs that can be used by CountryDropdown.js.
set -e
# Allow CTRL+C to terminate the script
trap "echo Exited!; exit;" SIGINT SIGTERM

View File

@@ -3,6 +3,8 @@
# Echoes a version based on the git hashes of the element-web, react-sdk & js-sdk checkouts, for the case where
# these dependencies are git checkouts.
set -e
# Since the deps are fetched from git, we can rev-parse
REACT_SHA=$(git -C node_modules/matrix-react-sdk rev-parse --short=12 HEAD)
JSSDK_SHA=$(git -C node_modules/matrix-js-sdk rev-parse --short=12 HEAD)

View File

@@ -1,6 +1,6 @@
#!/bin/bash
set -x
set -ex
# Creates a layered environment with the full repo for the app and SDKs cloned
# and linked. This gives an element-web dev environment ready to build with

View File

@@ -12,8 +12,7 @@ then
exit
fi
set -e
set -x
set -ex
tmpdir=`mktemp -d 2>/dev/null || mktemp -d -t 'icontmp'`

View File

@@ -1,5 +1,7 @@
#!/bin/bash
set -e
# If $1 looks like v1.2.3 or v1.2.3-foo, strip the leading v, then print it to stdout
if [[ $1 =~ ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-.+)?$ ]]; then
echo ${1:1}

View File

@@ -10,10 +10,10 @@
"Download Completed": "Download Completed",
"Open": "Open",
"Dismiss": "Dismiss",
"%(brand)s Desktop (%(platformName)s)": "%(brand)s Desktop (%(platformName)s)",
"%(brand)s Desktop: %(platformName)s": "%(brand)s Desktop: %(platformName)s",
"Go to your browser to complete Sign In": "Go to your browser to complete Sign In",
"Unknown device": "Unknown device",
"%(appName)s (%(browserName)s, %(osName)s)": "%(appName)s (%(browserName)s, %(osName)s)",
"%(appName)s: %(browserName)s on %(osName)s": "%(appName)s: %(browserName)s on %(osName)s",
"Powered by Matrix": "Powered by Matrix",
"Use %(brand)s on mobile": "Use %(brand)s on mobile",
"Unsupported browser": "Unsupported browser",

View File

@@ -18,8 +18,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// To ensure we load the browser-request version
import "matrix-js-sdk"; // eslint-disable-line no-restricted-imports
// To ensure we load the browser-matrix version first
import "matrix-js-sdk/src/browser-index";
import React from 'react';
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg';

View File

@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from 'browser-request';
import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
// Load the config file. First try to load up a domain-specific config of the
@@ -32,44 +30,28 @@ export async function getVectorConfig(relativeLocation=''): Promise<IConfigOptio
if (Object.keys(configJson).length === 0) {
throw new Error(); // throw to enter the catch
}
return configJson as IConfigOptions;
return configJson;
} catch (e) {
return await generalConfigPromise as IConfigOptions;
return generalConfigPromise;
}
}
function getConfig(configJsonFilename: string): Promise<{}> {
return new Promise(function(resolve, reject) {
request(
{ method: "GET", url: configJsonFilename, qs: { cachebuster: Date.now() } },
(err, response, body) => {
try {
if (err || response.status < 200 || response.status >= 300) {
// Lack of a config isn't an error, we should
// just use the defaults.
// Also treat a blank config as no config, assuming
// the status code is 0, because we don't get 404s
// from file: URIs so this is the only way we can
// not fail if the file doesn't exist when loading
// from a file:// URI.
if (response) {
if (response.status == 404 || (response.status == 0 && body == '')) {
resolve({});
}
}
reject({ err: err, response: response });
return;
}
// We parse the JSON ourselves rather than use the JSON
// parameter, since this throws a parse error on empty
// which breaks if there's no config.json and we're
// loading from the filesystem (see above).
resolve(JSON.parse(body));
} catch (e) {
reject({ err: e });
}
},
);
async function getConfig(configJsonFilename: string): Promise<IConfigOptions> {
const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString());
const res = await fetch(url, {
cache: "no-cache",
method: "GET",
});
if (res.status === 404 || res.status === 0) {
// Lack of a config isn't an error, we should just use the defaults.
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
return {} as IConfigOptions;
}
if (res.ok) {
return res.json();
}
}

View File

@@ -45,7 +45,6 @@ import VectorBasePlatform from './VectorBasePlatform';
import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
const electron = window.electron;
const isMac = navigator.platform.toUpperCase().includes('MAC');
function platformFriendlyName(): string {
@@ -70,7 +69,7 @@ function platformFriendlyName(): string {
function onAction(payload: ActionPayload): void {
// Whitelist payload actions, no point sending most across
if (['call_state'].includes(payload.action)) {
electron.send('app_onAction', payload);
window.electron.send('app_onAction', payload);
}
}
@@ -103,7 +102,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
false if there is not
or the error if one is encountered
*/
electron.on('check_updates', (event, status) => {
window.electron.on('check_updates', (event, status) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...getUpdateCheckStatus(status),
@@ -111,27 +110,27 @@ export default class ElectronPlatform extends VectorBasePlatform {
});
// try to flush the rageshake logs to indexeddb before quit.
electron.on('before-quit', function() {
window.electron.on('before-quit', function() {
logger.log('element-desktop closing');
rageshake.flush();
});
electron.on('update-downloaded', this.onUpdateDownloaded);
window.electron.on('update-downloaded', this.onUpdateDownloaded);
electron.on('preferences', () => {
window.electron.on('preferences', () => {
dis.fire(Action.ViewUserSettings);
});
electron.on('userDownloadCompleted', (ev, { id, name }) => {
window.electron.on('userDownloadCompleted', (ev, { id, name }) => {
const key = `DOWNLOAD_TOAST_${id}`;
const onAccept = () => {
electron.send('userDownloadAction', { id, open: true });
window.electron.send('userDownloadAction', { id, open: true });
ToastStore.sharedInstance().dismissToast(key);
};
const onDismiss = () => {
electron.send('userDownloadAction', { id });
window.electron.send('userDownloadAction', { id });
};
ToastStore.sharedInstance().addOrReplaceToast({
@@ -187,7 +186,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
electron.send('setBadgeCount', count);
window.electron.send('setBadgeCount', count);
}
public supportsNotifications(): boolean {
@@ -233,7 +232,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
}
public loudNotification(ev: MatrixEvent, room: Room) {
electron.send('loudNotification');
window.electron.send('loudNotification');
}
public needsUrlTooltips(): boolean {
@@ -269,19 +268,19 @@ export default class ElectronPlatform extends VectorBasePlatform {
public startUpdateCheck() {
super.startUpdateCheck();
electron.send('check_updates');
window.electron.send('check_updates');
}
public installUpdate() {
// IPC to the main process to install the update, since quitAndInstall
// doesn't fire the before-quit event so the main process needs to know
// it should exit.
electron.send('install_update');
window.electron.send('install_update');
}
public getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand;
return _t('%(brand)s Desktop (%(platformName)s)', {
return _t('%(brand)s Desktop: %(platformName)s', {
brand,
platformName: platformFriendlyName(),
});

View File

@@ -19,8 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { ElectronChannel } from "../../@types/global";
const electron = window.electron;
interface IPCPayload {
id?: number;
error?: string;
@@ -35,7 +33,7 @@ export class IPCManager {
private readonly sendChannel: ElectronChannel = "ipcCall",
private readonly recvChannel: ElectronChannel = "ipcReply",
) {
electron.on(this.recvChannel, this.onIpcReply);
window.electron.on(this.recvChannel, this.onIpcReply);
}
public async call(name: string, ...args: any[]): Promise<any> {

View File

@@ -17,7 +17,6 @@ limitations under the License.
*/
import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import request from 'browser-request';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { _t } from 'matrix-react-sdk/src/languageHandler';
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast";
@@ -87,31 +86,18 @@ export default class WebPlatform extends VectorBasePlatform {
});
}
private getMostRecentVersion(): Promise<string> {
// We add a cachebuster to the request to make sure that we know about
// the most recent version on the origin server. That might not
// actually be the version we'd get on a reload (particularly in the
// presence of intermediate caching proxies), but still: we're trying
// to tell the user that there is a new version.
return new Promise((resolve, reject) => {
request(
{
method: "GET",
url: "version",
qs: { cachebuster: Date.now() },
},
(err, response, body) => {
if (err || response.status < 200 || response.status >= 300) {
if (err === null) err = { status: response.status };
reject(err);
return;
}
resolve(getNormalizedAppVersion(body.trim()));
},
);
private async getMostRecentVersion(): Promise<string> {
const res = await fetch("version", {
method: "GET",
cache: "no-cache",
});
if (res.ok) {
const text = await res.text();
return getNormalizedAppVersion(text.trim());
}
return Promise.reject({ status: res.status });
}
public getAppVersion(): Promise<string> {
@@ -210,7 +196,7 @@ export default class WebPlatform extends VectorBasePlatform {
let osName = ua.getOS().name || "unknown OS";
// Stylise the value from the parser to match Apple's current branding.
if (osName === "Mac OS") osName = "macOS";
return _t('%(appName)s (%(browserName)s, %(osName)s)', {
return _t('%(appName)s: %(browserName)s on %(osName)s', {
appName,
browserName,
osName,

View File

@@ -41,6 +41,7 @@ import { RoomView as RoomViewClass } from 'matrix-react-sdk/src/components/struc
import LoginComponent from 'matrix-react-sdk/src/components/structures/auth/Login';
import WelcomeComponent from "matrix-react-sdk/src/components/views/auth/Welcome";
import EmbeddedPage from "matrix-react-sdk/src/components/structures/EmbeddedPage";
import { AutoDiscovery } from 'matrix-js-sdk/src/matrix';
const DEFAULT_HS_URL='http://my_server';
const DEFAULT_IS_URL='http://my_is';
@@ -60,7 +61,7 @@ describe('loading:', function() {
beforeEach(function() {
httpBackend = new MockHttpBackend();
jssdk.request(httpBackend.requestFn);
window.fetch = httpBackend.fetchFn;
parentDiv = document.createElement('div');
// uncomment this to actually add the div to the UI, to help with
@@ -73,21 +74,25 @@ describe('loading:', function() {
afterEach(async function() {
console.log(`${Date.now()}: loading: afterEach`);
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
try {
if (matrixChat) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
}
// unmounting should have cleared the MatrixClientPeg
expect(MatrixClientPeg.get()).toBe(null);
// clear the indexeddbs so we can start from a clean slate next time.
await Promise.all([
test_utils.deleteIndexedDB('matrix-js-sdk:crypto'),
test_utils.deleteIndexedDB('matrix-js-sdk:riot-web-sync'),
]);
cleanLocalstorage();
} catch (e) {
console.error(e);
}
// unmounting should have cleared the MatrixClientPeg
expect(MatrixClientPeg.get()).toBe(null);
// clear the indexeddbs so we can start from a clean slate next time.
await Promise.all([
test_utils.deleteIndexedDB('matrix-js-sdk:crypto'),
test_utils.deleteIndexedDB('matrix-js-sdk:riot-web-sync'),
]);
cleanLocalstorage();
console.log(`${Date.now()}: loading: afterEach complete`);
});
@@ -421,7 +426,6 @@ describe('loading:', function() {
describe('Guest auto-registration:', function() {
it('shows a welcome page by default', function() {
loadApp();
return sleep(1).then(() => {

View File

@@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from 'browser-request';
import fetchMock from "fetch-mock-jest";
import { getVectorConfig } from "../../../src/vector/getconfig";
describe('getVectorConfig()', () => {
const setRequestMockImplementationOnce = (err?: unknown, response?: { status: number }, body?: string) =>
request.mockImplementationOnce((_opts, callback) => callback(err, response, body));
fetchMock.config.overwriteRoutes = true;
describe('getVectorConfig()', () => {
const prevDocumentDomain = document.domain;
const elementDomain = 'app.element.io';
const now = 1234567890;
@@ -38,6 +37,7 @@ describe('getVectorConfig()', () => {
// stable value for cachebuster
jest.spyOn(Date, 'now').mockReturnValue(now);
jest.clearAllMocks();
fetchMock.mockClear();
});
afterAll(() => {
@@ -46,85 +46,67 @@ describe('getVectorConfig()', () => {
});
it('requests specific config for document domain', async () => {
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(specificConfig))
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", specificConfig);
fetchMock.getOnce("express:/config.json", generalConfig);
await getVectorConfig();
expect(request.mock.calls[0][0]).toEqual({ method: "GET", url: 'config.app.element.io.json', qs: { cachebuster: now } })
await expect(getVectorConfig()).resolves.toEqual(specificConfig);
});
it('adds trailing slash to relativeLocation when not an empty string', async () => {
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(specificConfig))
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:../config.app.element.io.json", specificConfig);
fetchMock.getOnce("express:../config.json", generalConfig);
await getVectorConfig('..');
expect(request.mock.calls[0][0]).toEqual(expect.objectContaining({ url: '../config.app.element.io.json' }))
expect(request.mock.calls[1][0]).toEqual(expect.objectContaining({ url: '../config.json' }))
});
it('returns parsed specific config when it is non-empty', async () => {
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(specificConfig))
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
const result = await getVectorConfig();
expect(result).toEqual(specificConfig);
await expect(getVectorConfig("..")).resolves.toEqual(specificConfig);
});
it('returns general config when specific config succeeds but is empty', async () => {
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify({}))
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", {});
fetchMock.getOnce("express:/config.json", generalConfig);
const result = await getVectorConfig();
expect(result).toEqual(generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
});
it('returns general config when specific config 404s', async () => {
setRequestMockImplementationOnce(undefined, { status: 404 })
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", { status: 404 });
fetchMock.getOnce("express:/config.json", generalConfig);
const result = await getVectorConfig();
expect(result).toEqual(generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
});
it('returns general config when specific config is fetched from a file and is empty', async () => {
setRequestMockImplementationOnce(undefined, { status: 0 }, '')
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", 0);
fetchMock.getOnce("express:/config.json", generalConfig);
const result = await getVectorConfig();
expect(result).toEqual(generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
});
it('returns general config when specific config returns a non-200 status', async () => {
setRequestMockImplementationOnce(undefined, { status: 401 })
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", { status: 401 });
fetchMock.getOnce("express:/config.json", generalConfig);
const result = await getVectorConfig();
expect(result).toEqual(generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
});
it('returns general config when specific config returns an error', async () => {
setRequestMockImplementationOnce('err1')
setRequestMockImplementationOnce(undefined, { status: 200 }, JSON.stringify(generalConfig))
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err1" });
fetchMock.getOnce("express:/config.json", generalConfig);
const result = await getVectorConfig();
expect(result).toEqual(generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
});
it('rejects with an error when general config rejects', async () => {
setRequestMockImplementationOnce('err-specific');
setRequestMockImplementationOnce('err-general');
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
fetchMock.getOnce("express:/config.json", { throws: "err-general" });
await expect(() => getVectorConfig()).rejects.toEqual({"err": "err-general", "response": undefined});
await expect(getVectorConfig()).rejects.toBe("err-general");
});
it('rejects with an error when config is invalid JSON', async () => {
setRequestMockImplementationOnce('err-specific');
setRequestMockImplementationOnce(undefined, { status: 200 }, '{"invalid": "json",}');
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
fetchMock.getOnce("express:/config.json", '{"invalid": "json",}');
await expect(() => getVectorConfig()).rejects.toEqual({
err: new SyntaxError("Unexpected token } in JSON at position 19"),
});
// We can't assert it'll be a SyntaxError as node-fetch behaves differently
// https://github.com/wheresrhys/fetch-mock/issues/270
await expect(getVectorConfig()).rejects.toThrow("Unexpected token } in JSON at position 19");
});
});

View File

@@ -0,0 +1,279 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import request from 'browser-request';
import EventEmitter from 'events';
import { logger } from 'matrix-js-sdk/src/logger';
import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk/src/matrix';
import { UpdateCheckStatus } from 'matrix-react-sdk/src/BasePlatform';
import { Action } from 'matrix-react-sdk/src/dispatcher/actions';
import dispatcher from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg';
import * as rageshake from 'matrix-react-sdk/src/rageshake/rageshake';
import ElectronPlatform from '../../../../src/vector/platform/ElectronPlatform';
jest.mock('matrix-react-sdk/src/rageshake/rageshake', () => ({
flush: jest.fn()
}))
describe('ElectronPlatform', () => {
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()
};
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';
window.electron = mockElectron;
beforeEach(() => {
window.electron = mockElectron;
jest.clearAllMocks();
delete window.navigator;
window.navigator = { userAgent: defaultUserAgent } as unknown as Navigator;
});
const getElectronEventHandlerCall = (eventType: string): [type: string, handler: Function] | 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('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);
});
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) => {
delete window.navigator;
window.navigator = { userAgent } as unknown as Navigator;
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');
});
});
});

View File

@@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from 'browser-request';
import fetchMock from "fetch-mock-jest";
import { UpdateCheckStatus } from 'matrix-react-sdk/src/BasePlatform';
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg';
import WebPlatform from '../../../../src/vector/platform/WebPlatform';
fetchMock.config.overwriteRoutes = true;
describe('WebPlatform', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -65,7 +67,7 @@ describe('WebPlatform', () => {
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, macOS)",
"develop.element.io: Chrome on macOS",
]])("%s & %s = %s", (url, userAgent, result) => {
delete window.navigator;
window.navigator = { userAgent } as unknown as Navigator;
@@ -120,9 +122,6 @@ describe('WebPlatform', () => {
const envVersion = process.env.VERSION;
const prodVersion = '1.10.13';
const setRequestMockImplementation = (err?: unknown, response?: { status: number }, body?: string) =>
request.mockImplementation((_opts, callback) => callback(err, response, body));
beforeEach(() => {
jest.spyOn(MatrixClientPeg, 'userRegisteredWithinLastHours').mockReturnValue(false);
})
@@ -157,7 +156,7 @@ describe('WebPlatform', () => {
describe('pollForUpdate()', () => {
it('should return not available and call showNoUpdate when current version matches most recent version', async () => {
process.env.VERSION = prodVersion;
setRequestMockImplementation(undefined, { status: 200}, prodVersion);
fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform();
const showUpdate = jest.fn();
@@ -171,7 +170,7 @@ describe('WebPlatform', () => {
it('should strip v prefix from versions before comparing', async () => {
process.env.VERSION = prodVersion;
setRequestMockImplementation(undefined, { status: 200}, `v${prodVersion}`);
fetchMock.getOnce("/version", `v${prodVersion}`);
const platform = new WebPlatform();
const showUpdate = jest.fn();
@@ -186,7 +185,7 @@ describe('WebPlatform', () => {
it('should return ready and call showUpdate when current version differs from most recent version', async () => {
process.env.VERSION = '0.0.0'; // old version
setRequestMockImplementation(undefined, { status: 200}, prodVersion);
fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform();
const showUpdate = jest.fn();
@@ -201,7 +200,7 @@ describe('WebPlatform', () => {
it('should return ready without showing update when user registered in last 24', async () => {
process.env.VERSION = '0.0.0'; // old version
jest.spyOn(MatrixClientPeg, 'userRegisteredWithinLastHours').mockReturnValue(true);
setRequestMockImplementation(undefined, { status: 200}, prodVersion);
fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform();
const showUpdate = jest.fn();
@@ -214,7 +213,7 @@ describe('WebPlatform', () => {
});
it('should return error when version check fails', async () => {
setRequestMockImplementation('oups');
fetchMock.getOnce("/version", { throws: "oups" });
const platform = new WebPlatform();
const showUpdate = jest.fn();

1072
yarn.lock

File diff suppressed because it is too large Load Diff