1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-08-07 21:23:00 +03:00

Merge branch 'develop' into export-conversations

This commit is contained in:
Jaiwanth
2021-09-22 18:07:01 +05:30
498 changed files with 13790 additions and 23008 deletions

View File

@@ -136,18 +136,6 @@ describe("PosthogAnalytics", () => {
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
});
it("Should pass trackRoomEvent to posthog", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
const roomId = "42";
await analytics.trackRoomEvent<IRoomEvent>("jest_test_event", roomId, {
foo: "bar",
});
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"])
.toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049");
});
it("Should pass trackPseudonymousEvent() to posthog", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
@@ -173,9 +161,6 @@ describe("PosthogAnalytics", () => {
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
foo: "bar",
});
await analytics.trackRoomEvent<ITestRoomEvent>("room id", "foo", {
foo: "bar",
});
await analytics.trackPageView(200);
expect(fakePosthog.capture.mock.calls.length).toBe(0);
});
@@ -183,31 +168,25 @@ describe("PosthogAnalytics", () => {
it("Should pseudonymise a location of a known screen", async () => {
const location = await getRedactedCurrentLocation(
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
expect(location).toBe(
`https://foo.bar/#/register/\
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
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>/<redacted>");
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>/\
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
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);
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>/<redacted>");
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
});
it("Should handle an empty hash", async () => {
@@ -218,15 +197,28 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
it("Should identify the user to posthog if pseudonymous", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await analytics.identifyUser("foo");
expect(fakePosthog.identify.mock.calls[0][0])
.toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
class FakeClient {
getAccountDataFromServer = jest.fn().mockResolvedValue(null);
setAccountData = jest.fn().mockResolvedValue({});
}
await analytics.identifyUser(new FakeClient(), () => "analytics_id" );
expect(fakePosthog.identify.mock.calls[0][0]).toBe("analytics_id");
});
it("Should not identify the user to posthog if anonymous", async () => {
analytics.setAnonymity(Anonymity.Anonymous);
await analytics.identifyUser("foo");
await analytics.identifyUser(null);
expect(fakePosthog.identify.mock.calls.length).toBe(0);
});
it("Should identify using the server's analytics id if present", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
class FakeClient {
getAccountDataFromServer = jest.fn().mockResolvedValue({ id: "existing_analytics_id" });
setAccountData = jest.fn().mockResolvedValue({});
}
await analytics.identifyUser(new FakeClient(), () => "new_analytics_id" );
expect(fakePosthog.identify.mock.calls[0][0]).toBe("existing_analytics_id");
});
});
});

129
test/TextForEvent-test.ts Normal file
View File

@@ -0,0 +1,129 @@
import './skinned-sdk';
import { textForEvent } from "../src/TextForEvent";
import { MatrixEvent } from "matrix-js-sdk";
import SettingsStore from "../src/settings/SettingsStore";
import { SettingLevel } from "../src/settings/SettingLevel";
import renderer from 'react-test-renderer';
function mockPinnedEvent(
pinnedMessageIds?: string[],
prevPinnedMessageIds?: string[],
): MatrixEvent {
return new MatrixEvent({
type: "m.room.pinned_events",
state_key: "",
sender: "@foo:example.com",
content: {
pinned: pinnedMessageIds,
},
prev_content: {
pinned: prevPinnedMessageIds,
},
});
}
// Helper function that renders a component to a plain text string.
// Once snapshots are introduced in tests, this function will no longer be necessary,
// and should be replaced with snapshots.
function renderComponent(component): string {
const serializeObject = (object): string => {
if (typeof object === 'string') {
return object === ' ' ? '' : object;
}
if (Array.isArray(object) && object.length === 1 && typeof object[0] === 'string') {
return object[0];
}
if (object['type'] !== undefined && typeof object['children'] !== undefined) {
return serializeObject(object.children);
}
if (!Array.isArray(object)) {
return '';
}
return object.map(child => {
return serializeObject(child);
}).join('');
};
return serializeObject(component.toJSON());
}
describe('TextForEvent', () => {
describe("TextForPinnedEvent", () => {
SettingsStore.setValue("feature_pinning", null, SettingLevel.DEVICE, true);
it("mentions message when a single message was pinned, with no previously pinned messages", () => {
const event = mockPinnedEvent(['message-1']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("mentions message when a single message was pinned, with multiple previously pinned messages", () => {
const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1', 'message-2']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("mentions message when a single message was unpinned, with a single message previously pinned", () => {
const event = mockPinnedEvent([], ['message-1']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("mentions message when a single message was unpinned, with multiple previously pinned messages", () => {
const event = mockPinnedEvent(['message-2'], ['message-1', 'message-2']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("shows generic text when multiple messages were pinned", () => {
const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com changed the pinned messages for the room.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("shows generic text when multiple messages were unpinned", () => {
const event = mockPinnedEvent(['message-3'], ['message-1', 'message-2', 'message-3']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com changed the pinned messages for the room.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
it("shows generic text when one message was pinned, and another unpinned", () => {
const event = mockPinnedEvent(['message-2'], ['message-1']);
const plainText = textForEvent(event);
const component = renderer.create(textForEvent(event, true));
const expectedText = "@foo:example.com changed the pinned messages for the room.";
expect(plainText).toBe(expectedText);
expect(renderComponent(component)).toBe(expectedText);
});
});
});

View File

@@ -0,0 +1,209 @@
import "../../../skinned-sdk";
import * as testUtils from '../../../test-utils';
import ReplyThread from '../../../../src/components/views/elements/ReplyThread';
describe("ReplyThread", () => {
describe('getParentEventId', () => {
it('retrieves relation reply from unedited event', () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
},
},
},
user: "some_other_user",
room: "room_id",
});
expect(ReplyThread.getParentEventId(originalEventWithRelation))
.toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
});
it('retrieves relation reply from original event when edited', () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
},
},
},
user: "some_other_user",
room: "room_id",
});
const editEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n * foo bar",
"m.new_content": {
"msgtype": "m.text",
"body": "foo bar",
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": originalEventWithRelation.event_id,
},
},
user: "some_other_user",
room: "room_id",
});
// The edit replaces the original event
originalEventWithRelation.makeReplaced(editEvent);
// The relation should be pulled from the original event
expect(ReplyThread.getParentEventId(originalEventWithRelation))
.toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
});
it('retrieves relation reply from edit event when provided', () => {
const originalEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
msgtype: "m.text",
body: "foo",
},
user: "some_other_user",
room: "room_id",
});
const editEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n * foo bar",
"m.new_content": {
"msgtype": "m.text",
"body": "foo bar",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
},
},
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": originalEvent.event_id,
},
},
user: "some_other_user",
room: "room_id",
});
// The edit replaces the original event
originalEvent.makeReplaced(editEvent);
// The relation should be pulled from the edit event
expect(ReplyThread.getParentEventId(originalEvent))
.toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
});
it('prefers relation reply from edit event over original event', () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$111",
},
},
},
user: "some_other_user",
room: "room_id",
});
const editEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n * foo bar",
"m.new_content": {
"msgtype": "m.text",
"body": "foo bar",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$999",
},
},
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": originalEventWithRelation.event_id,
},
},
user: "some_other_user",
room: "room_id",
});
// The edit replaces the original event
originalEventWithRelation.makeReplaced(editEvent);
// The relation should be pulled from the edit event
expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual('$999');
});
it('able to clear relation reply from original event by providing empty relation field', () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$111",
},
},
},
user: "some_other_user",
room: "room_id",
});
const editEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n * foo bar",
"m.new_content": {
"msgtype": "m.text",
"body": "foo bar",
// Clear the relation from the original event
"m.relates_to": {},
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": originalEventWithRelation.event_id,
},
},
user: "some_other_user",
room: "room_id",
});
// The edit replaces the original event
originalEventWithRelation.makeReplaced(editEvent);
// The relation should be pulled from the edit event
expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual(undefined);
});
});
});

View File

@@ -9,23 +9,16 @@ import sdk from '../../../skinned-sdk';
import dis from '../../../../src/dispatcher/dispatcher';
import DMRoomMap from '../../../../src/utils/DMRoomMap';
import GroupStore from '../../../../src/stores/GroupStore';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain';
}
function waitForRoomListStoreUpdate() {
return new Promise((resolve) => {
RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve());
});
}
describe('RoomList', () => {
function createRoom(opts) {
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
@@ -239,73 +232,6 @@ describe('RoomList', () => {
});
}
describe('when no tags are selected', () => {
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
describe('when tags are selected', () => {
function setupSelectedTag() {
// Simulate a complete sync BEFORE dispatching anything else
dis.dispatch({
action: 'MatrixActions.sync',
prevState: null,
state: 'PREPARED',
matrixClient: client,
}, true);
// Simulate joined groups being received
dis.dispatch({
action: 'GroupActions.fetchJoinedGroups.success',
result: {
groups: ['+group:domain'],
},
}, true);
// Simulate receiving tag ordering account data
dis.dispatch({
action: 'MatrixActions.accountData',
event_type: 'im.vector.web.tag_ordering',
event_content: {
tags: ['+group:domain'],
},
}, true);
// GroupStore is not flux, mock and notify
GroupStore.getGroupRooms = (groupId) => {
return [movingRoom];
};
GroupStore._notifyListeners();
// We also have to mock the client's getGroup function for the room list to filter it.
// It's not smart enough to tell the difference between a real group and a template though.
client.getGroup = (groupId) => {
return { groupId };
};
// Select tag
dis.dispatch({ action: 'select_tag', tag: '+group:domain' }, true);
}
beforeEach(() => {
setupSelectedTag();
});
it('displays the correct rooms when the groups rooms are changed', async () => {
GroupStore.getGroupRooms = (groupId) => {
return [movingRoom, otherRoom];
};
GroupStore._notifyListeners();
await waitForRoomListStoreUpdate();
// XXX: Even though the store updated, it can take a bit before the update makes
// it to the components. This gives it plenty of time to figure out what to do.
await (new Promise(resolve => setTimeout(resolve, 500)));
expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
});
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});

View File

@@ -46,7 +46,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("hello world", "insertText", { offset: 11, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator);
const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({
body: "hello world",
@@ -58,7 +58,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator);
const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({
body: "hello *world*",
@@ -72,7 +72,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator);
const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({
body: "blinks __quickly__",
@@ -86,7 +86,7 @@ describe('<SendMessageComposer/>', () => {
const model = new EditorModel([], createPartCreator(), createRenderer());
model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true });
const content = createMessageContent(model, permalinkCreator);
const content = createMessageContent(model, null, false, permalinkCreator);
expect(content).toEqual({
body: "/dev/null is my favourite place",

View File

@@ -43,6 +43,9 @@ module.exports = async function scenario(createSession, restCreator) {
console.log("create REST users:");
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);
};
async function createRestUsers(restCreator) {

View File

@@ -0,0 +1,32 @@
/*
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.
*/
const { createSpace, inviteSpace } = require("../usecases/create-space");
module.exports = async function spacesScenarios(alice, bob) {
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) {
await createSpace(alice, space);
await inviteSpace(alice, space, "@bob:localhost");
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received
}

View File

@@ -0,0 +1,80 @@
/*
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.
*/
async function openSpaceCreateMenu(session) {
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
await spaceCreateButton.click();
}
async function createSpace(session, name, isPublic = false) {
session.log.step(`creates space "${name}"`);
await openSpaceCreateMenu(session);
const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private";
const visibilityButton = await session.query(className);
await visibilityButton.click();
const nameInput = await session.query('input[name="spaceName"]');
await session.replaceInputText(nameInput, name);
await session.delay(100);
const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary');
await createButton.click();
if (!isPublic) {
const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton');
await justMeButton.click();
const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary');
await continueButton.click();
} else {
for (let i = 0; i < 2; i++) {
const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary');
await continueButton.click();
}
}
session.log.done();
}
async function inviteSpace(session, spaceName, userId) {
session.log.step(`invites "${userId}" to space "${spaceName}"`);
const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
await spaceButton.click({
button: 'right',
});
const inviteButton = await session.query('[aria-label="Invite people"]');
await inviteButton.click();
try {
const button = await session.query('.mx_SpacePublicShare_inviteButton');
await button.click();
} catch (e) {
// ignore
}
const inviteTextArea = await session.query(".mx_InviteDialog_editor input");
await inviteTextArea.type(userId);
const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
await selectUserItem.click();
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
}
module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };

View File

@@ -161,8 +161,8 @@ async function changeRoomSettings(session, settings) {
if (settings.visibility) {
session.log.step(`sets visibility to ${settings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
assert.equal(radios.length, 6);
const [inviteOnlyRoom, publicRoom] = radios;
assert.equal(radios.length, 7);
const [inviteOnlyRoom,, publicRoom] = radios;
if (settings.visibility === "invite_only") {
await inviteOnlyRoom.click();

View File

@@ -1,20 +0,0 @@
/*
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.
*/
// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here.
// SpaceStore reads the SettingsStore which needs the localStorage values set at init time.
localStorage.setItem("mx_labs_feature_feature_spaces", "true");

View File

@@ -18,7 +18,6 @@ import { EventEmitter } from "events";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import "./SpaceStore-setup"; // enable space lab
import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, {
UPDATE_HOME_BEHAVIOUR,
@@ -276,10 +275,12 @@ describe("SpaceStore", () => {
describe("test fixture 1", () => {
beforeEach(async () => {
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom);
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3]
.forEach(mkRoom);
mkSpace(space1, [fav1, room1]);
mkSpace(space2, [fav1, fav2, fav3, room1]);
mkSpace(space3, [invite2]);
// client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));
[fav1, fav2, fav3].forEach(roomId => {
client.getRoom(roomId).tags = {
@@ -329,6 +330,48 @@ describe("SpaceStore", () => {
]);
// dmPartner3 is not in any common spaces with you
// room 2 claims to be a child of space2 and is so via a valid m.space.parent
const cliRoom2 = client.getRoom(room2);
cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room2,
user: client.getUserId(),
skey: space2,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace2 = client.getRoom(space2);
cliSpace2.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return userId === client.getUserId();
}
return true;
});
// room 3 claims to be a child of space3 but is not due to invalid m.space.parent (permissions)
const cliRoom3 = client.getRoom(room3);
cliRoom3.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room3,
user: client.getUserId(),
skey: space3,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace3 = client.getRoom(space3);
cliSpace3.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return false;
}
return true;
});
await run();
});
@@ -445,6 +488,14 @@ describe("SpaceStore", () => {
expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy();
expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy();
});
it("honours m.space.parent if sender has permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(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();
});
});
});
@@ -562,7 +613,7 @@ describe("SpaceStore", () => {
]);
mkSpace(space3).getMyMembership.mockReturnValue("invite");
await run();
await store.setActiveSpace(null);
store.setActiveSpace(null);
expect(store.activeSpace).toBe(null);
});
afterEach(() => {
@@ -570,31 +621,31 @@ describe("SpaceStore", () => {
});
it("switch to home space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
fn.mockClear();
await store.setActiveSpace(null);
store.setActiveSpace(null);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null);
expect(store.activeSpace).toBe(null);
});
it("switch to invited space", async () => {
const space = client.getRoom(space3);
await store.setActiveSpace(space);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
});
it("switch to top level space", async () => {
const space = client.getRoom(space1);
await store.setActiveSpace(space);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
});
it("switch to subspace", async () => {
const space = client.getRoom(space2);
await store.setActiveSpace(space);
store.setActiveSpace(space);
expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(space);
});
@@ -602,7 +653,7 @@ describe("SpaceStore", () => {
it("switch to unknown space is a nop", async () => {
expect(store.activeSpace).toBe(null);
const space = client.getRoom(room1); // not a space
await store.setActiveSpace(space);
store.setActiveSpace(space);
expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space);
expect(store.activeSpace).toBe(null);
});
@@ -635,59 +686,59 @@ describe("SpaceStore", () => {
};
it("last viewed room in target space is the current viewed and in both spaces", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room2);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
viewRoom(room2);
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is in the current space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room2);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
expect(getCurrentRoom()).toBe(space2);
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
expect(getCurrentRoom()).toBe(room2);
});
it("last viewed room in target space is not in the current space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
viewRoom(room2);
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
expect(getCurrentRoom()).toBe(room1);
});
it("last viewed room is target space is not known", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, orphan2);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
expect(getCurrentRoom()).toBe(space2);
});
it("last viewed room is target space is no longer in that space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
localStorage.setItem(`mx_space_context_${space2}`, room1);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
expect(getCurrentRoom()).toBe(space2); // Space home instead of room1
});
it("no last viewed room in target space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
await store.setActiveSpace(client.getRoom(space2));
store.setActiveSpace(client.getRoom(space2));
expect(getCurrentRoom()).toBe(space2);
});
it("no last viewed room in home space", async () => {
await store.setActiveSpace(client.getRoom(space1));
store.setActiveSpace(client.getRoom(space1));
viewRoom(room1);
await store.setActiveSpace(null);
store.setActiveSpace(null);
expect(getCurrentRoom()).toBeNull(); // Home
});
});
@@ -715,28 +766,28 @@ describe("SpaceStore", () => {
it("no switch required, room is in current space", async () => {
viewRoom(room1);
await store.setActiveSpace(client.getRoom(space1), false);
store.setActiveSpace(client.getRoom(space1), false);
viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space1));
});
it("switch to canonical parent space for room", async () => {
viewRoom(room1);
await store.setActiveSpace(client.getRoom(space2), false);
store.setActiveSpace(client.getRoom(space2), false);
viewRoom(room2);
expect(store.activeSpace).toBe(client.getRoom(space2));
});
it("switch to first containing space for room", async () => {
viewRoom(room2);
await store.setActiveSpace(client.getRoom(space2), false);
store.setActiveSpace(client.getRoom(space2), false);
viewRoom(room3);
expect(store.activeSpace).toBe(client.getRoom(space1));
});
it("switch to home for orphaned room", async () => {
viewRoom(room1);
await store.setActiveSpace(client.getRoom(space1), false);
store.setActiveSpace(client.getRoom(space1), false);
viewRoom(orphan1);
expect(store.activeSpace).toBeNull();
});
@@ -744,7 +795,7 @@ describe("SpaceStore", () => {
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true);
viewRoom(room2);
await store.setActiveSpace(null, false);
store.setActiveSpace(null, false);
viewRoom(room1);
expect(store.activeSpace).toBeNull();
});

View File

@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../SpaceStore-setup"; // enable space lab
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";
@@ -57,7 +56,7 @@ describe("SpaceWatcher", () => {
beforeEach(async () => {
filter = null;
store.removeAllListeners();
await store.setActiveSpace(null);
store.setActiveSpace(null);
client.getVisibleRooms.mockReturnValue(rooms = []);
space1 = mkSpace(space1Id);
@@ -95,7 +94,7 @@ describe("SpaceWatcher", () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
@@ -114,7 +113,7 @@ describe("SpaceWatcher", () => {
await setShowAllRooms(false);
new SpaceWatcher(mockRoomListStore);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
@@ -124,22 +123,22 @@ describe("SpaceWatcher", () => {
await setShowAllRooms(true);
new SpaceWatcher(mockRoomListStore);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
await SpaceStore.instance.setActiveSpace(null);
SpaceStore.instance.setActiveSpace(null);
expect(filter).toBeNull();
});
it("updates filter correctly for space -> home transition", async () => {
await setShowAllRooms(false);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
await SpaceStore.instance.setActiveSpace(null);
SpaceStore.instance.setActiveSpace(null);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(null);
@@ -147,12 +146,12 @@ describe("SpaceWatcher", () => {
it("updates filter correctly for space -> space transition", async () => {
await setShowAllRooms(false);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space1);
await SpaceStore.instance.setActiveSpace(space2);
SpaceStore.instance.setActiveSpace(space2);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
expect(filter["space"]).toBe(space2);
@@ -160,7 +159,7 @@ describe("SpaceWatcher", () => {
it("doesn't change filter when changing showAllRooms mode to true", async () => {
await setShowAllRooms(false);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);
@@ -173,7 +172,7 @@ describe("SpaceWatcher", () => {
it("doesn't change filter when changing showAllRooms mode to false", async () => {
await setShowAllRooms(true);
await SpaceStore.instance.setActiveSpace(space1);
SpaceStore.instance.setActiveSpace(space1);
new SpaceWatcher(mockRoomListStore);
expect(filter).toBeInstanceOf(SpaceFilterCondition);

View File

@@ -85,9 +85,8 @@ export function createTestClient() {
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: () => false,
isCryptoEnabled: () => false,
getSpaceSummary: jest.fn().mockReturnValue({
getRoomHierarchy: jest.fn().mockReturnValue({
rooms: [],
events: [],
}),
// Used by various internal bits we aren't concerned with (yet)

View File

@@ -0,0 +1,31 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 { formatSeconds } from "../../src/DateUtils";
describe("formatSeconds", () => {
it("correctly formats time with hours", () => {
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (55))).toBe("03:31:55");
expect(formatSeconds((60 * 60 * 3) + (60 * 0) + (55))).toBe("03:00:55");
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (0))).toBe("03:31:00");
});
it("correctly formats time without hours", () => {
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (55))).toBe("31:55");
expect(formatSeconds((60 * 60 * 0) + (60 * 0) + (55))).toBe("00:55");
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00");
});
});