Merge branch 'develop' into sort-imports

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-12-09 08:34:20 +00:00
commit 7b94e13a84
642 changed files with 30052 additions and 8035 deletions

View file

@ -16,17 +16,16 @@ limitations under the License.
import './skinned-sdk';
import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call';
import EventEmitter from 'events';
import CallHandler, { PlaceCallType, CallHandlerEvent } from '../src/CallHandler';
import CallHandler, { CallHandlerEvent } from '../src/CallHandler';
import { stubClient, mkStubRoom } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import dis from '../src/dispatcher/dispatcher';
import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
import DMRoomMap from '../src/utils/DMRoomMap';
import EventEmitter from 'events';
import SdkConfig from '../src/SdkConfig';
import { ActionPayload } from '../src/dispatcher/payloads';
import { Action } from '../src/dispatcher/actions';
import { Action } from "../src/dispatcher/actions";
const REAL_ROOM_ID = '$room1:example.org';
const MAPPED_ROOM_ID = '$room2:example.org';
@ -89,6 +88,14 @@ function untilDispatch(waitForAction: string): Promise<ActionPayload> {
});
}
function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent): Promise<void> {
return new Promise<void>((resolve) => {
callHandler.addListener(event, () => {
resolve();
});
});
}
describe('CallHandler', () => {
let dmRoomMap;
let callHandler;
@ -174,12 +181,9 @@ describe('CallHandler', () => {
},
}]);
dis.dispatch({
action: Action.DialNumber,
number: '01818118181',
}, true);
await callHandler.dialNumber('01818118181');
const viewRoomPayload = await untilDispatch('view_room');
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
// Check that a call was started
@ -187,14 +191,9 @@ describe('CallHandler', () => {
});
it('should move calls between rooms when remote asserted identity changes', async () => {
dis.dispatch({
action: 'place_call',
type: PlaceCallType.Voice,
room_id: REAL_ROOM_ID,
}, true);
callHandler.placeCall(REAL_ROOM_ID, CallType.Voice);
// wait for the call to be set up
await untilDispatch('call_state');
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
// should start off in the actual room ID it's in at the protocol level
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall);

View file

@ -39,7 +39,7 @@ describe('DecryptionFailureTracker', function() {
const failedDecryptionEvent = createFailedDecryptionEvent();
let count = 0;
const tracker = new DecryptionFailureTracker((total) => count += total);
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
const err = new MockDecryptionError();
tracker.eventDecrypted(failedDecryptionEvent, err);
@ -59,7 +59,7 @@ describe('DecryptionFailureTracker', function() {
const decryptedEvent = createFailedDecryptionEvent();
const tracker = new DecryptionFailureTracker((total) => {
expect(true).toBe(false, 'should not track an event that has since been decrypted correctly');
});
}, () => "UnknownError");
const err = new MockDecryptionError();
tracker.eventDecrypted(decryptedEvent, err);
@ -81,7 +81,7 @@ describe('DecryptionFailureTracker', function() {
const decryptedEvent2 = createFailedDecryptionEvent();
let count = 0;
const tracker = new DecryptionFailureTracker((total) => count += total);
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
// Arbitrary number of failed decryptions for both events
const err = new MockDecryptionError();
@ -112,7 +112,7 @@ describe('DecryptionFailureTracker', function() {
const decryptedEvent = createFailedDecryptionEvent();
let count = 0;
const tracker = new DecryptionFailureTracker((total) => count += total);
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
// Indicate decryption
const err = new MockDecryptionError();
@ -140,7 +140,7 @@ describe('DecryptionFailureTracker', function() {
const decryptedEvent = createFailedDecryptionEvent();
let count = 0;
const tracker = new DecryptionFailureTracker((total) => count += total);
const tracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
// Indicate decryption
const err = new MockDecryptionError();
@ -153,7 +153,7 @@ describe('DecryptionFailureTracker', function() {
tracker.trackFailures();
// Simulate the browser refreshing by destroying tracker and creating a new tracker
const secondTracker = new DecryptionFailureTracker((total) => count += total);
const secondTracker = new DecryptionFailureTracker((total) => count += total, () => "UnknownError");
//secondTracker.loadTrackedEventHashMap();
@ -170,28 +170,29 @@ describe('DecryptionFailureTracker', function() {
const counts = {};
const tracker = new DecryptionFailureTracker(
(total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total,
(error) => error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError",
);
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
tracker.addDecryptionFailure(new DecryptionFailure('$event_id1', 'ERROR_CODE_1'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'ERROR_CODE_2'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'ERROR_CODE_2'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id3', 'ERROR_CODE_2'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id1', 'UnknownError'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'OlmKeysNotSentError'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id2', 'OlmKeysNotSentError'));
tracker.addDecryptionFailure(new DecryptionFailure('$event_id3', 'OlmKeysNotSentError'));
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
expect(counts['ERROR_CODE_1']).toBe(1, 'should track one ERROR_CODE_1');
expect(counts['ERROR_CODE_2']).toBe(2, 'should track two ERROR_CODE_2');
expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
expect(counts['OlmKeysNotSentError']).toBe(2, 'should track two OlmKeysNotSentError');
});
it('should map error codes correctly', () => {
const counts = {};
const tracker = new DecryptionFailureTracker(
(total, errorCode) => counts[errorCode] = (counts[errorCode] || 0) + total,
(errorCode) => 'MY_NEW_ERROR_CODE',
(errorCode) => 'OlmUnspecifiedError',
);
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
@ -204,7 +205,7 @@ describe('DecryptionFailureTracker', function() {
tracker.trackFailures();
expect(counts['MY_NEW_ERROR_CODE'])
.toBe(3, 'should track three MY_NEW_ERROR_CODE, got ' + counts['MY_NEW_ERROR_CODE']);
expect(counts['OlmUnspecifiedError'])
.toBe(3, 'should track three OlmUnspecifiedError, got ' + counts['OlmUnspecifiedError']);
});
});

161
test/Markdown-test.ts Normal file
View file

@ -0,0 +1,161 @@
/*
Copyright 2021 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 Markdown from "../src/Markdown";
describe("Markdown parser test", () => {
describe("fixing HTML links", () => {
const testString = [
"Test1:",
"#_foonetic_xkcd:matrix.org",
"http://google.com/_thing_",
"https://matrix.org/_matrix/client/foo/123_",
"#_foonetic_xkcd:matrix.org",
"",
"Test1A:",
"#_foonetic_xkcd:matrix.org",
"http://google.com/_thing_",
"https://matrix.org/_matrix/client/foo/123_",
"#_foonetic_xkcd:matrix.org",
"",
"Test2:",
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
"",
"Test3:",
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
].join("\n");
it('tests that links with markdown empasis in them are getting properly HTML formatted', () => {
/* eslint-disable max-len */
const expectedResult = [
"<p>Test1:<br />#_foonetic_xkcd:matrix.org<br />http://google.com/_thing_<br />https://matrix.org/_matrix/client/foo/123_<br />#_foonetic_xkcd:matrix.org</p>",
"<p>Test1A:<br />#_foonetic_xkcd:matrix.org<br />http://google.com/_thing_<br />https://matrix.org/_matrix/client/foo/123_<br />#_foonetic_xkcd:matrix.org</p>",
"<p>Test2:<br />http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg<br />http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</p>",
"<p>Test3:<br />https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org<br />https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</p>",
"",
].join("\n");
/* eslint-enable max-len */
const md = new Markdown(testString);
expect(md.toHTML()).toEqual(expectedResult);
});
it('tests that links with autolinks are not touched at all and are still properly formatted', () => {
const test = [
"Test1:",
"<#_foonetic_xkcd:matrix.org>",
"<http://google.com/_thing_>",
"<https://matrix.org/_matrix/client/foo/123_>",
"<#_foonetic_xkcd:matrix.org>",
"",
"Test1A:",
"<#_foonetic_xkcd:matrix.org>",
"<http://google.com/_thing_>",
"<https://matrix.org/_matrix/client/foo/123_>",
"<#_foonetic_xkcd:matrix.org>",
"",
"Test2:",
"<http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg>",
"<http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg>",
"",
"Test3:",
"<https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org>",
"<https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org>",
].join("\n");
/* eslint-disable max-len */
/**
* NOTE: I'm not entirely sure if those "<"" and ">" should be visible in here for #_foonetic_xkcd:matrix.org
* but it seems to be actually working properly
*/
const expectedResult = [
"<p>Test1:<br />&lt;#_foonetic_xkcd:matrix.org&gt;<br /><a href=\"http://google.com/_thing_\">http://google.com/_thing_</a><br /><a href=\"https://matrix.org/_matrix/client/foo/123_\">https://matrix.org/_matrix/client/foo/123_</a><br />&lt;#_foonetic_xkcd:matrix.org&gt;</p>",
"<p>Test1A:<br />&lt;#_foonetic_xkcd:matrix.org&gt;<br /><a href=\"http://google.com/_thing_\">http://google.com/_thing_</a><br /><a href=\"https://matrix.org/_matrix/client/foo/123_\">https://matrix.org/_matrix/client/foo/123_</a><br />&lt;#_foonetic_xkcd:matrix.org&gt;</p>",
"<p>Test2:<br /><a href=\"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg\">http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</a><br /><a href=\"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg\">http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</a></p>",
"<p>Test3:<br /><a href=\"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org\">https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</a><br /><a href=\"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org\">https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</a></p>",
"",
].join("\n");
/* eslint-enable max-len */
const md = new Markdown(test);
expect(md.toHTML()).toEqual(expectedResult);
});
it('expects that links in codeblock are not modified', () => {
const expectedResult = [
'<pre><code class="language-Test1:">#_foonetic_xkcd:matrix.org',
'http://google.com/_thing_',
'https://matrix.org/_matrix/client/foo/123_',
'#_foonetic_xkcd:matrix.org',
'',
'Test1A:',
'#_foonetic_xkcd:matrix.org',
'http://google.com/_thing_',
'https://matrix.org/_matrix/client/foo/123_',
'#_foonetic_xkcd:matrix.org',
'',
'Test2:',
'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg',
'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg',
'',
'Test3:',
'https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org',
'https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org```',
'</code></pre>',
'',
].join('\n');
const md = new Markdown("```" + testString + "```");
expect(md.toHTML()).toEqual(expectedResult);
});
it('expects that links with emphasis are "escaped" correctly', () => {
/* eslint-disable max-len */
const testString = [
'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg' + " " + 'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg',
'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg' + " " + 'http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg',
].join('\n');
const expectedResult = [
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
].join('<br />');
/* eslint-enable max-len */
const md = new Markdown(testString);
expect(md.toHTML()).toEqual(expectedResult);
});
it('expects that the link part will not be accidentally added to <strong>', () => {
/* eslint-disable max-len */
const testString = `https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py`;
const expectedResult = 'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py';
/* eslint-enable max-len */
const md = new Markdown(testString);
expect(md.toHTML()).toEqual(expectedResult);
});
it('expects that the link part will not be accidentally added to <strong> for multiline links', () => {
/* eslint-disable max-len */
const testString = [
'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py' + " " + 'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py',
'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py' + " " + 'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py',
].join('\n');
const expectedResult = [
'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py' + " " + 'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py',
'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py' + " " + 'https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py',
].join('<br />');
/* eslint-enable max-len */
const md = new Markdown(testString);
expect(md.toHTML()).toEqual(expectedResult);
});
});
});

View file

@ -17,9 +17,7 @@ limitations under the License.
import {
Anonymity,
getRedactedCurrentLocation,
IAnonymousEvent,
IPseudonymousEvent,
IRoomEvent,
IEvent,
PosthogAnalytics,
} from '../src/PosthogAnalytics';
import SdkConfig from '../src/SdkConfig';
@ -40,25 +38,9 @@ class FakePosthog {
}
}
export interface ITestEvent extends IAnonymousEvent {
key: "jest_test_event";
properties: {
foo: string;
};
}
export interface ITestPseudonymousEvent extends IPseudonymousEvent {
key: "jest_test_pseudo_event";
properties: {
foo: string;
};
}
export interface ITestRoomEvent extends IRoomEvent {
key: "jest_test_room_event";
properties: {
foo: string;
};
export interface ITestEvent extends IEvent {
eventName: "JestTestEvents";
foo: string;
}
describe("PosthogAnalytics", () => {
@ -126,27 +108,20 @@ describe("PosthogAnalytics", () => {
analytics = new PosthogAnalytics(fakePosthog);
});
it("Should pass trackAnonymousEvent() to posthog", async () => {
it("Should pass event to posthog", () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
analytics.trackEvent<ITestEvent>({
eventName: "JestTestEvents",
foo: "bar",
});
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
expect(fakePosthog.capture.mock.calls[0][0]).toBe("JestTestEvents");
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
});
it("Should pass trackPseudonymousEvent() to posthog", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
foo: "bar",
});
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_pseudo_event");
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
});
it("Should not track pseudonymous messages if anonymous", async () => {
it("Should not track events if anonymous", async () => {
analytics.setAnonymity(Anonymity.Anonymous);
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_event", {
await analytics.trackEvent<ITestEvent>({
eventName: "JestTestEvents",
foo: "bar",
});
expect(fakePosthog.capture.mock.calls.length).toBe(0);
@ -154,43 +129,25 @@ describe("PosthogAnalytics", () => {
it("Should not track any events if disabled", async () => {
analytics.setAnonymity(Anonymity.Disabled);
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_event", {
analytics.trackEvent<ITestEvent>({
eventName: "JestTestEvents",
foo: "bar",
});
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
foo: "bar",
});
await analytics.trackPageView(200);
expect(fakePosthog.capture.mock.calls.length).toBe(0);
});
it("Should pseudonymise a location of a known screen", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
it("Should anonymise location of a known screen", async () => {
const location = getRedactedCurrentLocation("https://foo.bar", "#/register/some/pii", "/");
expect(location).toBe("https://foo.bar/#/register/<redacted>");
});
it("Should anonymise a location of a known screen", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
expect(location).toBe("https://foo.bar/#/register/<redacted>");
});
it("Should pseudonymise a location of an unknown screen", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
});
it("Should anonymise a location of an unknown screen", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
it("Should anonymise location of an unknown screen", async () => {
const location = getRedactedCurrentLocation("https://foo.bar", "#/not_a_screen_name/some/pii", "/");
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
});
it("Should handle an empty hash", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "", "/", Anonymity.Anonymous);
const location = getRedactedCurrentLocation("https://foo.bar", "", "/");
expect(location).toBe("https://foo.bar/");
});

View file

@ -45,6 +45,11 @@ const button2 = <Button key={2}>b</Button>;
const button3 = <Button key={3}>c</Button>;
const button4 = <Button key={4}>d</Button>;
// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
get() { return this.parentNode; },
});
describe("RovingTabIndex", () => {
it("RovingTabIndexProvider renders children as expected", () => {
const wrapper = mount(<RovingTabIndexProvider>

View file

@ -70,7 +70,7 @@ describe('ThreadPanel', () => {
wrapper.find(ContextMenuButton).simulate('click');
const found = wrapper.find(ThreadPanelHeaderFilterOptionItem);
expect(found.length).toEqual(2);
const foundButton = found.find('[aria-selected=true]').first();
const foundButton = found.find('[aria-checked=true]').first();
expect(foundButton.text()).toEqual(`${_t("All threads")}${_t('Shows all threads from current room')}`);
expect(foundButton).toMatchSnapshot();
});

View file

@ -8,6 +8,7 @@ exports[`ThreadPanel Header expect that All filter for ThreadPanelHeader properl
Threads
</span>
<ContextMenuButton
className="mx_ThreadPanel_dropdown"
inputRef={
Object {
"current": null,
@ -29,6 +30,7 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
Threads
</span>
<ContextMenuButton
className="mx_ThreadPanel_dropdown"
inputRef={
Object {
"current": null,
@ -44,21 +46,21 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = `
<AccessibleButton
aria-selected={true}
aria-checked={true}
className="mx_ThreadPanel_Header_FilterOptionItem"
element="div"
onClick={[Function]}
role="button"
tabIndex={0}
role="menuitemradio"
tabIndex={-1}
>
<div
aria-selected={true}
aria-checked={true}
className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
role="menuitemradio"
tabIndex={-1}
>
<span>
All threads

View file

@ -14,13 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import sdk from '../../../skinned-sdk';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import { createClient } from 'matrix-js-sdk/src/matrix';
import sdk from '../../../skinned-sdk';
import SdkConfig from '../../../../src/SdkConfig';
import { mkServerConfig } from "../../../test-utils";
import { createTestClient, mkServerConfig } from "../../../test-utils";
jest.mock('matrix-js-sdk/src/matrix');
jest.useFakeTimers();
const Registration = sdk.getComponent(
'structures.auth.Registration',
@ -32,6 +37,7 @@ describe('Registration', function() {
beforeEach(function() {
parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
createClient.mockImplementation(() => createTestClient());
});
afterEach(function() {
@ -49,13 +55,13 @@ describe('Registration', function() {
/>, parentDiv);
}
it('should show server picker', function() {
it('should show server picker', async function() {
const root = render();
const selector = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_ServerPicker");
expect(selector).toBeTruthy();
});
it('should show form when custom URLs disabled', function() {
it('should show form when custom URLs disabled', async function() {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});
@ -78,7 +84,7 @@ describe('Registration', function() {
expect(form).toBeTruthy();
});
it("should show SSO options if those are available", () => {
it("should show SSO options if those are available", async () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
disable_custom_urls: true,
});

View file

@ -0,0 +1,162 @@
/*
Copyright 2021 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 '../../../skinned-sdk';
import { Direction, mouseWithinRegion } from "../../../../src/components/views/elements/InteractiveTooltip";
describe("InteractiveTooltip", () => {
describe("mouseWithinRegion", () => {
it("direction=left", () => {
const targetRect = {
width: 20,
height: 20,
top: 300,
right: 370,
bottom: 320,
left: 350,
} as DOMRect;
const contentRect = {
width: 100,
height: 400,
top: 100,
right: 200,
bottom: 500,
left: 100,
} as DOMRect;
// just within top left corner of contentRect
expect(mouseWithinRegion(101, 101, Direction.Left, targetRect, contentRect)).toBe(true);
// just outside top left corner of contentRect, within buffer
expect(mouseWithinRegion(101, 90, Direction.Left, targetRect, contentRect)).toBe(true);
// just within top right corner of targetRect
expect(mouseWithinRegion(369, 301, Direction.Left, targetRect, contentRect)).toBe(true);
// within the top triangular portion of the trapezoid
expect(mouseWithinRegion(300, 200, Direction.Left, targetRect, contentRect)).toBe(true);
// within the bottom triangular portion of the trapezoid
expect(mouseWithinRegion(300, 350, Direction.Left, targetRect, contentRect)).toBe(true);
// outside the top triangular portion of the trapezoid
expect(mouseWithinRegion(300, 140, Direction.Left, targetRect, contentRect)).toBe(false);
// outside the bottom triangular portion of the trapezoid
expect(mouseWithinRegion(300, 460, Direction.Left, targetRect, contentRect)).toBe(false);
});
it("direction=right", () => {
const targetRect = {
width: 20,
height: 20,
top: 300,
right: 370,
bottom: 320,
left: 350,
} as DOMRect;
const contentRect = {
width: 100,
height: 400,
top: 100,
right: 620,
bottom: 500,
left: 520,
} as DOMRect;
// just within top right corner of contentRect
expect(mouseWithinRegion(619, 101, Direction.Right, targetRect, contentRect)).toBe(true);
// just outside top right corner of contentRect, within buffer
expect(mouseWithinRegion(619, 90, Direction.Right, targetRect, contentRect)).toBe(true);
// just within top left corner of targetRect
expect(mouseWithinRegion(351, 301, Direction.Right, targetRect, contentRect)).toBe(true);
// within the top triangular portion of the trapezoid
expect(mouseWithinRegion(420, 200, Direction.Right, targetRect, contentRect)).toBe(true);
// within the bottom triangular portion of the trapezoid
expect(mouseWithinRegion(420, 350, Direction.Right, targetRect, contentRect)).toBe(true);
// outside the top triangular portion of the trapezoid
expect(mouseWithinRegion(420, 140, Direction.Right, targetRect, contentRect)).toBe(false);
// outside the bottom triangular portion of the trapezoid
expect(mouseWithinRegion(420, 460, Direction.Right, targetRect, contentRect)).toBe(false);
});
it("direction=top", () => {
const targetRect = {
width: 20,
height: 20,
top: 300,
right: 370,
bottom: 320,
left: 350,
} as DOMRect;
const contentRect = {
width: 400,
height: 100,
top: 100,
right: 550,
bottom: 200,
left: 150,
} as DOMRect;
// just within top right corner of contentRect
expect(mouseWithinRegion(549, 101, Direction.Top, targetRect, contentRect)).toBe(true);
// just outside top right corner of contentRect, within buffer
expect(mouseWithinRegion(549, 99, Direction.Top, targetRect, contentRect)).toBe(true);
// just within bottom left corner of targetRect
expect(mouseWithinRegion(351, 319, Direction.Top, targetRect, contentRect)).toBe(true);
// within the left triangular portion of the trapezoid
expect(mouseWithinRegion(240, 260, Direction.Top, targetRect, contentRect)).toBe(true);
// within the right triangular portion of the trapezoid
expect(mouseWithinRegion(480, 260, Direction.Top, targetRect, contentRect)).toBe(true);
// outside the left triangular portion of the trapezoid
expect(mouseWithinRegion(220, 260, Direction.Top, targetRect, contentRect)).toBe(false);
// outside the right triangular portion of the trapezoid
expect(mouseWithinRegion(500, 260, Direction.Top, targetRect, contentRect)).toBe(false);
});
it("direction=bottom", () => {
const targetRect = {
width: 20,
height: 20,
top: 300,
right: 370,
bottom: 320,
left: 350,
} as DOMRect;
const contentRect = {
width: 400,
height: 100,
top: 420,
right: 550,
bottom: 520,
left: 150,
} as DOMRect;
// just within bottom left corner of contentRect
expect(mouseWithinRegion(101, 519, Direction.Bottom, targetRect, contentRect)).toBe(true);
// just outside bottom left corner of contentRect, within buffer
expect(mouseWithinRegion(101, 521, Direction.Bottom, targetRect, contentRect)).toBe(true);
// just within top left corner of targetRect
expect(mouseWithinRegion(351, 301, Direction.Bottom, targetRect, contentRect)).toBe(true);
// within the left triangular portion of the trapezoid
expect(mouseWithinRegion(240, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
// within the right triangular portion of the trapezoid
expect(mouseWithinRegion(480, 360, Direction.Bottom, targetRect, contentRect)).toBe(true);
// outside the left triangular portion of the trapezoid
expect(mouseWithinRegion(220, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
// outside the right triangular portion of the trapezoid
expect(mouseWithinRegion(500, 360, Direction.Bottom, targetRect, contentRect)).toBe(false);
});
});
});

View file

@ -0,0 +1,125 @@
/*
Copyright 2021 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.
*/
// skinned-sdk should be the first import in most tests
import '../../../skinned-sdk';
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import * as TestUtils from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { Room } from "matrix-js-sdk/src/models/room";
import _PollCreateDialog from "../../../../src/components/views/elements/PollCreateDialog";
const PollCreateDialog = TestUtils.wrapInMatrixClientContext(_PollCreateDialog);
// Fake date to give a predictable snapshot
const realDateNow = Date.now;
const realDateToISOString = Date.prototype.toISOString;
Date.now = jest.fn(() => 2345678901234);
// eslint-disable-next-line no-extend-native
Date.prototype.toISOString = jest.fn(() => "2021-11-23T14:35:14.240Z");
afterAll(() => {
Date.now = realDateNow;
// eslint-disable-next-line no-extend-native
Date.prototype.toISOString = realDateToISOString;
});
describe("PollCreateDialog", () => {
it("renders a blank poll", () => {
const dialog = mount(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />,
);
expect(dialog).toMatchSnapshot();
});
it("renders a question and some options", () => {
const dialog = mount(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />,
);
expect(submitIsDisabled(dialog)).toBe(true);
// When I set some values in the boxes
changeValue(
dialog,
"Question or topic",
"How many turnips is the optimal number?",
);
changeValue(dialog, "Option 1", "As many as my neighbour");
changeValue(dialog, "Option 2", "The question is meaningless");
dialog.find("div.mx_PollCreateDialog_addOption").simulate("click");
changeValue(dialog, "Option 3", "Mu");
expect(dialog).toMatchSnapshot();
});
it("doesn't allow submitting until there are options", () => {
const dialog = mount(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />,
);
expect(submitIsDisabled(dialog)).toBe(true);
});
it("does allow submitting when there are options and a question", () => {
// Given a dialog with no info in (which I am unable to submit)
const dialog = mount(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />,
);
expect(submitIsDisabled(dialog)).toBe(true);
// When I set some values in the boxes
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
// Then I am able to submit
expect(submitIsDisabled(dialog)).toBe(false);
});
it("displays a spinner after submitting", () => {
TestUtils.stubClient();
MatrixClientPeg.get().sendEvent = jest.fn(() => Promise.resolve());
const dialog = mount(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />,
);
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
expect(dialog.find("Spinner").length).toBe(0);
dialog.find("button").simulate("click");
expect(dialog.find("Spinner").length).toBe(1);
});
});
function createRoom(): Room {
return new Room(
"roomid",
MatrixClientPeg.get(),
"@name:example.com",
{},
);
}
function changeValue(wrapper: ReactWrapper, labelText: string, value: string) {
wrapper.find(`input[label="${labelText}"]`).simulate(
"change",
{ target: { value: value } },
);
}
function submitIsDisabled(wrapper: ReactWrapper) {
return wrapper.find('button[type="submit"]').prop("aria-disabled") === true;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -282,6 +282,30 @@ describe("<TextualBody />", () => {
'!ZxbRYPQXDXKGmDnJNg:example.com</a></span> with vias</span>',
);
});
it('renders formatted body without html corretly', () => {
const ev = mkEvent({
type: "m.room.message",
room: "room_id",
user: "sender",
content: {
body: "escaped \\*markdown\\*",
msgtype: "m.text",
format: "org.matrix.custom.html",
formatted_body: "escaped *markdown*",
},
event: true,
});
const wrapper = mount(<TextualBody mxEvent={ev} />);
const content = wrapper.find(".mx_EventTile_body");
expect(content.html()).toBe(
'<span class="mx_EventTile_body" dir="auto">' +
'escaped *markdown*' +
'</span>',
);
});
});
it("renders url previews correctly", () => {

File diff suppressed because it is too large Load diff

View file

@ -155,7 +155,6 @@ function render(room: Room): HTMLDivElement {
<RoomHeader
room={room}
inRoom={true}
onSettingsClick={() => {}}
onSearchClick={() => {}}
onForgetClick={() => {}}
onCallPlaced={(_type: PlaceCallType) => {}}

View file

@ -35,7 +35,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import DocumentOffset from '../../../../src/editor/offset';
import { Layout } from '../../../../src/settings/Layout';
import { Layout } from '../../../../src/settings/enums/Layout';
jest.mock("../../../../src/stores/RoomViewStore");

View file

@ -243,7 +243,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
onChange={[Function]}
>
<span
className="mx_Checkbox "
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked={false}
@ -258,8 +258,8 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
<div
className="mx_Checkbox_background"
>
<img
src="image-file-stub"
<div
className="mx_Checkbox_checkmark"
/>
</div>
<div>

View file

@ -46,7 +46,7 @@ exports[`ThemeChoicePanel renders the theme choice UI 1`] = `
value="light"
>
<label
className="mx_RadioButton mx_ThemeSelector_light mx_RadioButton_disabled mx_RadioButton_outlined"
className="mx_StyledRadioButton mx_ThemeSelector_light mx_StyledRadioButton_disabled mx_StyledRadioButton_outlined"
>
<input
checked={false}
@ -60,12 +60,12 @@ exports[`ThemeChoicePanel renders the theme choice UI 1`] = `
<div />
</div>
<div
className="mx_RadioButton_content"
className="mx_StyledRadioButton_content"
>
Light
</div>
<div
className="mx_RadioButton_spacer"
className="mx_StyledRadioButton_spacer"
/>
</label>
</StyledRadioButton>
@ -80,7 +80,7 @@ exports[`ThemeChoicePanel renders the theme choice UI 1`] = `
value="dark"
>
<label
className="mx_RadioButton mx_ThemeSelector_dark mx_RadioButton_disabled mx_RadioButton_outlined"
className="mx_StyledRadioButton mx_ThemeSelector_dark mx_StyledRadioButton_disabled mx_StyledRadioButton_outlined"
>
<input
checked={false}
@ -94,12 +94,12 @@ exports[`ThemeChoicePanel renders the theme choice UI 1`] = `
<div />
</div>
<div
className="mx_RadioButton_content"
className="mx_StyledRadioButton_content"
>
Dark
</div>
<div
className="mx_RadioButton_spacer"
className="mx_StyledRadioButton_spacer"
/>
</label>
</StyledRadioButton>

View file

@ -18,6 +18,8 @@ import '../skinned-sdk'; // Must be first for skinning to work
import { parseEvent } from "../../src/editor/deserialize";
import { createPartCreator } from "./mock";
const FOUR_SPACES = " ".repeat(4);
function htmlMessage(formattedBody, msgtype = "m.text") {
return {
getContent() {
@ -197,7 +199,6 @@ describe('editor/deserialize', function() {
it('code block with no trailing text', function() {
const html = "<pre><code>0xDEADBEEF\n</code></pre>\n";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
console.log(parts);
expect(parts.length).toBe(5);
expect(parts[0]).toStrictEqual({ type: "plain", text: "```" });
expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" });
@ -236,6 +237,26 @@ describe('editor/deserialize', function() {
expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[4]).toStrictEqual({ type: "plain", text: "3. Finish" });
});
it('nested unordered lists', () => {
const html = "<ul><li>Oak<ul><li>Spruce<ul><li>Birch</li></ul></li></ul></li></ul>";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(5);
expect(parts[0]).toStrictEqual({ type: "plain", text: "- Oak" });
expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[2]).toStrictEqual({ type: "plain", text: `${FOUR_SPACES}- Spruce` });
expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[4]).toStrictEqual({ type: "plain", text: `${FOUR_SPACES.repeat(2)}- Birch` });
});
it('nested ordered lists', () => {
const html = "<ol><li>Oak<ol><li>Spruce<ol><li>Birch</li></ol></li></ol></li></ol>";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(5);
expect(parts[0]).toStrictEqual({ type: "plain", text: "1. Oak" });
expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[2]).toStrictEqual({ type: "plain", text: `${FOUR_SPACES}1. Spruce` });
expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[4]).toStrictEqual({ type: "plain", text: `${FOUR_SPACES.repeat(2)}1. Birch` });
});
it('mx-reply is stripped', function() {
const html = "<mx-reply>foo</mx-reply>bar";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));

View file

@ -2,3 +2,5 @@ node_modules
*.png
element/env
performance-entries.json
lib
logs

View file

@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc -p ./tsconfig.json"
},
"author": "",
"license": "ISC",
@ -15,5 +16,8 @@
"request": "^2.88.0",
"request-promise-native": "^1.0.7",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/puppeteer": "^5.4.4"
}
}

View file

@ -35,5 +35,6 @@ trap 'handle_error' ERR
if [ $has_custom_app -ne "1" ]; then
./element/start.sh
fi
node start.js $@
yarn build
node lib/start.js $@
stop_servers

View file

@ -0,0 +1,24 @@
/*
Copyright 2021 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 "matrix-react-sdk/src/@types/global"; // load matrix-react-sdk's type extensions first
declare global {
interface Window {
mxPerformanceMonitor: any;
mxPerformanceEntryNames: any;
}
}

View file

@ -15,16 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = class LogBuffer {
constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") {
import { Page, PageEventObject } from "puppeteer";
export class LogBuffer<EventMapperArg extends Parameters<Parameters<Page['on']>[1]>[0]> {
buffer: string;
constructor(
page: Page,
eventName: keyof PageEventObject,
eventMapper: (arg: EventMapperArg) => Promise<string>,
initialValue = "",
) {
this.buffer = initialValue;
page.on(eventName, (arg) => {
const result = eventMapper(arg);
if (reduceAsync) {
result.then((r) => this.buffer += r);
} else {
this.buffer += result;
}
page.on(eventName, (arg: EventMapperArg) => {
eventMapper(arg).then((r) => this.buffer += r);
});
}
};
}

View file

@ -15,14 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = class Logger {
constructor(username) {
this.indent = 0;
this.username = username;
this.muted = false;
}
export class Logger {
private indent = 0;
private muted = false;
startGroup(description) {
constructor(readonly username: string) {}
public startGroup(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
console.log(`${indent} * ${this.username} ${description}:`);
@ -31,12 +30,12 @@ module.exports = class Logger {
return this;
}
endGroup() {
public endGroup(): Logger {
this.indent -= 1;
return this;
}
step(description) {
public step(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
process.stdout.write(`${indent} * ${this.username} ${description} ... `);
@ -44,20 +43,20 @@ module.exports = class Logger {
return this;
}
done(status = "done") {
public done(status = "done"): Logger {
if (!this.muted) {
process.stdout.write(status + "\n");
}
return this;
}
mute() {
public mute(): Logger {
this.muted = true;
return this;
}
unmute() {
public unmute(): Logger {
this.muted = false;
return this;
}
};
}

View file

@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const request = require('request-promise-native');
const cheerio = require('cheerio');
const url = require("url");
import request = require('request-promise-native');
import * as cheerio from 'cheerio';
import * as url from "url";
module.exports.approveConsent = async function(consentUrl) {
export const approveConsent = async function(consentUrl: string): Promise<void> {
const body = await request.get(consentUrl);
const doc = cheerio.load(body);
const v = doc("input[name=v]").val();

View file

@ -15,13 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { exec } = require('child_process');
const request = require('request-promise-native');
import { exec } from 'child_process';
import request = require('request-promise-native');
import { RestSession } from './session';
import { RestMultiSession } from './multi';
const RestSession = require('./session');
const RestMultiSession = require('./multi');
interface ExecResult {
stdout: string;
stderr: string;
}
function execAsync(command, options) {
function execAsync(command: string, options: Parameters<typeof exec>[1]): Promise<ExecResult> {
return new Promise((resolve, reject) => {
exec(command, options, (error, stdout, stderr) => {
if (error) {
@ -33,27 +37,32 @@ function execAsync(command, options) {
});
}
module.exports = class RestSessionCreator {
constructor(synapseSubdir, hsUrl, cwd) {
this.synapseSubdir = synapseSubdir;
this.hsUrl = hsUrl;
this.cwd = cwd;
}
export interface Credentials {
accessToken: string;
homeServer: string;
userId: string;
deviceId: string;
hsUrl: string;
}
async createSessionRange(usernames, password, groupName) {
export class RestSessionCreator {
constructor(private readonly synapseSubdir: string, private readonly hsUrl: string, private readonly cwd: string) {}
public async createSessionRange(usernames: string[], password: string,
groupName: string): Promise<RestMultiSession> {
const sessionPromises = usernames.map((username) => this.createSession(username, password));
const sessions = await Promise.all(sessionPromises);
return new RestMultiSession(sessions, groupName);
}
async createSession(username, password) {
await this._register(username, password);
public async createSession(username: string, password: string): Promise<RestSession> {
await this.register(username, password);
console.log(` * created REST user ${username} ... done`);
const authResult = await this._authenticate(username, password);
const authResult = await this.authenticate(username, password);
return new RestSession(authResult);
}
async _register(username, password) {
private async register(username: string, password: string): Promise<void> {
const registerArgs = [
'-c homeserver.yaml',
`-u ${username}`,
@ -71,7 +80,7 @@ module.exports = class RestSessionCreator {
await execAsync(allCmds, { cwd: this.cwd, encoding: 'utf-8' });
}
async _authenticate(username, password) {
private async authenticate(username: string, password: string): Promise<Credentials> {
const requestBody = {
"type": "m.login.password",
"identifier": {
@ -90,4 +99,4 @@ module.exports = class RestSessionCreator {
hsUrl: this.hsUrl,
};
}
};
}

View file

@ -15,19 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const Logger = require('../logger');
import { Logger } from '../logger';
import { RestSession } from "./session";
import { RestRoom } from "./room";
module.exports = class RestMultiSession {
constructor(sessions, groupName) {
export class RestMultiSession {
readonly log: Logger;
constructor(public readonly sessions: RestSession[], groupName: string) {
this.log = new Logger(groupName);
this.sessions = sessions;
}
slice(groupName, start, end) {
public slice(groupName: string, start: number, end?: number): RestMultiSession {
return new RestMultiSession(this.sessions.slice(start, end), groupName);
}
pop(userName) {
public pop(userName: string): RestSession {
const idx = this.sessions.findIndex((s) => s.userName() === userName);
if (idx === -1) {
throw new Error(`user ${userName} not found`);
@ -36,9 +39,9 @@ module.exports = class RestMultiSession {
return session;
}
async setDisplayName(fn) {
public async setDisplayName(fn: (s: RestSession) => string): Promise<void> {
this.log.step("set their display name");
await Promise.all(this.sessions.map(async (s) => {
await Promise.all(this.sessions.map(async (s: RestSession) => {
s.log.mute();
await s.setDisplayName(fn(s));
s.log.unmute();
@ -46,7 +49,7 @@ module.exports = class RestMultiSession {
this.log.done();
}
async join(roomIdOrAlias) {
public async join(roomIdOrAlias: string): Promise<RestMultiRoom> {
this.log.step(`join ${roomIdOrAlias}`);
const rooms = await Promise.all(this.sessions.map(async (s) => {
s.log.mute();
@ -58,22 +61,19 @@ module.exports = class RestMultiSession {
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
}
room(roomIdOrAlias) {
public room(roomIdOrAlias: string): RestMultiRoom {
const rooms = this.sessions.map(s => s.room(roomIdOrAlias));
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
}
};
}
class RestMultiRoom {
constructor(rooms, roomIdOrAlias, log) {
this.rooms = rooms;
this.roomIdOrAlias = roomIdOrAlias;
this.log = log;
}
constructor(public readonly rooms: RestRoom[], private readonly roomIdOrAlias: string,
private readonly log: Logger) {}
async talk(message) {
public async talk(message: string): Promise<void> {
this.log.step(`say "${message}" in ${this.roomIdOrAlias}`);
await Promise.all(this.rooms.map(async (r) => {
await Promise.all(this.rooms.map(async (r: RestRoom) => {
r.log.mute();
await r.talk(message);
r.log.unmute();
@ -81,7 +81,7 @@ class RestMultiRoom {
this.log.done();
}
async leave() {
public async leave() {
this.log.step(`leave ${this.roomIdOrAlias}`);
await Promise.all(this.rooms.map(async (r) => {
r.log.mute();

View file

@ -15,20 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const uuidv4 = require('uuid/v4');
import uuidv4 = require('uuid/v4');
import { RestSession } from "./session";
import { Logger } from "../logger";
/* no pun intented */
module.exports = class RestRoom {
constructor(session, roomId, log) {
this.session = session;
this._roomId = roomId;
this.log = log;
}
export class RestRoom {
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
async talk(message) {
this.log.step(`says "${message}" in ${this._roomId}`);
async talk(message: string): Promise<void> {
this.log.step(`says "${message}" in ${this.roomId}`);
const txId = uuidv4();
await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, {
await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
"msgtype": "m.text",
"body": message,
});
@ -36,13 +34,9 @@ module.exports = class RestRoom {
return txId;
}
async leave() {
this.log.step(`leaves ${this._roomId}`);
await this.session._post(`/rooms/${this._roomId}/leave`);
async leave(): Promise<void> {
this.log.step(`leaves ${this.roomId}`);
await this.session.post(`/rooms/${this.roomId}/leave`);
this.log.done();
}
roomId() {
return this._roomId;
}
};
}

View file

@ -0,0 +1,137 @@
/*
Copyright 2018 New Vector Ltd
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 = require('request-promise-native');
import { Logger } from '../logger';
import { RestRoom } from './room';
import { approveConsent } from './consent';
import { Credentials } from "./creator";
interface RoomOptions {
invite: string;
public: boolean;
topic: string;
dm: boolean;
}
export class RestSession {
private _displayName: string = null;
private readonly rooms: Record<string, RestRoom> = {};
readonly log: Logger;
constructor(private readonly credentials: Credentials) {
this.log = new Logger(credentials.userId);
}
userId(): string {
return this.credentials.userId;
}
userName(): string {
return this.credentials.userId.split(":")[0].substr(1);
}
displayName(): string {
return this._displayName;
}
async setDisplayName(displayName: string): Promise<void> {
this.log.step(`sets their display name to ${displayName}`);
this._displayName = displayName;
await this.put(`/profile/${this.credentials.userId}/displayname`, {
displayname: displayName,
});
this.log.done();
}
async join(roomIdOrAlias: string): Promise<RestRoom> {
this.log.step(`joins ${roomIdOrAlias}`);
const roomId = (await this.post(`/join/${encodeURIComponent(roomIdOrAlias)}`)).room_id;
this.log.done();
const room = new RestRoom(this, roomId, this.log);
this.rooms[roomId] = room;
this.rooms[roomIdOrAlias] = room;
return room;
}
room(roomIdOrAlias: string): RestRoom {
if (this.rooms.hasOwnProperty(roomIdOrAlias)) {
return this.rooms[roomIdOrAlias];
} else {
throw new Error(`${this.credentials.userId} is not in ${roomIdOrAlias}`);
}
}
async createRoom(name: string, options: RoomOptions): Promise<RestRoom> {
this.log.step(`creates room ${name}`);
const body = {
name,
};
if (options.invite) {
body['invite'] = options.invite;
}
if (options.public) {
body['visibility'] = "public";
} else {
body['visibility'] = "private";
}
if (options.dm) {
body['is_direct'] = true;
}
if (options.topic) {
body['topic'] = options.topic;
}
const roomId = (await this.post(`/createRoom`, body)).room_id;
this.log.done();
return new RestRoom(this, roomId, this.log);
}
post(csApiPath: string, body?: any): Promise<any> {
return this.request("POST", csApiPath, body);
}
put(csApiPath: string, body?: any): Promise<any> {
return this.request("PUT", csApiPath, body);
}
async request(method: string, csApiPath: string, body?: any): Promise<any> {
try {
return await request({
url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`,
method,
headers: {
"Authorization": `Bearer ${this.credentials.accessToken}`,
},
json: true,
body,
});
} catch (err) {
if (!err.response) {
throw err;
}
const responseBody = err.response.body;
if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') {
await approveConsent(responseBody.consent_uri);
return this.request(method, csApiPath, body);
} else if (responseBody && responseBody.error) {
throw new Error(`${method} ${csApiPath}: ${responseBody.error}`);
} else {
throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`);
}
}
}
}

View file

@ -14,14 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { range } = require('./util');
const signup = require('./usecases/signup');
const toastScenarios = require('./scenarios/toast');
const roomDirectoryScenarios = require('./scenarios/directory');
const lazyLoadingScenarios = require('./scenarios/lazy-loading');
const e2eEncryptionScenarios = require('./scenarios/e2e-encryption');
import { range } from './util';
import { signup } from './usecases/signup';
import { toastScenarios } from './scenarios/toast';
import { roomDirectoryScenarios } from './scenarios/directory';
import { lazyLoadingScenarios } from './scenarios/lazy-loading';
import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
import { ElementSession } from "./session";
import { RestSessionCreator } from "./rest/creator";
import { RestMultiSession } from "./rest/multi";
import { spacesScenarios } from './scenarios/spaces';
import { RestSession } from "./rest/session";
module.exports = async function scenario(createSession, restCreator) {
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
restCreator: RestSessionCreator): Promise<void> {
let firstUser = true;
async function createUser(username) {
const session = await createSession(username);
@ -44,13 +50,12 @@ module.exports = async function scenario(createSession, restCreator) {
const charlies = await createRestUsers(restCreator);
await lazyLoadingScenarios(alice, bob, charlies);
// do spaces scenarios last as the rest of the tests may get confused by spaces
// XXX: disabled for now as fails in CI but succeeds locally
// await spacesScenarios(alice, bob);
};
await spacesScenarios(alice, bob);
}
async function createRestUsers(restCreator) {
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
const usernames = range(1, 10).map((i) => `charly-${i}`);
const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10");
await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`);
await charlies.setDisplayName((s: RestSession) => `Charly #${s.userName().split('-')[1]}`);
return charlies;
}

View file

@ -15,13 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const join = require('../usecases/join');
const sendMessage = require('../usecases/send-message');
const { receiveMessage } = require('../usecases/timeline');
const { createRoom } = require('../usecases/create-room');
const { changeRoomSettings } = require('../usecases/room-settings');
import { join } from '../usecases/join';
import { sendMessage } from '../usecases/send-message';
import { receiveMessage } from '../usecases/timeline';
import { createRoom } from '../usecases/create-room';
import { changeRoomSettings } from '../usecases/room-settings';
import { ElementSession } from "../session";
module.exports = async function roomDirectoryScenarios(alice, bob) {
export async function roomDirectoryScenarios(alice: ElementSession, bob: ElementSession) {
console.log(" creating a public room and join through directory:");
const room = 'test';
await createRoom(alice, room);
@ -33,4 +34,4 @@ module.exports = async function roomDirectoryScenarios(alice, bob) {
const aliceMessage = "hi Bob, welcome!";
await sendMessage(alice, aliceMessage);
await receiveMessage(bob, { sender: "alice", body: aliceMessage });
};
}

View file

@ -15,23 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { ElementSession } from "../session";
const sendMessage = require('../usecases/send-message');
const acceptInvite = require('../usecases/accept-invite');
const { receiveMessage } = require('../usecases/timeline');
const { createDm } = require('../usecases/create-room');
const { checkRoomSettings } = require('../usecases/room-settings');
const { startSasVerification, acceptSasVerification } = require('../usecases/verify');
const { setupSecureBackup } = require('../usecases/security');
const { measureStart, measureStop } = require('../util');
import { sendMessage } from '../usecases/send-message';
import { acceptInvite } from '../usecases/accept-invite';
import { receiveMessage } from '../usecases/timeline';
import { createDm } from '../usecases/create-room';
import { checkRoomSettings } from '../usecases/room-settings';
import { startSasVerification, acceptSasVerification } from '../usecases/verify';
import { setupSecureBackup } from '../usecases/security';
import { strict as assert } from 'assert';
import { measureStart, measureStop } from '../util';
module.exports = async function e2eEncryptionScenarios(alice, bob) {
export async function e2eEncryptionScenarios(alice: ElementSession, bob: ElementSession) {
console.log(" creating an e2e encrypted DM and join through invite:");
await createDm(bob, ['@alice:localhost']);
await checkRoomSettings(bob, { encryption: true }); // for sanity, should be e2e-by-default
await acceptInvite(alice, 'bob');
// do sas verifcation
// do sas verification
bob.log.step(`starts SAS verification with ${alice.username}`);
await measureStart(bob, "mx_VerifyE2EEUser");
const bobSasPromise = startSasVerification(bob, alice.username);
@ -49,4 +50,4 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) {
await sendMessage(bob, bobMessage);
await receiveMessage(alice, { sender: "bob", body: bobMessage, encrypted: true });
await setupSecureBackup(alice);
};
}

View file

@ -15,25 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
const { delay } = require('../util');
const join = require('../usecases/join');
const sendMessage = require('../usecases/send-message');
const {
import { delay } from '../util';
import { join } from '../usecases/join';
import { sendMessage } from '../usecases/send-message';
import {
checkTimelineContains,
scrollToTimelineTop,
} = require('../usecases/timeline');
const { createRoom } = require('../usecases/create-room');
const { getMembersInMemberlist } = require('../usecases/memberlist');
const { changeRoomSettings } = require('../usecases/room-settings');
} from '../usecases/timeline';
import { createRoom } from '../usecases/create-room';
import { getMembersInMemberlist } from '../usecases/memberlist';
import { changeRoomSettings } from '../usecases/room-settings';
import { strict as assert } from 'assert';
import { RestMultiSession } from "../rest/multi";
import { ElementSession } from "../session";
module.exports = async function lazyLoadingScenarios(alice, bob, charlies) {
export async function lazyLoadingScenarios(alice: ElementSession,
bob: ElementSession, charlies: RestMultiSession): Promise<void> {
console.log(" creating a room for lazy loading member scenarios:");
const charly1to5 = charlies.slice("charly-1..5", 0, 5);
const charly6to10 = charlies.slice("charly-6..10", 5);
assert(charly1to5.sessions.length, 5);
assert(charly6to10.sessions.length, 5);
assert(charly1to5.sessions.length == 5);
assert(charly6to10.sessions.length == 5);
await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5);
await checkPaginatedDisplayNames(alice, charly1to5);
await checkMemberList(alice, charly1to5);
@ -43,14 +45,15 @@ module.exports = async function lazyLoadingScenarios(alice, bob, charlies) {
await delay(1000);
await checkMemberListLacksCharlies(alice, charlies);
await checkMemberListLacksCharlies(bob, charlies);
};
}
const room = "Lazy Loading Test";
const alias = "#lltest:localhost";
const charlyMsg1 = "hi bob!";
const charlyMsg2 = "how's it going??";
async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
async function setupRoomWithBobAliceAndCharlies(alice: ElementSession, bob: ElementSession,
charlies: RestMultiSession): Promise<void> {
await createRoom(bob, room);
await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
// wait for alias to be set by server after clicking "save"
@ -67,7 +70,7 @@ async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) {
await join(alice, alias);
}
async function checkPaginatedDisplayNames(alice, charlies) {
async function checkPaginatedDisplayNames(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
await scrollToTimelineTop(alice);
//alice should see 2 messages from every charly with
//the correct display name
@ -82,7 +85,7 @@ async function checkPaginatedDisplayNames(alice, charlies) {
await checkTimelineContains(alice, expectedMessages, charlies.log.username);
}
async function checkMemberList(alice, charlies) {
async function checkMemberList(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
alice.log.step(`checks the memberlist contains herself, bob and ${charlies.log.username}`);
const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName);
assert(displayNames.includes("alice"));
@ -95,7 +98,7 @@ async function checkMemberList(alice, charlies) {
alice.log.done();
}
async function checkMemberListLacksCharlies(session, charlies) {
async function checkMemberListLacksCharlies(session: ElementSession, charlies: RestMultiSession): Promise<void> {
session.log.step(`checks the memberlist doesn't contain ${charlies.log.username}`);
const displayNames = (await getMembersInMemberlist(session)).map((m) => m.displayName);
charlies.sessions.forEach((charly) => {
@ -106,7 +109,7 @@ async function checkMemberListLacksCharlies(session, charlies) {
session.log.done();
}
async function joinCharliesWhileAliceIsOffline(alice, charly6to10) {
async function joinCharliesWhileAliceIsOffline(alice: ElementSession, charly6to10: RestMultiSession) {
await alice.setOffline(true);
await delay(1000);
const members6to10 = await charly6to10.join(alias);

View file

@ -1,3 +1,5 @@
import { ElementSession } from "../session";
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
@ -14,18 +16,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { createSpace, inviteSpace } = require("../usecases/create-space");
import { createSpace, inviteSpace } from "../usecases/create-space";
module.exports = async function spacesScenarios(alice, bob) {
export async function spacesScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
console.log(" creating a space for spaces scenarios:");
await alice.delay(1000); // wait for dialogs to close
await setupSpaceUsingAliceAndInviteBob(alice, bob);
};
}
const space = "Test Space";
async function setupSpaceUsingAliceAndInviteBob(alice, bob) {
async function setupSpaceUsingAliceAndInviteBob(alice: ElementSession, bob: ElementSession): Promise<void> {
await createSpace(alice, space);
await inviteSpace(alice, space, "@bob:localhost");
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { assertNoToasts, acceptToast, rejectToast } = require("../usecases/toasts");
import { assertNoToasts, acceptToast, rejectToast } from "../usecases/toasts";
import { ElementSession } from "../session";
module.exports = async function toastScenarios(alice, bob) {
export async function toastScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
console.log(" checking and clearing toasts:");
alice.log.startGroup(`clears toasts`);
@ -25,7 +26,8 @@ module.exports = async function toastScenarios(alice, bob) {
alice.log.done();
alice.log.step(`accepts analytics toast`);
await acceptToast(alice, "Help us improve Element");
await acceptToast(alice, "Help improve Element");
await rejectToast(alice, "Testing small changes");
alice.log.done();
alice.log.step(`checks no remaining toasts`);
@ -39,11 +41,12 @@ module.exports = async function toastScenarios(alice, bob) {
bob.log.done();
bob.log.step(`reject analytics toast`);
await rejectToast(bob, "Help us improve Element");
await rejectToast(bob, "Help improve Element");
await rejectToast(bob, "Testing small changes");
bob.log.done();
bob.log.step(`checks no remaining toasts`);
await assertNoToasts(bob);
bob.log.done();
bob.log.endGroup();
};
}

View file

@ -15,31 +15,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const puppeteer = require('puppeteer');
const Logger = require('./logger');
const LogBuffer = require('./logbuffer');
const { delay } = require('./util');
import * as puppeteer from 'puppeteer';
import { Logger } from './logger';
import { LogBuffer } from './logbuffer';
import { delay } from './util';
const DEFAULT_TIMEOUT = 20000;
module.exports = class ElementSession {
constructor(browser, page, username, elementServer, hsUrl) {
this.browser = browser;
this.page = page;
this.hsUrl = hsUrl;
this.elementServer = elementServer;
this.username = username;
this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`);
this.networkLog = new LogBuffer(page, "requestfinished", async (req) => {
const type = req.resourceType();
const response = await req.response();
return `${type} ${response.status()} ${req.method()} ${req.url()} \n`;
}, true);
interface XHRLogger {
logs: () => string;
}
export class ElementSession {
readonly consoleLog: LogBuffer<puppeteer.ConsoleMessage>;
readonly networkLog: LogBuffer<puppeteer.HTTPRequest>;
readonly log: Logger;
constructor(readonly browser: puppeteer.Browser, readonly page: puppeteer.Page, readonly username: string,
readonly elementServer: string, readonly hsUrl: string) {
this.consoleLog = new LogBuffer(page, "console",
async (msg: puppeteer.ConsoleMessage) => Promise.resolve(`${msg.text()}\n`));
this.networkLog = new LogBuffer(page,
"requestfinished", async (req: puppeteer.HTTPRequest) => {
const type = req.resourceType();
const response = await req.response();
return `${type} ${response.status()} ${req.method()} ${req.url()} \n`;
});
this.log = new Logger(this.username);
}
static async create(username, puppeteerOptions, elementServer, hsUrl, throttleCpuFactor = 1) {
public static async create(username: string, puppeteerOptions: Parameters<typeof puppeteer.launch>[0],
elementServer: string, hsUrl: string, throttleCpuFactor = 1): Promise<ElementSession> {
const browser = await puppeteer.launch(puppeteerOptions);
const page = await browser.newPage();
await page.setViewport({
@ -54,7 +60,7 @@ module.exports = class ElementSession {
return new ElementSession(browser, page, username, elementServer, hsUrl);
}
async tryGetInnertext(selector) {
public async tryGetInnertext(selector: string): Promise<string> {
const field = await this.page.$(selector);
if (field != null) {
const textHandle = await field.getProperty('innerText');
@ -63,32 +69,32 @@ module.exports = class ElementSession {
return null;
}
async getElementProperty(handle, property) {
public async getElementProperty(handle: puppeteer.ElementHandle, property: string): Promise<string> {
const propHandle = await handle.getProperty(property);
return await propHandle.jsonValue();
}
innerText(field) {
public innerText(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'innerText');
}
getOuterHTML(field) {
public getOuterHTML(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'outerHTML');
}
isChecked(field) {
public isChecked(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'checked');
}
consoleLogs() {
public consoleLogs(): string {
return this.consoleLog.buffer;
}
networkLogs() {
public networkLogs(): string {
return this.networkLog.buffer;
}
logXHRRequests() {
public logXHRRequests(): XHRLogger {
let buffer = "";
this.page.on('requestfinished', async (req) => {
const type = req.resourceType();
@ -107,11 +113,11 @@ module.exports = class ElementSession {
};
}
async printElements(label, elements) {
public async printElements(label: string, elements: puppeteer.ElementHandle[] ): Promise<void> {
console.log(label, await Promise.all(elements.map(this.getOuterHTML)));
}
async replaceInputText(input, text) {
public async replaceInputText(input: puppeteer.ElementHandle, text: string): Promise<void> {
// click 3 times to select all text
await input.click({ clickCount: 3 });
// waiting here solves not having selected all the text by the 3x click above,
@ -123,21 +129,22 @@ module.exports = class ElementSession {
await input.type(text);
}
query(selector, timeout = DEFAULT_TIMEOUT, hidden = false) {
public query(selector: string, timeout: number = DEFAULT_TIMEOUT,
hidden = false): Promise<puppeteer.ElementHandle> {
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
}
async queryAll(selector) {
public async queryAll(selector: string): Promise<puppeteer.ElementHandle[]> {
const timeout = DEFAULT_TIMEOUT;
await this.query(selector, timeout);
return await this.page.$$(selector);
}
waitForReload() {
public waitForReload(): Promise<void> {
const timeout = DEFAULT_TIMEOUT;
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
this.browser.removeEventListener('domcontentloaded', callback);
this.page.off('domcontentloaded', callback);
reject(new Error(`timeout of ${timeout}ms for waitForReload elapsed`));
}, timeout);
@ -150,11 +157,11 @@ module.exports = class ElementSession {
});
}
waitForNewPage() {
public waitForNewPage(): Promise<void> {
const timeout = DEFAULT_TIMEOUT;
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
this.browser.removeListener('targetcreated', callback);
this.browser.off('targetcreated', callback);
reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`));
}, timeout);
@ -162,7 +169,7 @@ module.exports = class ElementSession {
if (target.type() !== 'page') {
return;
}
this.browser.removeListener('targetcreated', callback);
this.browser.off('targetcreated', callback);
clearTimeout(timeoutHandle);
const page = await target.page();
resolve(page);
@ -173,7 +180,7 @@ module.exports = class ElementSession {
}
/** wait for a /sync request started after this call that gets a 200 response */
async waitForNextSuccessfulSync() {
public async waitForNextSuccessfulSync(): Promise<void> {
const syncUrls = [];
function onRequest(request) {
if (request.url().indexOf("/sync") !== -1) {
@ -187,33 +194,33 @@ module.exports = class ElementSession {
return syncUrls.includes(response.request().url()) && response.status() === 200;
});
this.page.removeListener('request', onRequest);
this.page.off('request', onRequest);
}
goto(url) {
public goto(url: string): Promise<puppeteer.HTTPResponse> {
return this.page.goto(url);
}
url(path) {
public url(path: string): string {
return this.elementServer + path;
}
delay(ms) {
public delay(ms: number) {
return delay(ms);
}
async setOffline(enabled) {
public async setOffline(enabled: boolean): Promise<void> {
const description = enabled ? "offline" : "back online";
this.log.step(`goes ${description}`);
await this.page.setOfflineMode(enabled);
this.log.done();
}
async close() {
public async close(): Promise<void> {
return this.browser.close();
}
async poll(callback, interval = 100) {
public async poll(callback: () => Promise<boolean>, interval = 100): Promise<boolean> {
const timeout = DEFAULT_TIMEOUT;
let waited = 0;
while (waited < timeout) {
@ -225,4 +232,4 @@ module.exports = class ElementSession {
}
return false;
}
};
}

View file

@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { findSublist } = require("./create-room");
import { findSublist } from "./create-room";
import { ElementSession } from "../session";
module.exports = async function acceptInvite(session, name) {
export async function acceptInvite(session: ElementSession, name: string): Promise<void> {
session.log.step(`accepts "${name}" invite`);
const inviteSublist = await findSublist(session, "invites");
const invitesHandles = await inviteSublist.$$(".mx_RoomTile_name");
@ -35,4 +36,4 @@ module.exports = async function acceptInvite(session, name) {
await acceptInvitationLink.click();
session.log.done();
};
}

View file

@ -15,18 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { measureStart, measureStop } = require('../util');
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
import * as puppeteer from "puppeteer";
async function openRoomDirectory(session) {
export async function openRoomDirectory(session: ElementSession): Promise<void> {
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
await roomDirectoryButton.click();
}
async function findSublist(session, name) {
export async function findSublist(session: ElementSession, name: string): Promise<puppeteer.ElementHandle> {
return await session.query(`.mx_RoomSublist[aria-label="${name}" i]`);
}
async function createRoom(session, roomName, encrypted=false) {
export async function createRoom(session: ElementSession, roomName: string, encrypted = false): Promise<void> {
session.log.step(`creates room "${roomName}"`);
const roomsSublist = await findSublist(session, "rooms");
@ -51,7 +53,7 @@ async function createRoom(session, roomName, encrypted=false) {
session.log.done();
}
async function createDm(session, invitees) {
export async function createDm(session: ElementSession, invitees: string[]): Promise<void> {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
await measureStart(session, "mx_CreateDM");
@ -83,5 +85,3 @@ async function createDm(session, invitees) {
await measureStop(session, "mx_CreateDM");
}
module.exports = { openRoomDirectory, findSublist, createRoom, createDm };

View file

@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
async function openSpaceCreateMenu(session) {
import { ElementSession } from "../session";
export async function openSpaceCreateMenu(session: ElementSession): Promise<void> {
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
await spaceCreateButton.click();
}
async function createSpace(session, name, isPublic = false) {
export async function createSpace(session: ElementSession, name: string, isPublic = false): Promise<void> {
session.log.step(`creates space "${name}"`);
await openSpaceCreateMenu(session);
@ -50,7 +52,7 @@ async function createSpace(session, name, isPublic = false) {
session.log.done();
}
async function inviteSpace(session, spaceName, userId) {
export async function inviteSpace(session: ElementSession, spaceName: string, userId: string): Promise<void> {
session.log.step(`invites "${userId}" to space "${spaceName}"`);
const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
@ -58,7 +60,7 @@ async function inviteSpace(session, spaceName, userId) {
button: 'right',
});
const inviteButton = await session.query('[aria-label="Invite people"]');
const inviteButton = await session.query('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]');
await inviteButton.click();
try {
@ -76,5 +78,3 @@ async function inviteSpace(session, spaceName, userId) {
await confirmButton.click();
session.log.done();
}
module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };

View file

@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function assertDialog(session, expectedTitle) {
export async function assertDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const titleElement = await session.query(".mx_Dialog .mx_Dialog_title");
const dialogHeader = await session.innerText(titleElement);
assert.equal(dialogHeader, expectedTitle);
}
async function acceptDialog(session, expectedTitle) {
export async function acceptDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const foundDialog = await acceptDialogMaybe(session, expectedTitle);
if (!foundDialog) {
throw new Error("could not find a dialog");
}
}
async function acceptDialogMaybe(session, expectedTitle) {
export async function acceptDialogMaybe(session: ElementSession, expectedTitle: string): Promise<boolean> {
let primaryButton = null;
try {
primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary");
@ -43,9 +44,3 @@ async function acceptDialogMaybe(session, expectedTitle) {
await primaryButton.click();
return true;
}
module.exports = {
assertDialog,
acceptDialog,
acceptDialogMaybe,
};

View file

@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports = async function invite(session, userId) {
import { ElementSession } from "../session";
export async function invite(session: ElementSession, userId: string): Promise<void> {
session.log.step(`invites "${userId}" to room`);
await session.delay(1000);
const memberPanelButton = await session.query(".mx_RightPanel_membersButton");
@ -38,4 +40,4 @@ module.exports = async function invite(session, userId) {
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
};
}

View file

@ -15,10 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { openRoomDirectory } = require('./create-room');
const { measureStart, measureStop } = require('../util');
import { openRoomDirectory } from './create-room';
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
module.exports = async function join(session, roomName) {
export async function join(session: ElementSession, roomName: string): Promise<void> {
session.log.step(`joins room "${roomName}"`);
await measureStart(session, "mx_JoinRoom");
await openRoomDirectory(session);
@ -30,4 +31,4 @@ module.exports = async function join(session, roomName) {
await session.query('.mx_MessageComposer');
await measureStop(session, "mx_JoinRoom");
session.log.done();
};
}

View file

@ -15,11 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { openRoomSummaryCard } from "./rightpanel";
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
const { openRoomSummaryCard } = require("./rightpanel");
async function openMemberInfo(session, name) {
export async function openMemberInfo(session: ElementSession, name: String): Promise<void> {
const membersAndNames = await getMembersInMemberlist(session);
const matchingLabel = membersAndNames.filter((m) => {
return m.displayName === name;
@ -27,9 +28,13 @@ async function openMemberInfo(session, name) {
await matchingLabel.click();
}
module.exports.openMemberInfo = openMemberInfo;
interface Device {
id: string;
key: string;
}
module.exports.verifyDeviceForUser = async function(session, name, expectedDevice) {
export async function verifyDeviceForUser(session: ElementSession, name: string,
expectedDevice: Device): Promise<void> {
session.log.step(`verifies e2e device for ${name}`);
const membersAndNames = await getMembersInMemberlist(session);
const matchingLabel = membersAndNames.filter((m) => {
@ -51,19 +56,24 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic
console.log("my sas labels", sasLabels);
const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code");
assert.equal(dialogCodeFields.length, 2);
assert.strictEqual(dialogCodeFields.length, 2);
const deviceId = await session.innerText(dialogCodeFields[0]);
const deviceKey = await session.innerText(dialogCodeFields[1]);
assert.equal(expectedDevice.id, deviceId);
assert.equal(expectedDevice.key, deviceKey);
assert.strictEqual(expectedDevice.id, deviceId);
assert.strictEqual(expectedDevice.key, deviceKey);
const confirmButton = await session.query(".mx_Dialog_primary");
await confirmButton.click();
const closeMemberInfo = await session.query(".mx_MemberInfo_cancel");
await closeMemberInfo.click();
session.log.done();
};
}
async function getMembersInMemberlist(session) {
interface MemberName {
label: ElementHandle;
displayName: string;
}
export async function getMembersInMemberlist(session: ElementSession): Promise<MemberName[]> {
await openRoomSummaryCard(session);
const memberPanelButton = await session.query(".mx_RoomSummaryCard_icon_people");
// We are back at the room summary card
@ -74,5 +84,3 @@ async function getMembersInMemberlist(session) {
return { label: el, displayName: await session.innerText(el) };
}));
}
module.exports.getMembersInMemberlist = getMembersInMemberlist;

View file

@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports.openRoomRightPanel = async function(session) {
import { ElementSession } from "../session";
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
try {
await session.query('.mx_RoomHeader .mx_RightPanel_headerButton_highlight[aria-label="Room Info"]');
} catch (e) {
@ -22,9 +24,9 @@ module.exports.openRoomRightPanel = async function(session) {
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
await roomSummaryButton.click();
}
};
}
module.exports.goBackToRoomSummaryCard = async function(session) {
export async function goBackToRoomSummaryCard(session: ElementSession): Promise<void> {
for (let i = 0; i < 5; i++) {
try {
const backButton = await session.query(".mx_BaseCard_back", 500);
@ -39,9 +41,9 @@ module.exports.goBackToRoomSummaryCard = async function(session) {
}
}
}
};
}
module.exports.openRoomSummaryCard = async function(session) {
await module.exports.openRoomRightPanel(session);
await module.exports.goBackToRoomSummaryCard(session);
};
export async function openRoomSummaryCard(session: ElementSession) {
await openRoomRightPanel(session);
await goBackToRoomSummaryCard(session);
}

View file

@ -15,12 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { openRoomSummaryCard } from "./rightpanel";
import { acceptDialog } from './dialog';
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
const { openRoomSummaryCard } = require("./rightpanel");
const { acceptDialog } = require('./dialog');
async function setSettingsToggle(session, toggle, enabled) {
export async function setSettingsToggle(session: ElementSession, toggle: ElementHandle, enabled): Promise<boolean> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked !== enabled) {
@ -32,7 +33,8 @@ async function setSettingsToggle(session, toggle, enabled) {
}
}
async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
export async function checkSettingsToggle(session: ElementSession,
toggle: ElementHandle, shouldBeEnabled: boolean): Promise<void> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked === shouldBeEnabled) {
@ -43,7 +45,11 @@ async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
}
}
async function findTabs(session) {
interface Tabs {
securityTabButton: ElementHandle;
}
async function findTabs(session: ElementSession): Promise<Tabs> {
/// XXX delay is needed here, possibly because the header is being rerendered
/// click doesn't do anything otherwise
await session.delay(1000);
@ -61,7 +67,14 @@ async function findTabs(session) {
return { securityTabButton };
}
async function checkRoomSettings(session, expectedSettings) {
interface Settings {
encryption: boolean;
directory?: boolean;
alias?: string;
visibility?: string;
}
export async function checkRoomSettings(session: ElementSession, expectedSettings: Settings): Promise<void> {
session.log.startGroup(`checks the room settings`);
const { securityTabButton } = await findTabs(session);
@ -77,7 +90,7 @@ async function checkRoomSettings(session, expectedSettings) {
session.log.step(`checks for local alias of ${expectedSettings.alias}`);
const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
await summary.click();
const localAliases = await session.query('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
const localAliases = await session.queryAll('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a)));
if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) {
session.log.done("present");
@ -86,7 +99,7 @@ async function checkRoomSettings(session, expectedSettings) {
}
}
securityTabButton.click();
await securityTabButton.click();
await session.delay(500);
const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const e2eEncryptionToggle = securitySwitches[0];
@ -123,7 +136,7 @@ async function checkRoomSettings(session, expectedSettings) {
session.log.endGroup();
}
async function changeRoomSettings(session, settings) {
export async function changeRoomSettings(session, settings) {
session.log.startGroup(`changes the room settings`);
const { securityTabButton } = await findTabs(session);
@ -180,5 +193,3 @@ async function changeRoomSettings(session, settings) {
session.log.endGroup();
}
module.exports = { checkRoomSettings, changeRoomSettings };

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const { acceptToast } = require("./toasts");
import { acceptToast } from "./toasts";
import { ElementSession } from "../session";
async function setupSecureBackup(session) {
export async function setupSecureBackup(session: ElementSession): Promise<void> {
session.log.step("sets up Secure Backup");
await acceptToast(session, "Set up Secure Backup");

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
module.exports = async function sendMessage(session, message) {
export async function sendMessage(session: ElementSession, message: string): Promise<void> {
session.log.step(`writes "${message}" in room`);
// this selector needs to be the element that has contenteditable=true,
// not any if its parents, otherwise it behaves flaky at best.
@ -31,4 +32,4 @@ module.exports = async function sendMessage(session, message) {
// wait for the message to appear sent
await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)");
session.log.done();
};
}

View file

@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function openSettings(session, section) {
export async function openSettings(session: ElementSession, section: string): Promise<void> {
const menuButton = await session.query(".mx_UserMenu");
await menuButton.click();
const settingsItem = await session.query(".mx_UserMenu_iconSettings");
@ -29,7 +30,7 @@ async function openSettings(session, section) {
}
}
module.exports.enableLazyLoading = async function(session) {
export async function enableLazyLoading(session: ElementSession): Promise<void> {
session.log.step(`enables lazy loading of members in the lab settings`);
const settingsButton = await session.query('.mx_BottomLeftMenu_settings');
await settingsButton.click();
@ -39,17 +40,22 @@ module.exports.enableLazyLoading = async function(session) {
const closeButton = await session.query(".mx_RoomHeader_cancelButton");
await closeButton.click();
session.log.done();
};
}
module.exports.getE2EDeviceFromSettings = async function(session) {
interface E2EDevice {
id: string;
key: string;
}
export async function getE2EDeviceFromSettings(session: ElementSession): Promise<E2EDevice> {
session.log.step(`gets e2e device/key from settings`);
await openSettings(session, "security");
const deviceAndKey = await session.queryAll(".mx_SettingsTab_section .mx_CryptographyPanel code");
assert.equal(deviceAndKey.length, 2);
const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue();
const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue();
const id: string = await (await deviceAndKey[0].getProperty("innerText")).jsonValue();
const key: string = await (await deviceAndKey[1].getProperty("innerText")).jsonValue();
const closeButton = await session.query(".mx_UserSettingsDialog .mx_Dialog_cancelButton");
await closeButton.click();
session.log.done();
return { id, key };
};
}

View file

@ -15,9 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
module.exports = async function signup(session, username, password, homeserver) {
export async function signup(session: ElementSession, username: string, password: string,
homeserver: string): Promise<void> {
session.log.step("signs up");
await session.goto(session.url('/#/register'));
// change the homeserver by clicking the advanced section
@ -79,4 +81,4 @@ module.exports = async function signup(session, username, password, homeserver)
});
assert(foundHomeUrl);
session.log.done();
};
}

View file

@ -15,9 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
import { ElementHandle } from "puppeteer";
module.exports.scrollToTimelineTop = async function(session) {
export async function scrollToTimelineTop(session: ElementSession): Promise<void> {
session.log.step(`scrolls to the top of the timeline`);
await session.page.evaluate(() => {
return Promise.resolve().then(async () => {
@ -41,14 +43,21 @@ module.exports.scrollToTimelineTop = async function(session) {
});
});
session.log.done();
};
}
module.exports.receiveMessage = async function(session, expectedMessage) {
interface Message {
sender: string;
encrypted?: boolean;
body: string;
continuation?: boolean;
}
export async function receiveMessage(session: ElementSession, expectedMessage: Message): Promise<void> {
session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`);
// wait for a response to come in that contains the message
// crude, but effective
async function getLastMessage() {
async function getLastMessage(): Promise<Message> {
const lastTile = await getLastEventTile(session);
return getMessageFromEventTile(lastTile);
}
@ -67,25 +76,26 @@ module.exports.receiveMessage = async function(session, expectedMessage) {
});
assertMessage(lastMessage, expectedMessage);
session.log.done();
};
}
module.exports.checkTimelineContains = async function(session, expectedMessages, sendersDescription) {
export async function checkTimelineContains(session: ElementSession, expectedMessages: Message[],
sendersDescription: string): Promise<void> {
session.log.step(`checks timeline contains ${expectedMessages.length} ` +
`given messages${sendersDescription ? ` from ${sendersDescription}`:""}`);
const eventTiles = await getAllEventTiles(session);
let timelineMessages = await Promise.all(eventTiles.map((eventTile) => {
let timelineMessages: Message[] = await Promise.all(eventTiles.map((eventTile) => {
return getMessageFromEventTile(eventTile);
}));
//filter out tiles that were not messages
timelineMessages = timelineMessages.filter((m) => !!m);
timelineMessages.reduce((prevSender, m) => {
timelineMessages.reduce((prevSender: string, m) => {
if (m.continuation) {
m.sender = prevSender;
return prevSender;
} else {
return m.sender;
}
});
}, "");
expectedMessages.forEach((expectedMessage) => {
const foundMessage = timelineMessages.find((message) => {
@ -101,9 +111,9 @@ module.exports.checkTimelineContains = async function(session, expectedMessages,
});
session.log.done();
};
}
function assertMessage(foundMessage, expectedMessage) {
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
assert.equal(foundMessage.body, expectedMessage.body);
assert.equal(foundMessage.sender, expectedMessage.sender);
@ -112,17 +122,17 @@ function assertMessage(foundMessage, expectedMessage) {
}
}
function getLastEventTile(session) {
function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
return session.query(".mx_EventTile_last");
}
function getAllEventTiles(session) {
function getAllEventTiles(session: ElementSession): Promise<ElementHandle[]> {
return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
}
async function getMessageFromEventTile(eventTile) {
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
const senderElement = await eventTile.$(".mx_SenderProfile_displayName");
const className = await (await eventTile.getProperty("className")).jsonValue();
const className: string = await (await eventTile.getProperty("className")).jsonValue();
const classNames = className.split(" ");
const bodyElement = await eventTile.$(".mx_EventTile_body");
let sender = null;
@ -132,7 +142,7 @@ async function getMessageFromEventTile(eventTile) {
if (!bodyElement) {
return null;
}
const body = await(await bodyElement.getProperty("innerText")).jsonValue();
const body: string = await(await bodyElement.getProperty("innerText")).jsonValue();
return {
sender,

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { ElementSession } from "../session";
async function assertNoToasts(session) {
export async function assertNoToasts(session: ElementSession): Promise<void> {
try {
await session.query('.mx_Toast_toast', 1000, true);
} catch (e) {
@ -26,22 +27,20 @@ async function assertNoToasts(session) {
}
}
async function assertToast(session, expectedTitle) {
export async function assertToast(session: ElementSession, expectedTitle: string): Promise<void> {
const h2Element = await session.query('.mx_Toast_title h2');
const toastTitle = await session.innerText(h2Element);
assert.equal(toastTitle, expectedTitle);
}
async function acceptToast(session, expectedTitle) {
export async function acceptToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary');
await btn.click();
}
async function rejectToast(session, expectedTitle) {
export async function rejectToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline');
await btn.click();
}
module.exports = { assertNoToasts, assertToast, acceptToast, rejectToast };

View file

@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
import { strict as assert } from 'assert';
import { openMemberInfo } from "./memberlist";
import { ElementSession } from "../session";
const { openMemberInfo } = require("./memberlist");
async function startVerification(session, name) {
export async function startVerification(session: ElementSession, name: string): Promise<void> {
session.log.step("opens their opponent's profile and starts verification");
await openMemberInfo(session, name);
// click verify in member info
@ -30,22 +30,22 @@ async function startVerification(session, name) {
await session.delay(1000);
// click 'start verification'
const startVerifyButton = await session.query('.mx_UserInfo_container .mx_AccessibleButton_kind_primary');
const startVerifyButton = await session.query('.mx_UserInfo_container .mx_UserInfo_startVerification');
await startVerifyButton.click();
session.log.done();
}
async function getSasCodes(session) {
async function getSasCodes(session: ElementSession): Promise<string[]> {
const sasLabelElements = await session.queryAll(
".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label");
const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e)));
return sasLabels;
}
async function doSasVerification(session) {
async function doSasVerification(session: ElementSession): Promise<string[]> {
session.log.step("hunts for the emoji to yell at their opponent");
const sasCodes = await getSasCodes(session);
session.log.done(sasCodes);
session.log.done(sasCodes.join("\n"));
// Assume they match
session.log.step("assumes the emoji match");
@ -75,7 +75,7 @@ async function doSasVerification(session) {
return sasCodes;
}
module.exports.startSasVerification = async function(session, name) {
export async function startSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("starts verification");
await startVerification(session, name);
@ -85,9 +85,9 @@ module.exports.startSasVerification = async function(session, name) {
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
};
}
module.exports.acceptSasVerification = async function(session, name) {
export async function acceptSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("accepts verification");
const requestToast = await session.query('.mx_Toast_icon_verification');
@ -111,4 +111,4 @@ module.exports.acceptSasVerification = async function(session, name) {
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
};
}

View file

@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
module.exports.range = function(start, amount, step = 1) {
import { ElementSession } from "./session";
export const range = function(start: number, amount: number, step = 1): Array<number> {
const r = [];
for (let i = 0; i < amount; ++i) {
r.push(start + (i * step));
@ -23,17 +25,17 @@ module.exports.range = function(start, amount, step = 1) {
return r;
};
module.exports.delay = function(ms) {
export const delay = function(ms): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
};
module.exports.measureStart = function(session, name) {
export const measureStart = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.start(_name);
}, name);
};
module.exports.measureStop = function(session, name) {
export const measureStop = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.stop(_name);
}, name);

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const fs = require("fs");
const program = require('commander');
const ElementSession = require('./src/session');
const scenario = require('./src/scenario');
const RestSessionCreator = require('./src/rest/creator');
import { ElementSession } from './src/session';
import { scenario } from './src/scenario';
import { RestSessionCreator } from './src/rest/creator';
import * as fs from "fs";
import program = require('commander');
program
.option('--no-logs', "don't output logs, document html on error", false)
.option('--app-url [url]', "url to test", "http://localhost:5000")
@ -48,11 +47,11 @@ async function runTests() {
if (process.env.CHROME_PATH) {
const path = process.env.CHROME_PATH;
console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`);
options.executablePath = path;
options['executablePath'] = path;
}
const restCreator = new RestSessionCreator(
'synapse/installations/consent/env/bin',
'../synapse/installations/consent/env/bin',
hsUrl,
__dirname,
);
@ -85,7 +84,7 @@ async function runTests() {
await Promise.all(sessions.map(async (session) => {
// Collecting all performance monitoring data before closing the session
const measurements = await session.page.evaluate(() => {
let measurements = [];
let measurements;
window.mxPerformanceMonitor.addPerformanceDataCallback({
entryNames: [
window.mxPerformanceEntryNames.REGISTER,
@ -107,7 +106,9 @@ async function runTests() {
performanceEntries = JSON.parse(measurements);
return session.close();
}));
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
if (performanceEntries) {
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
}
if (failure) {
process.exit(-1);
} else {

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"outDir": "./lib",
"declaration": true,
"lib": [
"es2019",
"dom",
"dom.iterable"
],
},
"include": [
"./src/**/*.ts",
"start.ts"
]
}

View file

@ -7,6 +7,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.1.tgz#d90123f6c61fdf2f7cddd286ddae891586dd3488"
integrity sha512-sKDlqv6COJrR7ar0+GqqhrXQDzQlMcqMnF2iEU6m9hLo8kxozoAGUazwPyELHlRVmjsbvlnGXjnzyptSXVmceA==
"@types/puppeteer@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0"
integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q==
dependencies:
"@types/node" "*"
"@types/yauzl@^2.9.1":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"

3
test/globalSetup.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = 'UTC';
};

230
test/linkify-matrix-test.ts Normal file
View file

@ -0,0 +1,230 @@
/*
Copyright 2021 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 { linkify } from '../src/linkify-matrix';
describe('linkify-matrix', () => {
describe('roomalias', () => {
it('properly parses #_foonetic_xkcd:matrix.org', () => {
const test = '#_foonetic_xkcd:matrix.org';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#_foonetic_xkcd:matrix.org",
type: "roomalias",
value: "#_foonetic_xkcd:matrix.org",
}]));
});
it('properly parses #foo:localhost', () => {
const test = "#foo:localhost";
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:localhost",
type: "roomalias",
value: "#foo:localhost",
}]));
});
it('accept #foo:bar.com', () => {
const test = '#foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
}]));
});
it('accept #foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '#foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:com",
type: "roomalias",
value: "#foo:com",
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '#foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.org.uk",
type: "roomalias",
value: "#foo:bar.org.uk",
}]));
});
it('ignores trailing `:`', () => {
const test = '#foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '#foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com:2225",
type: "roomalias",
value: "#foo:bar.com:2225",
}]));
});
it('ignores all the trailing :', () => {
const test = '#foo:bar.com::::';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
}]));
});
it('properly parses room alias with dots in name', () => {
const test = '#foo.asdf:bar.com::::';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo.asdf:bar.com",
type: "roomalias",
value: "#foo.asdf:bar.com",
}]));
});
it('does not parse room alias with too many separators', () => {
const test = '#foo:::bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "http://bar.com",
type: "url",
value: "bar.com",
}]));
});
it('does not parse multiple room aliases in one string', () => {
const test = '#foo:bar.com-baz.com';
const found = linkify.find(test);
expect(found).toEqual(([{
"href": "#foo:bar.com-baz.com",
"type": "roomalias",
"value": "#foo:bar.com-baz.com",
}]));
});
});
describe('groupid', () => {
it('properly parses +foo:localhost', () => {
const test = "+foo:localhost";
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:localhost",
type: "groupid",
value: "+foo:localhost",
}]));
});
it('accept +foo:bar.com', () => {
const test = '+foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.com",
type: "groupid",
value: "+foo:bar.com",
}]));
});
it('accept +foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '+foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:com",
type: "groupid",
value: "+foo:com",
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '+foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.org.uk",
type: "groupid",
value: "+foo:bar.org.uk",
}]));
});
it('ignore trailing `:`', () => {
const test = '+foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
"href": "+foo:bar.com",
"type": "groupid",
"value": "+foo:bar.com",
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '+foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.com:2225",
type: "groupid",
value: "+foo:bar.com:2225",
}]));
});
});
describe('userid', () => {
it('should not parse @foo without domain', () => {
const test = "@foo";
const found = linkify.find(test);
expect(found).toEqual(([]));
});
it('accept @foo:bar.com', () => {
const test = '@foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com",
type: "userid",
value: "@foo:bar.com",
}]));
});
it('accept @foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '@foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:com",
type: "userid",
value: "@foo:com",
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '@foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.org.uk",
type: "userid",
value: "@foo:bar.org.uk",
}]));
});
it('do not accept trailing `:`', () => {
const test = '@foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com",
type: "userid",
value: "@foo:bar.com",
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '@foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com:2225",
type: "userid",
value: "@foo:bar.com:2225",
}]));
});
});
});

View file

@ -0,0 +1,198 @@
/*
Copyright 2021 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 SettingsStore from '../../../src/settings/SettingsStore';
import ThemeWatcher from '../../../src/settings/watchers/ThemeWatcher';
import { SettingLevel } from '../../../src/settings/SettingLevel';
function makeMatchMedia(values: any) {
class FakeMediaQueryList {
matches: false;
media: null;
onchange: null;
addListener() {}
removeListener() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() { return true; }
constructor(query: string) {
this.matches = values[query];
}
}
return function matchMedia(query: string) {
return new FakeMediaQueryList(query);
};
}
function makeGetValue(values: any) {
return function getValue<T = any>(
settingName: string,
_roomId: string = null,
_excludeDefault = false,
): T {
return values[settingName];
};
}
function makeGetValueAt(values: any) {
return function getValueAt(
_level: SettingLevel,
settingName: string,
_roomId: string = null,
_explicit = false,
_excludeDefault = false,
): any {
return values[settingName];
};
}
describe('ThemeWatcher', function() {
it('should choose a light theme by default', () => {
// Given no system settings
global.matchMedia = makeMatchMedia({});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it('should choose default theme if system settings are inconclusive', () => {
// Given no system settings but we asked to use them
global.matchMedia = makeMatchMedia({});
SettingsStore.getValue = makeGetValue({
"use_system_theme": true,
"theme": "light",
});
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it('should choose a dark theme if that is selected', () => {
// Given system says light high contrast but theme is set to dark
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ "theme": "dark" });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it('should choose a light theme if that is selected', () => {
// Given system settings say dark high contrast but theme set to light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ "theme": "light" });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it('should choose a light-high-contrast theme if that is selected', () => {
// Given system settings say dark and theme set to light-high-contrast
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ "theme": "light-high-contrast" });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it('should choose a light theme if system prefers it (via default)', () => {
// Given system prefers lightness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it('should choose a dark theme if system prefers it (via default)', () => {
// Given system prefers darkness, even though we did not
// click "Use system theme" or choose a theme explicitly
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({});
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it('should choose a light theme if system prefers it (explicit)', () => {
// Given system prefers lightness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: light)": true });
SettingsStore.getValueAt = makeGetValueAt({ "use_system_theme": true });
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns light
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light");
});
it('should choose a dark theme if system prefers it (explicit)', () => {
// Given system prefers darkness
global.matchMedia = makeMatchMedia({ "(prefers-color-scheme: dark)": true });
SettingsStore.getValueAt = makeGetValueAt({ "use_system_theme": true });
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
it('should choose a high-contrast theme if system prefers it', () => {
// Given system prefers high contrast and light
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: light)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ "use_system_theme": true });
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns light-high-contrast
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("light-high-contrast");
});
it('should not choose a high-contrast theme if not available', () => {
// Given system prefers high contrast and dark, but we don't (yet)
// have a high-contrast dark theme
global.matchMedia = makeMatchMedia({
"(prefers-contrast: more)": true,
"(prefers-color-scheme: dark)": true,
});
SettingsStore.getValueAt = makeGetValueAt({ "use_system_theme": true });
SettingsStore.getValue = makeGetValue({ "use_system_theme": true });
// Then getEffectiveTheme returns dark
const themeWatcher = new ThemeWatcher();
expect(themeWatcher.getEffectiveTheme()).toBe("dark");
});
});

View file

@ -16,3 +16,5 @@ global.TextDecoder = TextDecoder;
configure({ adapter: new Adapter() });
// maplibre requires a createObjectURL mock
global.URL.createObjectURL = jest.fn();

View file

@ -1,5 +1,8 @@
import RoomViewStore from '../../src/stores/RoomViewStore';
import { Action } from '../../src/dispatcher/actions';
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
import * as testUtils from '../test-utils';
const dispatch = testUtils.getDispatchForStore(RoomViewStore);
@ -19,7 +22,7 @@ describe('RoomViewStore', function() {
done();
};
dispatch({ action: 'view_room', room_id: '!randomcharacters:aser.ver' });
dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
dispatch({ action: 'join_room' });
expect(RoomViewStore.isJoining()).toBe(true);
});
@ -41,6 +44,6 @@ describe('RoomViewStore', function() {
done();
};
dispatch({ action: 'view_room', room_alias: '#somealias2:aser.ver' });
dispatch({ action: Action.ViewRoom, room_alias: '#somealias2:aser.ver' });
});
});

View file

@ -18,13 +18,16 @@ import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import "./enable-metaspaces-labs";
import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, {
import SpaceStore from "../../src/stores/spaces/SpaceStore";
import {
MetaSpace,
UPDATE_HOME_BEHAVIOUR,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../src/stores/SpaceStore";
} from "../../src/stores/spaces";
import * as testUtils from "../utils/test-utils";
import { mkEvent, stubClient } from "../test-utils";
import DMRoomMap from "../../src/utils/DMRoomMap";
@ -90,10 +93,18 @@ describe("SpaceStore", () => {
await emitProm;
};
beforeEach(() => {
beforeEach(async () => {
jest.runAllTimers(); // run async dispatch
client.getVisibleRooms.mockReturnValue(rooms = []);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: true,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: true,
[MetaSpace.Orphans]: true,
});
});
afterEach(async () => {
await testUtils.resetAsyncStoreWithClient(store);
});
@ -377,69 +388,84 @@ describe("SpaceStore", () => {
});
it("home space contains orphaned rooms", () => {
expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(orphan2)).toBeTruthy();
});
it("home space contains favourites", () => {
expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy();
it("home space does not contain all favourites", () => {
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(fav3)).toBeFalsy();
});
it("home space contains dm rooms", () => {
expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(dm3)).toBeTruthy();
});
it("home space contains invites", () => {
expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
});
it("home space contains invites even if they are also shown in a space", () => {
expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite2)).toBeTruthy();
});
it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => {
await setShowAllRooms(true);
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeTruthy();
});
it("favourites space does contain favourites even if they are also shown in a space", async () => {
expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Favourites).has(fav3)).toBeTruthy();
});
it("people space does contain people even if they are also shown in a space", async () => {
expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.People).has(dm3)).toBeTruthy();
});
it("orphans space does contain orphans even if they are also shown in all rooms", async () => {
await setShowAllRooms(true);
expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Orphans).has(orphan2)).toBeTruthy();
});
it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => {
await setShowAllRooms(false);
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(room1)).toBeFalsy();
});
it("space contains child rooms", () => {
const space = client.getRoom(space1);
expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space1).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space1).has(room1)).toBeTruthy();
});
it("space contains child favourites", () => {
const space = client.getRoom(space2);
expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(fav1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(fav2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(fav3)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(room1)).toBeTruthy();
});
it("space contains child invites", () => {
const space = client.getRoom(space3);
expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space3).has(invite2)).toBeTruthy();
});
it("spaces contain dms which you have with members of that space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space1).has(dm1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space3).has(dm1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space1).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space2).has(dm2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space3).has(dm2)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space1).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space2).has(dm3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space3).has(dm3)).toBeFalsy();
});
it("dms are only added to Notification States for only the Home Space", () => {
@ -491,11 +517,11 @@ describe("SpaceStore", () => {
});
it("honours m.space.parent if sender has permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(room2)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space2).has(room2)).toBeTruthy();
});
it("does not honour m.space.parent if sender does not have permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(room3)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space3).has(room3)).toBeFalsy();
});
});
});
@ -586,8 +612,8 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([]);
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeFalsy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeFalsy();
const invite = mkRoom(invite1);
invite.getMyMembership.mockReturnValue("invite");
@ -599,8 +625,8 @@ describe("SpaceStore", () => {
expect(store.invitedSpaces).toStrictEqual([]);
expect(store.getChildSpaces(space1)).toStrictEqual([]);
expect(store.getChildRooms(space1)).toStrictEqual([invite]);
expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(space1).has(invite1)).toBeTruthy();
expect(store.getSpaceFilteredRoomIds(MetaSpace.Home).has(invite1)).toBeTruthy();
});
});
@ -614,49 +640,46 @@ describe("SpaceStore", () => {
]);
mkSpace(space3).getMyMembership.mockReturnValue("invite");
await run();
store.setActiveSpace(null);
expect(store.activeSpace).toBe(null);
store.setActiveSpace(MetaSpace.Home);
expect(store.activeSpace).toBe(MetaSpace.Home);
});
afterEach(() => {
fn.mockClear();
});
it("switch to home space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
fn.mockClear();
store.setActiveSpace(null);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null);
expect(store.activeSpace).toBe(null);
store.setActiveSpace(MetaSpace.Home);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, MetaSpace.Home);
expect(store.activeSpace).toBe(MetaSpace.Home);
});
it("switch to invited space", async () => {
const space = client.getRoom(space3);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
store.setActiveSpace(space3);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space3);
expect(store.activeSpace).toBe(space3);
});
it("switch to top level space", async () => {
const space = client.getRoom(space1);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
store.setActiveSpace(space1);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space1);
expect(store.activeSpace).toBe(space1);
});
it("switch to subspace", async () => {
const space = client.getRoom(space2);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
store.setActiveSpace(space2);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space2);
expect(store.activeSpace).toBe(space2);
});
it("switch to unknown space is a nop", async () => {
expect(store.activeSpace).toBe(null);
expect(store.activeSpace).toBe(MetaSpace.Home);
const space = client.getRoom(room1); // not a space
store.setActiveSpace(space);
expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(null);
store.setActiveSpace(space.roomId);
expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space.roomId);
expect(store.activeSpace).toBe(MetaSpace.Home);
});
});
@ -678,6 +701,7 @@ describe("SpaceStore", () => {
});
afterEach(() => {
localStorage.clear();
localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true");
defaultDispatcher.unregister(dispatcherRef);
});
@ -687,59 +711,59 @@ describe("SpaceStore", () => {
};
it("last viewed room in target space is the current viewed and in both spaces", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room2);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
viewRoom(room2);
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is in the current space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room2);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is not in the current space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room1);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
viewRoom(room2);
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
expect(getCurrentRoom()).toBe(room1);
});
it("last viewed room is target space is not known", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, orphan2);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
});
it("last viewed room is target space is no longer in that space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, room1);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2); // Space home instead of room1
});
it("no last viewed room in target space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room1);
store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(space2);
expect(getCurrentRoom()).toBe(space2);
});
it("no last viewed room in home space", async () => {
store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(space1);
viewRoom(room1);
store.setActiveSpace(null);
store.setActiveSpace(MetaSpace.Home);
expect(getCurrentRoom()).toBeNull(); // Home
});
});
@ -767,38 +791,51 @@ describe("SpaceStore", () => {
it("no switch required, room is in current space", async () => {
viewRoom(room1);
store.setActiveSpace(client.getRoom(space1), false);
store.setActiveSpace(space1, false);
viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space1));
expect(store.activeSpace).toBe(space1);
});
it("switch to canonical parent space for room", async () => {
viewRoom(room1);
store.setActiveSpace(client.getRoom(space2), false);
store.setActiveSpace(space2, false);
viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space2));
expect(store.activeSpace).toBe(space2);
});
it("switch to first containing space for room", async () => {
viewRoom(room2);
store.setActiveSpace(client.getRoom(space2), false);
store.setActiveSpace(space2, false);
viewRoom(room3);
expect(store.activeSpace).toBe(client.getRoom(space1));
expect(store.activeSpace).toBe(space1);
});
it("switch to home for orphaned room", async () => {
it("switch to other rooms for orphaned room", async () => {
viewRoom(room1);
store.setActiveSpace(client.getRoom(space1), false);
store.setActiveSpace(space1, false);
viewRoom(orphan1);
expect(store.activeSpace).toBeNull();
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});
it("switch to first space when selected metaspace is disabled", async () => {
store.setActiveSpace(MetaSpace.People, false);
expect(store.activeSpace).toBe(MetaSpace.People);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: false,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: false,
[MetaSpace.Orphans]: true,
});
jest.runAllTimers();
expect(store.activeSpace).toBe(MetaSpace.Favourites);
});
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true);
viewRoom(room2);
store.setActiveSpace(null, false);
store.setActiveSpace(MetaSpace.Home, false);
viewRoom(room1);
expect(store.activeSpace).toBeNull();
expect(store.activeSpace).toBe(MetaSpace.Home);
});
});

View file

@ -0,0 +1,125 @@
/*
Copyright 2021 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 "../skinned-sdk"; // Must be first for skinning to work
import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
import { Room } from "matrix-js-sdk";
import { stubClient } from "../test-utils";
// setup test env values
const roomId = "!room:server";
const mockRoom = <Room>{
roomId: roomId,
currentState: {
getStateEvents: (_l, _x) => {
return {
getId: ()=>"$layoutEventId",
getContent: () => null,
};
},
} };
const mockApps = [
<IApp> { roomId: roomId, id: "1" },
<IApp> { roomId: roomId, id: "2" },
<IApp> { roomId: roomId, id: "3" },
<IApp> { roomId: roomId, id: "4" },
];
// fake the WidgetStore.instance to just return an object with `getApps`
jest.spyOn(WidgetStore, 'instance', 'get').mockReturnValue(<WidgetStore>{ getApps: (_room) => mockApps });
describe("WidgetLayoutStore", () => {
// we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout"))
stubClient();
const store = WidgetLayoutStore.instance;
it("all widgets should be in the right container by default", async () => {
store.recalculateRoom(mockRoom);
expect(store.getContainerWidgets(mockRoom, Container.Right).length).toStrictEqual(mockApps.length);
});
it("add widget to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]);
});
it("add three widgets to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
expect(new Set(store.getContainerWidgets(mockRoom, Container.Top)))
.toEqual(new Set([mockApps[0], mockApps[1], mockApps[2]]));
});
it("cannot add more than three widgets to top container", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
expect(store.canAddToContainer(mockRoom, Container.Top))
.toEqual(false);
});
it("remove pins when maximising (other widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
store.moveToContainer(mockRoom, mockApps[3], Container.Center);
expect(store.getContainerWidgets(mockRoom, Container.Top))
.toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
.toEqual(new Set([mockApps[0], mockApps[1], mockApps[2]]));
expect(store.getContainerWidgets(mockRoom, Container.Center))
.toEqual([mockApps[3]]);
});
it("remove pins when maximising (one of the pinned widgets)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
expect(store.getContainerWidgets(mockRoom, Container.Top))
.toEqual([]);
expect(store.getContainerWidgets(mockRoom, Container.Center))
.toEqual([mockApps[0]]);
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
.toEqual(new Set([mockApps[1], mockApps[2], mockApps[3]]));
});
it("remove maximised when pinning (other widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
expect(store.getContainerWidgets(mockRoom, Container.Top))
.toEqual([mockApps[1]]);
expect(store.getContainerWidgets(mockRoom, Container.Center))
.toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
.toEqual(new Set([mockApps[2], mockApps[3], mockApps[0]]));
});
it("remove maximised when pinning (same widget)", async () => {
store.recalculateRoom(mockRoom);
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
expect(store.getContainerWidgets(mockRoom, Container.Top))
.toEqual([mockApps[0]]);
expect(store.getContainerWidgets(mockRoom, Container.Center))
.toEqual([]);
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right)))
.toEqual(new Set([mockApps[2], mockApps[3], mockApps[1]]));
});
});

View file

@ -0,0 +1,17 @@
/*
Copyright 2021 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.
*/
localStorage.setItem("mx_labs_feature_feature_spaces_metaspaces", "true");

View file

@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../enable-metaspaces-labs";
import "../../skinned-sdk"; // Must be first for skinning to work
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
import SettingsStore from "../../../src/settings/SettingsStore";
import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore";
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
import { MetaSpace, UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/spaces";
import { stubClient } from "../../test-utils";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import * as testUtils from "../../utils/test-utils";
import { setupAsyncStoreWithClient } from "../../utils/test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import * as testUtils from "../../utils/test-utils";
import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition";
import DMRoomMap from "../../../src/utils/DMRoomMap";
let filter: SpaceFilterCondition = null;
@ -33,8 +36,13 @@ const mockRoomListStore = {
removeFilter: () => filter = null,
} as unknown as RoomListStoreClass;
const space1Id = "!space1:server";
const space2Id = "!space2:server";
const getUserIdForRoomId = jest.fn();
const getDMRoomsForUserId = jest.fn();
// @ts-ignore
DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
const space1 = "!space1:server";
const space2 = "!space2:server";
describe("SpaceWatcher", () => {
stubClient();
@ -50,17 +58,21 @@ describe("SpaceWatcher", () => {
await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR);
};
let space1;
let space2;
beforeEach(async () => {
filter = null;
store.removeAllListeners();
store.setActiveSpace(null);
store.setActiveSpace(MetaSpace.Home);
client.getVisibleRooms.mockReturnValue(rooms = []);
space1 = mkSpace(space1Id);
space2 = mkSpace(space2Id);
mkSpace(space1);
mkSpace(space2);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: true,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: true,
[MetaSpace.Orphans]: true,
});
client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
await setupAsyncStoreWithClient(store, client);
@ -80,14 +92,14 @@ describe("SpaceWatcher", () => {
expect(filter).toBeNull();
});
it("sets space=null filter for all -> home transition", async () => {
it("sets space=Home filter for all -> home transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
await setShowAllRooms(false);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBeNull();
expect(filter["space"]).toBe(MetaSpace.Home);
});
it("sets filter correctly for all -> space transition", async () => {
@ -126,7 +138,43 @@ describe("SpaceWatcher", () => {
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(null);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
it("removes filter for favourites -> all transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(MetaSpace.Favourites);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.Favourites);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
it("removes filter for people -> all transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(MetaSpace.People);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.People);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
it("removes filter for orphans -> all transition", async () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.Orphans);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeNull();
});
@ -138,10 +186,36 @@ describe("SpaceWatcher", () => {
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(null);
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(null);
expect(filter["space"]).toBe(MetaSpace.Home);
});
it("updates filter correctly for space -> orphans transition", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.Orphans);
});
it("updates filter correctly for orphans -> people transition", async () => {
await setShowAllRooms(false);
SpaceStore.instance.setActiveSpace(MetaSpace.Orphans);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.Orphans);
SpaceStore.instance.setActiveSpace(MetaSpace.People);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(MetaSpace.People);
});
it("updates filter correctly for space -> space transition", async () => {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { formatSeconds } from "../../src/DateUtils";
import { formatSeconds, formatRelativeTime } from "../../src/DateUtils";
describe("formatSeconds", () => {
it("correctly formats time with hours", () => {
@ -29,3 +29,44 @@ describe("formatSeconds", () => {
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00");
});
});
describe("formatRelativeTime", () => {
let dateSpy;
beforeAll(() => {
dateSpy = jest
.spyOn(global.Date, 'now')
// Tuesday, 2 November 2021 11:18:03 UTC
.mockImplementation(() => 1635851883000);
});
afterAll(() => {
dateSpy.mockRestore();
});
it("returns hour format for events created less than 24 hours ago", () => {
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
expect(formatRelativeTime(date)).toBe("11:01");
});
it("honours the hour format setting", () => {
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
expect(formatRelativeTime(date)).toBe("11:01");
expect(formatRelativeTime(date, false)).toBe("11:01");
expect(formatRelativeTime(date, true)).toBe("11:01AM");
});
it("returns month and day for events created in the current year", () => {
const date = new Date(1632567741000);
expect(formatRelativeTime(date, true)).toBe("Sep 25");
});
it("does not return a leading 0 for single digit days", () => {
const date = new Date(1635764541000);
expect(formatRelativeTime(date, true)).toBe("Nov 1");
});
it("appends the year for events created in previous years", () => {
const date = new Date(1604142141000);
expect(formatRelativeTime(date, true)).toBe("Oct 31, 2020");
});
});