Merge branch 'develop' of https://github.com/yaya-usman/matrix-react-sdk into favouriteMessages_Panel
This commit is contained in:
commit
a53f7f8302
70 changed files with 2195 additions and 996 deletions
2
.github/workflows/cypress.yaml
vendored
2
.github/workflows/cypress.yaml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
- id: prdetails
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: matrix-org/pr-details-action@v1.1
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
|
2
.github/workflows/netlify.yaml
vendored
2
.github/workflows/netlify.yaml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
Exercise caution. Use test accounts.
|
||||
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.1
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
|
|
@ -139,6 +139,9 @@ describe("Spotlight", () => {
|
|||
const room2Name = "Lounge";
|
||||
let room2Id: string;
|
||||
|
||||
const room3Name = "Public";
|
||||
let room3Id: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
@ -163,6 +166,19 @@ describe("Spotlight", () => {
|
|||
room2Id = _room2Id;
|
||||
bot2.invite(room2Id, bot1.getUserId());
|
||||
});
|
||||
bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public, initial_state: [{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "world_readable",
|
||||
},
|
||||
}],
|
||||
}).then(({ room_id: _room3Id }) => {
|
||||
room3Id = _room3Id;
|
||||
bot2.invite(room3Id, bot1.getUserId());
|
||||
});
|
||||
}),
|
||||
).then(() =>
|
||||
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
|
||||
|
@ -212,6 +228,7 @@ describe("Spotlight", () => {
|
|||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
|
@ -225,6 +242,7 @@ describe("Spotlight", () => {
|
|||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room2Id);
|
||||
}).then(() => {
|
||||
|
@ -233,6 +251,21 @@ describe("Spotlight", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should find unknown public world readable rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room3Id);
|
||||
}).then(() => {
|
||||
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||
cy.roomHeaderName().should("contain", room3Name);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||
// We obviously don’t have federation or bridges in cypress tests
|
||||
/*
|
||||
|
|
249
cypress/e2e/15-polls/polls.spec.ts
Normal file
249
cypress/e2e/15-polls/polls.spec.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { PollResponseEvent } from "matrix-events-sdk";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
||||
|
||||
describe("Polls", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
type CreatePollOptions = {
|
||||
title: string;
|
||||
options: string[];
|
||||
};
|
||||
const createPoll = ({ title, options }: CreatePollOptions) => {
|
||||
if (options.length < 2) {
|
||||
throw new Error('Poll must have at least two options');
|
||||
}
|
||||
cy.get('.mx_PollCreateDialog').within((pollCreateDialog) => {
|
||||
cy.get('#poll-topic-input').type(title);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const optionId = `#pollcreate_option_${index}`;
|
||||
|
||||
// click 'add option' button if needed
|
||||
if (pollCreateDialog.find(optionId).length === 0) {
|
||||
cy.get('.mx_PollCreateDialog_addOption').scrollIntoView().click();
|
||||
}
|
||||
cy.get(optionId).scrollIntoView().type(option);
|
||||
});
|
||||
});
|
||||
cy.get('.mx_Dialog button[type="submit"]').click();
|
||||
};
|
||||
|
||||
const getPollTile = (pollId: string): Chainable<JQuery> => {
|
||||
return cy.get(`.mx_EventTile[data-scroll-tokens="${pollId}"]`);
|
||||
};
|
||||
|
||||
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
|
||||
return getPollTile(pollId).contains('.mx_MPollBody_option .mx_StyledRadioButton', optionText);
|
||||
};
|
||||
|
||||
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
|
||||
getPollOption(pollId, optionText).within(() => {
|
||||
cy.get('.mx_MPollBody_optionVoteCount').should('contain', `${votes} vote`);
|
||||
});
|
||||
};
|
||||
|
||||
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
|
||||
getPollOption(pollId, optionText).within(ref => {
|
||||
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
|
||||
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||
bot.sendEvent(
|
||||
roomId,
|
||||
pollVote.type,
|
||||
pollVote.content,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_thread");
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("Open polls can be created and voted in", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
cy.get('.mx_CompoundDialog').percySnapshotElement('Polls Composer');
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
getPollTile(pollId).percySnapshotElement('Polls Timeline tile - no votes', { percyCSS: hideTimestampCSS });
|
||||
|
||||
// Bot votes 'Maybe' in the poll
|
||||
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
||||
|
||||
// no votes shown until I vote, check bots vote has arrived
|
||||
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
|
||||
|
||||
// vote 'Maybe'
|
||||
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||
// both me and bot have voted Maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
|
||||
// change my vote to 'Yes'
|
||||
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||
|
||||
// 1 vote for yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 1 vote for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 1);
|
||||
|
||||
// Bot updates vote to 'No'
|
||||
botVoteForOption(bot, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// 1 vote for yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 1 vote for no
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 0 for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||
});
|
||||
});
|
||||
|
||||
it("displays polls correctly in thread panel", () => {
|
||||
let botBob: MatrixClient;
|
||||
let botCharlie: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
botBob = _bot;
|
||||
});
|
||||
cy.getBot(synapse, { displayName: "BotCharlie" }).then(_bot => {
|
||||
botCharlie = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, botBob.getUserId());
|
||||
cy.inviteUser(roomId, botCharlie.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
// Bob starts thread on the poll
|
||||
botBob.sendMessage(roomId, pollId, {
|
||||
body: "Hello there",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
|
||||
// open the thread summary
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
|
||||
|
||||
// Bob votes 'Maybe' in the poll
|
||||
botVoteForOption(botBob, roomId, pollId, pollParams.options[2]);
|
||||
// Charlie votes 'No'
|
||||
botVoteForOption(botCharlie, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// no votes shown until I vote, check votes have arrived in main tl
|
||||
cy.get('.mx_RoomView_body .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||
// and thread view
|
||||
cy.get('.mx_ThreadView .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
// vote 'Maybe' in the main timeline poll
|
||||
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||
// both me and bob have voted Maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
});
|
||||
|
||||
cy.get('.mx_ThreadView').within(() => {
|
||||
// votes updated in thread view too
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
// change my vote to 'Yes'
|
||||
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||
});
|
||||
|
||||
// Bob updates vote to 'No'
|
||||
botVoteForOption(botBob, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// me: yes, bob: no, charlie: no
|
||||
const expectVoteCounts = () => {
|
||||
// I voted yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// Bob and Charlie voted no
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[1], 2);
|
||||
// 0 for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||
};
|
||||
|
||||
// check counts are correct in main timeline tile
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
// and in thread view tile
|
||||
cy.get('.mx_ThreadView').within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
174
cypress/e2e/lazy-loading/lazy-loading.spec.ts
Normal file
174
cypress/e2e/lazy-loading/lazy-loading.spec.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
interface Charly {
|
||||
client: MatrixClient;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
describe("Lazy Loading", () => {
|
||||
let synapse: SynapseInstance;
|
||||
let bob: MatrixClient;
|
||||
const charlies: Charly[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Alice");
|
||||
|
||||
cy.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bob => {
|
||||
bob = _bob;
|
||||
});
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const displayName = `Charly #${i}`;
|
||||
cy.getBot(synapse, {
|
||||
displayName,
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(client => {
|
||||
charlies[i - 1] = { displayName, client };
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
const name = "Lazy Loading Test";
|
||||
const alias = "#lltest:localhost";
|
||||
const charlyMsg1 = "hi bob!";
|
||||
const charlyMsg2 = "how's it going??";
|
||||
|
||||
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
|
||||
cy.window({ log: false }).then(win => {
|
||||
return cy.wrap(bob.createRoom({
|
||||
name,
|
||||
room_alias_name: "lltest",
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
}).then(r => r.room_id), { log: false }).as("roomId");
|
||||
});
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
|
||||
for (const charly of charlies) {
|
||||
cy.botSendMessage(charly.client, roomId, charlyMsg1);
|
||||
}
|
||||
for (const charly of charlies) {
|
||||
cy.botSendMessage(charly.client, roomId, charlyMsg2);
|
||||
}
|
||||
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
cy.botSendMessage(bob, roomId, `I will only say this ${i} time(s)!`);
|
||||
}
|
||||
});
|
||||
|
||||
cy.joinRoom(alias);
|
||||
cy.viewRoomByName(name);
|
||||
}
|
||||
|
||||
function checkPaginatedDisplayNames(charlies: Charly[]) {
|
||||
cy.scrollToTop();
|
||||
for (const charly of charlies) {
|
||||
cy.findEventTile(charly.displayName, charlyMsg1).should("exist");
|
||||
cy.findEventTile(charly.displayName, charlyMsg2).should("exist");
|
||||
}
|
||||
}
|
||||
|
||||
function openMemberlist(): void {
|
||||
cy.get('.mx_HeaderButtons [aria-label="Room Info"]').click();
|
||||
cy.get(".mx_RoomSummaryCard").within(() => {
|
||||
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||
});
|
||||
}
|
||||
|
||||
function getMembersInMemberlist(): Chainable<JQuery> {
|
||||
return cy.get(".mx_MemberList .mx_EntityTile_name");
|
||||
}
|
||||
|
||||
function checkMemberList(charlies: Charly[]) {
|
||||
getMembersInMemberlist().contains("Alice").should("exist");
|
||||
getMembersInMemberlist().contains("Bob").should("exist");
|
||||
charlies.forEach(charly => {
|
||||
getMembersInMemberlist().contains(charly.displayName).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
function checkMemberListLacksCharlies(charlies: Charly[]) {
|
||||
charlies.forEach(charly => {
|
||||
getMembersInMemberlist().contains(charly.displayName).should("not.exist");
|
||||
});
|
||||
}
|
||||
|
||||
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
|
||||
cy.goOffline();
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
cy.botSendMessage(charlies[0].client, roomId, "where is charly?");
|
||||
}
|
||||
});
|
||||
|
||||
cy.goOnline();
|
||||
cy.wait(1000); // Ideally we'd await a /sync here but intercepts step on each other from going offline/online
|
||||
}
|
||||
|
||||
it("should handle lazy loading properly even when offline", () => {
|
||||
const charly1to5 = charlies.slice(0, 5);
|
||||
const charly6to10 = charlies.slice(5);
|
||||
|
||||
// Set up room with alice, bob & charlies 1-5
|
||||
setupRoomWithBobAliceAndCharlies(charly1to5);
|
||||
// Alice should see 2 messages from every charly with the correct display name
|
||||
checkPaginatedDisplayNames(charly1to5);
|
||||
|
||||
openMemberlist();
|
||||
checkMemberList(charly1to5);
|
||||
joinCharliesWhileAliceIsOffline(charly6to10);
|
||||
checkMemberList(charly6to10);
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.leave(roomId);
|
||||
}
|
||||
});
|
||||
|
||||
checkMemberListLacksCharlies(charlies);
|
||||
});
|
||||
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import request from "browser-request";
|
||||
|
||||
import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
|
@ -31,10 +31,15 @@ interface CreateBotOpts {
|
|||
* The display name to give to that bot user
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Whether or not to start the syncing client.
|
||||
*/
|
||||
startClient?: boolean;
|
||||
}
|
||||
|
||||
const defaultCreateBotOptions = {
|
||||
autoAcceptInvites: true,
|
||||
startClient: true,
|
||||
} as CreateBotOpts;
|
||||
|
||||
declare global {
|
||||
|
@ -59,6 +64,13 @@ declare global {
|
|||
* @param roomName Name of the room to join
|
||||
*/
|
||||
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
||||
/**
|
||||
* Send a message as a bot into a room
|
||||
* @param cli The bot's MatrixClient
|
||||
* @param roomId ID of the room to join
|
||||
* @param message the message body to send
|
||||
*/
|
||||
botSendMessage(cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +100,10 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
});
|
||||
}
|
||||
|
||||
if (!opts.startClient) {
|
||||
return cy.wrap(cli);
|
||||
}
|
||||
|
||||
return cy.wrap(
|
||||
cli.initCrypto()
|
||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||
|
@ -114,3 +130,14 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
|
|||
|
||||
return cy.wrap(Promise.reject());
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botSendMessage", (
|
||||
cli: MatrixClient,
|
||||
roomId: string,
|
||||
message: string,
|
||||
): Chainable<ISendEventResponse> => {
|
||||
return cy.wrap(cli.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: message,
|
||||
}), { log: false });
|
||||
});
|
||||
|
|
|
@ -124,6 +124,11 @@ declare global {
|
|||
* Boostraps cross-signing.
|
||||
*/
|
||||
bootstrapCrossSigning(): Chainable<void>;
|
||||
/**
|
||||
* Joins the given room by alias or ID
|
||||
* @param roomIdOrAlias the id or alias of the room to join
|
||||
*/
|
||||
joinRoom(roomIdOrAlias: string): Chainable<Room>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -217,3 +222,7 @@ Cypress.Commands.add("bootstrapCrossSigning", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
|
||||
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
|
||||
});
|
||||
|
|
48
cypress/support/composer.ts
Normal file
48
cypress/support/composer.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Get the composer element
|
||||
// selects main timeline composer by default
|
||||
// set `isRightPanel` true to select right panel composer
|
||||
getComposer(isRightPanel?: boolean): Chainable<JQuery>;
|
||||
// Open the message composer kebab menu
|
||||
openMessageComposerOptions(isRightPanel?: boolean): Chainable<JQuery>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getComposer", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||
const panelClass = isRightPanel ? '.mx_RightPanel' : '.mx_RoomView_body';
|
||||
return cy.get(`${panelClass} .mx_MessageComposer`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||
cy.getComposer(isRightPanel).within(() => {
|
||||
cy.get('[aria-label="More options"]').click();
|
||||
});
|
||||
return cy.get('.mx_MessageComposer_Menu');
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -33,3 +33,6 @@ import "./percy";
|
|||
import "./webserver";
|
||||
import "./views";
|
||||
import "./iframes";
|
||||
import "./timeline";
|
||||
import "./network";
|
||||
import "./composer";
|
||||
|
|
62
cypress/support/network.ts
Normal file
62
cypress/support/network.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Intercept all /_matrix/ networking requests for the logged in user and fail them
|
||||
goOffline(): void;
|
||||
// Remove intercept on all /_matrix/ networking requests
|
||||
goOnline(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We manage intercepting Matrix APIs here, as fully disabling networking will disconnect
|
||||
// the browser under test from the Cypress runner, so can cause issues.
|
||||
|
||||
Cypress.Commands.add("goOffline", (): void => {
|
||||
cy.log("Going offline");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
req.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("goOnline", (): void => {
|
||||
cy.log("Going online");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
req.continue();
|
||||
});
|
||||
win.dispatchEvent(new Event("online"));
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
68
cypress/support/timeline.ts
Normal file
68
cypress/support/timeline.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Scroll to the top of the timeline
|
||||
scrollToTop(): void;
|
||||
// Find the event tile matching the given sender & body
|
||||
findEventTile(sender: string, body: string): Chainable<JQuery>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
sender: string;
|
||||
body: string;
|
||||
encrypted: boolean;
|
||||
continuation: boolean;
|
||||
}
|
||||
|
||||
Cypress.Commands.add("scrollToTop", (): void => {
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
|
||||
if (ref.scrollTop() > 0) {
|
||||
return cy.scrollToTop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<JQuery> => {
|
||||
// We can't just use a bunch of `.contains` here due to continuations meaning that the events don't
|
||||
// have their own rendered sender displayname so we have to walk the list to keep track of the sender.
|
||||
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then(refs => {
|
||||
let latestSender: string;
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const ref = refs.eq(i);
|
||||
const displayName = ref.find(".mx_DisambiguatedProfile_displayName");
|
||||
if (displayName) {
|
||||
latestSender = displayName.text();
|
||||
}
|
||||
|
||||
if (latestSender === sender && ref.find(".mx_EventTile_body").text() === body) {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -172,7 +172,7 @@
|
|||
"blob-polyfill": "^6.0.20211015",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^10.3.0",
|
||||
"cypress-real-events": "^1.7.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"eslint": "8.9.0",
|
||||
|
|
|
@ -22,6 +22,8 @@ limitations under the License.
|
|||
padding: $spacing-12 0;
|
||||
|
||||
border-bottom: 1px solid $system;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_BeaconListItem_avatarIcon {
|
||||
|
@ -61,3 +63,8 @@ limitations under the License.
|
|||
color: $tertiary-content;
|
||||
font-size: $font-10px;
|
||||
}
|
||||
|
||||
.mx_BeaconListItem_interactions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||
}
|
||||
|
||||
&.mx_RoomStatusBar_unsentResendAllBtn {
|
||||
&.mx_RoomStatusBar_unsentRetry {
|
||||
padding-left: 34px; // 28px from above, but +6px to account for the wider icon
|
||||
|
||||
&::before {
|
||||
|
|
|
@ -100,7 +100,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
&:not(.mx_RoomHeader_name--textonly):hover {
|
||||
background-color: $quinary-content;
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ limitations under the License.
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_name,
|
||||
.mx_RoomHeader_name:not(.mx_RoomHeader_name--textonly),
|
||||
.mx_RoomHeader_avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { split } from "lodash";
|
|||
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(
|
||||
|
@ -142,7 +143,12 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||
if (room.isSpaceRoom()) return null;
|
||||
|
||||
// If the room is not a DM don't fallback to a member avatar
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;
|
||||
if (
|
||||
!DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||
&& !(isLocalRoom(room))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there are only two members in the DM use the avatar of the other member
|
||||
const otherMember = room.getAvatarFallbackMember();
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
|
@ -44,6 +45,8 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
|||
case EventType.RoomAliases:
|
||||
case EventType.RoomCanonicalAlias:
|
||||
case EventType.RoomServerAcl:
|
||||
case M_BEACON.name:
|
||||
case M_BEACON.altName:
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,9 @@ import QueryMatcher from './QueryMatcher';
|
|||
import { PillCompletion } from './Components';
|
||||
import { ICompletion, ISelectionRange } from './Autocompleter';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { EMOJI, IEmoji } from '../emoji';
|
||||
import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji';
|
||||
import { TimelineRenderingType } from '../contexts/RoomContext';
|
||||
import * as recent from '../emojipicker/recent';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
|
@ -73,6 +74,7 @@ function colonsTrimmed(str: string): string {
|
|||
export default class EmojiProvider extends AutocompleteProvider {
|
||||
matcher: QueryMatcher<ISortedEmoji>;
|
||||
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||
private readonly recentlyUsed: IEmoji[];
|
||||
|
||||
constructor(room: Room, renderingType?: TimelineRenderingType) {
|
||||
super({ commandRegex: EMOJI_REGEX, renderingType });
|
||||
|
@ -87,6 +89,8 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
// For removing punctuation
|
||||
shouldMatchWordsOnly: true,
|
||||
});
|
||||
|
||||
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
|
||||
}
|
||||
|
||||
async getCompletions(
|
||||
|
@ -109,7 +113,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||
|
||||
const sorters = [];
|
||||
let sorters = [];
|
||||
// make sure that emoticons come first
|
||||
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
|
||||
|
||||
|
@ -130,6 +134,15 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
sorters.push(c => c._orderBy);
|
||||
completions = sortBy(uniq(completions), sorters);
|
||||
|
||||
completions = completions.slice(0, LIMIT);
|
||||
|
||||
// Do a second sort to place emoji matching with frequently used one on top
|
||||
sorters = [];
|
||||
this.recentlyUsed.forEach(emoji => {
|
||||
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
|
||||
});
|
||||
completions = sortBy(uniq(completions), sorters);
|
||||
|
||||
completions = completions.map(c => ({
|
||||
completion: c.emoji.unicode,
|
||||
component: (
|
||||
|
@ -138,7 +151,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
})).slice(0, LIMIT);
|
||||
}));
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@ import VideoChannelStore from "../../stores/VideoChannelStore";
|
|||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
||||
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
|
||||
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
||||
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
@ -890,7 +891,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||
let replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||
|
||||
if (isLocalRoom(this.state.currentRoomId)) {
|
||||
// Replace local room history items
|
||||
replaceLast = true;
|
||||
}
|
||||
|
||||
if (roomInfo.room_id === this.state.currentRoomId) {
|
||||
// if we are re-viewing the same room then copy any state we already know
|
||||
|
|
|
@ -24,11 +24,11 @@ import Resend from '../../Resend';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -240,7 +240,7 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
|||
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||
{ _t("Delete all") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentRetry">
|
||||
{ _t("Retry all") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
|
@ -252,28 +252,12 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
|||
</>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_unsentBadge">
|
||||
<NotificationBadge
|
||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_unsentTitle">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_unsentDescription">
|
||||
{ _t("You can select all or individual messages to retry or delete") }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||
{ buttonRow }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
return <RoomStatusBarUnsentMessages
|
||||
title={title}
|
||||
description={_t("You can select all or individual messages to retry or delete")}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
buttons={buttonRow}
|
||||
/>;
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
|
55
src/components/structures/RoomStatusBarUnsentMessages.tsx
Normal file
55
src/components/structures/RoomStatusBarUnsentMessages.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||
|
||||
interface RoomStatusBarUnsentMessagesProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
notificationState: StaticNotificationState;
|
||||
buttons: ReactElement;
|
||||
}
|
||||
|
||||
export const RoomStatusBarUnsentMessages = (props: RoomStatusBarUnsentMessagesProps): ReactElement => {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_unsentBadge">
|
||||
<NotificationBadge
|
||||
notification={props.notificationState}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_unsentTitle">
|
||||
{ props.title }
|
||||
</div>
|
||||
{
|
||||
props.description &&
|
||||
<div className="mx_RoomStatusBar_unsentDescription">
|
||||
{ props.description }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||
{ props.buttons }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { HTMLProps, useContext } from 'react';
|
||||
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||
import { humanizeTime } from '../../../utils/humanize';
|
||||
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import BeaconStatus from './BeaconStatus';
|
||||
|
@ -32,7 +33,7 @@ interface Props {
|
|||
beacon: Beacon;
|
||||
}
|
||||
|
||||
const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||
const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ...rest }) => {
|
||||
const latestLocationState = useEventEmitterState(
|
||||
beacon,
|
||||
BeaconEvent.LocationUpdate,
|
||||
|
@ -52,7 +53,7 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
|||
|
||||
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
||||
|
||||
return <li className='mx_BeaconListItem'>
|
||||
return <li className='mx_BeaconListItem' {...rest}>
|
||||
{ isSelfLocation ?
|
||||
<MemberAvatar
|
||||
className='mx_BeaconListItem_avatar'
|
||||
|
@ -69,7 +70,11 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
|||
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
||||
displayStatus={BeaconDisplayStatus.Active}
|
||||
>
|
||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||
{ /* eat events from interactive share buttons
|
||||
so parent click handlers are not triggered */ }
|
||||
<div className='mx_BeaconListItem_interactions' onClick={preventDefaultWrapper(() => {})}>
|
||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||
</div>
|
||||
</BeaconStatus>
|
||||
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import {
|
||||
Beacon,
|
||||
|
@ -45,7 +45,16 @@ interface IProps extends IDialogProps {
|
|||
roomId: Room['roomId'];
|
||||
matrixClient: MatrixClient;
|
||||
// open the map centered on this beacon's location
|
||||
focusBeacon?: Beacon;
|
||||
initialFocusedBeacon?: Beacon;
|
||||
}
|
||||
|
||||
// track the 'focused time' as ts
|
||||
// to make it possible to refocus the same beacon
|
||||
// as the beacon location may change
|
||||
// or the map may move around
|
||||
interface FocusedBeaconState {
|
||||
ts: number;
|
||||
beacon?: Beacon;
|
||||
}
|
||||
|
||||
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||
|
@ -59,31 +68,52 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
|||
});
|
||||
};
|
||||
|
||||
const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): {
|
||||
const useInitialMapPosition = (liveBeacons: Beacon[], { beacon, ts }: FocusedBeaconState): {
|
||||
bounds?: Bounds; centerGeoUri: string;
|
||||
} => {
|
||||
const bounds = useRef<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||
const centerGeoUri = useRef<string>(
|
||||
focusBeacon?.latestLocationState?.uri ||
|
||||
getBoundsCenter(bounds.current),
|
||||
const [bounds, setBounds] = useState<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||
const [centerGeoUri, setCenterGeoUri] = useState<string>(
|
||||
beacon?.latestLocationState?.uri ||
|
||||
getBoundsCenter(bounds),
|
||||
);
|
||||
return { bounds: bounds.current, centerGeoUri: centerGeoUri.current };
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
// this check ignores the first initial focused beacon state
|
||||
// as centering logic on map zooms to show everything
|
||||
// instead of focusing down
|
||||
ts !== 0 &&
|
||||
// only set focus to a known location
|
||||
beacon?.latestLocationState?.uri
|
||||
) {
|
||||
// append custom `mxTs` parameter to geoUri
|
||||
// so map is triggered to refocus on this uri
|
||||
// event if it was previously the center geouri
|
||||
// but the map have moved/zoomed
|
||||
setCenterGeoUri(`${beacon?.latestLocationState?.uri};mxTs=${Date.now()}`);
|
||||
setBounds(getBeaconBounds([beacon]));
|
||||
}
|
||||
}, [beacon, ts]);
|
||||
|
||||
return { bounds, centerGeoUri };
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog to view live beacons maximised
|
||||
*/
|
||||
const BeaconViewDialog: React.FC<IProps> = ({
|
||||
focusBeacon,
|
||||
initialFocusedBeacon,
|
||||
roomId,
|
||||
matrixClient,
|
||||
onFinished,
|
||||
}) => {
|
||||
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
||||
const [focusedBeaconState, setFocusedBeaconState] =
|
||||
useState<FocusedBeaconState>({ beacon: initialFocusedBeacon, ts: 0 });
|
||||
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
|
||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusedBeaconState);
|
||||
|
||||
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
||||
|
||||
|
@ -94,6 +124,10 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
|||
}
|
||||
}, [mapDisplayError]);
|
||||
|
||||
const onBeaconListItemClick = (beacon: Beacon) => {
|
||||
setFocusedBeaconState({ beacon, ts: Date.now() });
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_BeaconViewDialog'
|
||||
|
@ -144,7 +178,7 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
|||
</MapFallback>
|
||||
}
|
||||
{ isSidebarOpen ?
|
||||
<DialogSidebar beacons={liveBeacons} requestClose={() => setSidebarOpen(false)} /> :
|
||||
<DialogSidebar beacons={liveBeacons} onBeaconClick={onBeaconListItemClick} requestClose={() => setSidebarOpen(false)} /> :
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
|
|
|
@ -26,9 +26,14 @@ import BeaconListItem from './BeaconListItem';
|
|||
interface Props {
|
||||
beacons: Beacon[];
|
||||
requestClose: () => void;
|
||||
onBeaconClick: (beacon: Beacon) => void;
|
||||
}
|
||||
|
||||
const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
||||
const DialogSidebar: React.FC<Props> = ({
|
||||
beacons,
|
||||
onBeaconClick,
|
||||
requestClose,
|
||||
}) => {
|
||||
return <div className='mx_DialogSidebar'>
|
||||
<div className='mx_DialogSidebar_header'>
|
||||
<Heading size='h4'>{ _t('View List') }</Heading>
|
||||
|
@ -36,13 +41,17 @@ const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
|||
className='mx_DialogSidebar_closeButton'
|
||||
onClick={requestClose}
|
||||
title={_t('Close sidebar')}
|
||||
data-test-id='dialog-sidebar-close'
|
||||
data-testid='dialog-sidebar-close'
|
||||
>
|
||||
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<ol className='mx_DialogSidebar_list'>
|
||||
{ beacons.map((beacon) => <BeaconListItem key={beacon.identifier} beacon={beacon} />) }
|
||||
{ beacons.map((beacon) => <BeaconListItem
|
||||
key={beacon.identifier}
|
||||
beacon={beacon}
|
||||
onClick={() => onBeaconClick(beacon)}
|
||||
/>) }
|
||||
</ol>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import React, { HTMLProps } from 'react';
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
||||
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||
import BeaconStatus from './BeaconStatus';
|
||||
import { BeaconDisplayStatus } from './displayStatus';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
|
@ -45,14 +46,6 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
onResetLocationPublishError,
|
||||
} = useOwnLiveBeacons([beacon?.identifier]);
|
||||
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
callback();
|
||||
};
|
||||
|
||||
// combine display status with errors that only occur for user's own beacons
|
||||
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
||||
BeaconDisplayStatus.Error :
|
||||
|
@ -68,7 +61,9 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
||||
data-test-id='beacon-status-stop-beacon'
|
||||
kind='link'
|
||||
onClick={preventDefaultWrapper(onStopSharing)}
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper<ButtonEvent>(onStopSharing)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
disabled={stoppingInProgress}
|
||||
>
|
||||
|
@ -78,6 +73,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ hasLocationPublishError && <AccessibleButton
|
||||
data-test-id='beacon-status-reset-wire-error'
|
||||
kind='link'
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
>
|
||||
|
@ -87,6 +84,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ hasStopSharingError && <AccessibleButton
|
||||
data-test-id='beacon-status-stop-beacon-retry'
|
||||
kind='link'
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper(onStopSharing)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
>
|
||||
|
|
|
@ -91,6 +91,7 @@ import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
|
|||
import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
||||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||
import { TooltipOption } from "./TooltipOption";
|
||||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||
|
@ -243,6 +244,9 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
|||
|
||||
const findVisibleRooms = (cli: MatrixClient) => {
|
||||
return cli.getVisibleRooms().filter(room => {
|
||||
// Do not show local rooms
|
||||
if (isLocalRoom(room)) return false;
|
||||
|
||||
// TODO we may want to put invites in their own list
|
||||
return room.getMyMembership() === "join" || room.getMyMembership() == "invite";
|
||||
});
|
||||
|
@ -395,7 +399,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
|
||||
possibleResults.forEach(entry => {
|
||||
if (isRoomResult(entry)) {
|
||||
if (!entry.room.normalizedName.includes(normalizedQuery) &&
|
||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||
!entry.query?.some(q => q.includes(lcQuery))
|
||||
) return; // bail, does not match query
|
||||
|
@ -603,6 +607,16 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
if (isPublicRoomResult(result)) {
|
||||
const clientRoom = cli.getRoom(result.publicRoom.room_id);
|
||||
// Element Web currently does not allow guests to join rooms, so we
|
||||
// instead show them view buttons for all rooms. If the room is not
|
||||
// world readable, a modal will appear asking you to register first. If
|
||||
// it is readable, the preview appears as normal.
|
||||
const showViewButton = (
|
||||
clientRoom?.getMyMembership() === "join" ||
|
||||
result.publicRoom.world_readable ||
|
||||
cli.isGuest()
|
||||
);
|
||||
|
||||
const listener = (ev) => {
|
||||
const { publicRoom } = result;
|
||||
viewRoom({
|
||||
|
@ -618,11 +632,11 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
onClick={listener}
|
||||
endAdornment={
|
||||
<AccessibleButton
|
||||
kind={clientRoom ? "primary" : "primary_outline"}
|
||||
kind={showViewButton ? "primary_outline" : "primary"}
|
||||
onClick={listener}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{ _t(clientRoom ? "View" : "Join") }
|
||||
{ showViewButton ? _t("View") : _t("Join") }
|
||||
</AccessibleButton>}
|
||||
aria-labelledby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_name`}
|
||||
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
|
||||
|
|
|
@ -80,6 +80,13 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) =>
|
|||
interface MapProps {
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
/**
|
||||
* set map center to geoUri coords
|
||||
* Center will only be set to valid geoUri
|
||||
* this prop is only simply diffed by useEffect, so to trigger *recentering* of the same geoUri
|
||||
* append the uri with a var not used by the geoUri spec
|
||||
* eg a timestamp: `geo:54,42;mxTs=123`
|
||||
*/
|
||||
centerGeoUri?: string;
|
||||
bounds?: Bounds;
|
||||
className?: string;
|
||||
|
|
|
@ -24,6 +24,7 @@ import EventTileBubble from "./EventTileBubble";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -46,12 +47,15 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp
|
|||
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
|
||||
let subtitle: string;
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
const room = cli?.getRoom(roomId);
|
||||
if (prevContent.algorithm === ALGORITHM) {
|
||||
subtitle = _t("Some encryption parameters have been changed.");
|
||||
} else if (dmPartner) {
|
||||
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||
const displayName = room.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||
subtitle = _t("Messages here are end-to-end encrypted. " +
|
||||
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
|
||||
} else if (isLocalRoom(room)) {
|
||||
subtitle = _t("Messages in this chat will be end-to-end encrypted.");
|
||||
} else {
|
||||
subtitle = _t("Messages in this room are end-to-end encrypted. " +
|
||||
"When people join, you can verify them in their profile, just tap on their avatar.");
|
||||
|
|
|
@ -162,7 +162,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
|
|||
{
|
||||
roomId: mxEvent.getRoomId(),
|
||||
matrixClient,
|
||||
focusBeacon: beacon,
|
||||
initialFocusedBeacon: beacon,
|
||||
isMapDisplayError,
|
||||
},
|
||||
"mx_BeaconViewDialog_wrapper",
|
||||
|
|
|
@ -80,6 +80,7 @@ import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../event
|
|||
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
||||
import { ReadReceiptGroup } from './ReadReceiptGroup';
|
||||
import { useTooltip } from "../../../utils/useTooltip";
|
||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||
|
||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||
|
||||
|
@ -766,6 +767,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
private renderE2EPadlock() {
|
||||
const ev = this.props.mxEvent;
|
||||
|
||||
// no icon for local rooms
|
||||
if (isLocalRoom(ev.getRoomId())) return;
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable />;
|
||||
|
|
|
@ -38,6 +38,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
import { LocalRoom } from "../../../models/LocalRoom";
|
||||
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
|
@ -49,11 +50,19 @@ const NewRoomIntro = () => {
|
|||
const cli = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useContext(RoomContext);
|
||||
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
const isLocalRoom = room instanceof LocalRoom;
|
||||
const dmPartner = isLocalRoom
|
||||
? room.targets[0]?.userId
|
||||
: DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
|
||||
let body;
|
||||
if (dmPartner) {
|
||||
let introMessage = _t("This is the beginning of your direct message history with <displayName/>.");
|
||||
let caption;
|
||||
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||
|
||||
if (isLocalRoom) {
|
||||
introMessage = _t("Send your first message to invite <displayName/> to chat");
|
||||
} else if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||
}
|
||||
|
||||
|
@ -75,7 +84,7 @@ const NewRoomIntro = () => {
|
|||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
<p>{ _t("This is the beginning of your direct message history with <displayName/>.", {}, {
|
||||
<p>{ _t(introMessage, {}, {
|
||||
displayName: () => <b>{ displayName }</b>,
|
||||
}) }</p>
|
||||
{ caption && <p>{ caption }</p> }
|
||||
|
@ -200,7 +209,7 @@ const NewRoomIntro = () => {
|
|||
);
|
||||
|
||||
let subButton;
|
||||
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) {
|
||||
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get()) && !isLocalRoom) {
|
||||
subButton = (
|
||||
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
|
||||
);
|
||||
|
|
|
@ -65,6 +65,8 @@ interface IProps {
|
|||
appsShown: boolean;
|
||||
searchInfo: ISearchInfo;
|
||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -76,6 +78,8 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
editing: false,
|
||||
inRoom: false,
|
||||
excludedRightPanelPhaseButtons: [],
|
||||
showButtons: true,
|
||||
enableRoomOptionsMenu: true,
|
||||
};
|
||||
|
||||
static contextType = RoomContext;
|
||||
|
@ -130,81 +134,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: null });
|
||||
};
|
||||
|
||||
public render() {
|
||||
let searchStatus = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let oobName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
oobName = this.props.oobData.name;
|
||||
}
|
||||
|
||||
let contextMenu: JSX.Element;
|
||||
if (this.state.contextMenuPosition && this.props.room) {
|
||||
contextMenu = (
|
||||
<RoomContextMenu
|
||||
{...contextMenuBelow(this.state.contextMenuPosition)}
|
||||
room={this.props.room}
|
||||
onFinished={this.onContextMenuCloseClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const name = (
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_RoomHeader_name"
|
||||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
>
|
||||
<RoomName room={this.props.room}>
|
||||
{ (name) => {
|
||||
const roomName = name || oobName;
|
||||
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
|
||||
} }
|
||||
</RoomName>
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
{ contextMenu }
|
||||
</ContextMenuTooltipButton>
|
||||
);
|
||||
|
||||
const topicElement = <RoomTopic
|
||||
room={this.props.room}
|
||||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
|
||||
let roomAvatar;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderButtons(): JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.inRoom &&
|
||||
|
@ -269,10 +199,105 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
buttons.push(inviteButton);
|
||||
}
|
||||
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ buttons }
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private renderName(oobName) {
|
||||
let contextMenu: JSX.Element;
|
||||
if (this.state.contextMenuPosition && this.props.room) {
|
||||
contextMenu = (
|
||||
<RoomContextMenu
|
||||
{...contextMenuBelow(this.state.contextMenuPosition)}
|
||||
room={this.props.room}
|
||||
onFinished={this.onContextMenuCloseClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const roomName = <RoomName room={this.props.room}>
|
||||
{ (name) => {
|
||||
const roomName = name || oobName;
|
||||
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
|
||||
} }
|
||||
</RoomName>;
|
||||
|
||||
if (this.props.enableRoomOptionsMenu) {
|
||||
return (
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_RoomHeader_name"
|
||||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
>
|
||||
{ roomName }
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
{ contextMenu }
|
||||
</ContextMenuTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">
|
||||
{ roomName }
|
||||
</div>;
|
||||
}
|
||||
|
||||
public render() {
|
||||
let searchStatus = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let oobName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
oobName = this.props.oobData.name;
|
||||
}
|
||||
|
||||
const name = this.renderName(oobName);
|
||||
|
||||
const topicElement = <RoomTopic
|
||||
room={this.props.room}
|
||||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
|
||||
let roomAvatar;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (this.props.showButtons) {
|
||||
buttons = <React.Fragment>
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||
|
||||
|
@ -294,8 +319,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
{ searchStatus }
|
||||
{ topicElement }
|
||||
{ betaPill }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||
{ buttons }
|
||||
</div>
|
||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||
</div>
|
||||
|
|
|
@ -1730,8 +1730,9 @@
|
|||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Insert link": "Insert link",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"Send your first message to invite <displayName/> to chat": "Send your first message to invite <displayName/> to chat",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
||||
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
|
||||
|
@ -1771,15 +1772,15 @@
|
|||
"Room %(name)s": "Room %(name)s",
|
||||
"Recently visited rooms": "Recently visited rooms",
|
||||
"No recently visited rooms": "No recently visited rooms",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
"Join Room": "Join Room",
|
||||
"Room options": "Room options",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Invite": "Invite",
|
||||
"Room options": "Room options",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
"Join Room": "Join Room",
|
||||
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
||||
"Video room": "Video room",
|
||||
"Public space": "Public space",
|
||||
|
@ -2102,6 +2103,7 @@
|
|||
"View Source": "View Source",
|
||||
"Some encryption parameters have been changed.": "Some encryption parameters have been changed.",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
||||
"Messages in this chat will be end-to-end encrypted.": "Messages in this chat will be end-to-end encrypted.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
||||
"Encryption enabled": "Encryption enabled",
|
||||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { isLocalRoom } from "../utils/localRoom/isLocalRoom";
|
||||
import Timer from "../utils/Timer";
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000;
|
||||
|
@ -64,6 +65,9 @@ export default class TypingStore {
|
|||
* @param {boolean} isTyping Whether the user is typing or not.
|
||||
*/
|
||||
public setSelfTyping(roomId: string, threadId: string | null, isTyping: boolean): void {
|
||||
// No typing notifications for local rooms
|
||||
if (isLocalRoom(roomId)) return;
|
||||
|
||||
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
||||
if (SettingsStore.getValue('lowBandwidth')) return;
|
||||
// Disable typing notification for threads for the initial launch
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||
import { LocalRoom } from "../../../models/LocalRoom";
|
||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||
import VoipUserMapper from "../../../VoipUserMapper";
|
||||
|
||||
export class VisibilityProvider {
|
||||
|
@ -55,7 +55,7 @@ export class VisibilityProvider {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (room instanceof LocalRoom) {
|
||||
if (isLocalRoom(room)) {
|
||||
// local rooms shouldn't show up anywhere
|
||||
return false;
|
||||
}
|
||||
|
|
25
src/utils/NativeEventUtils.ts
Normal file
25
src/utils/NativeEventUtils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
// Wrap DOM event handlers with stopPropagation and preventDefault
|
||||
export const preventDefaultWrapper =
|
||||
<T extends React.BaseSyntheticEvent = React.BaseSyntheticEvent>(callback: () => void) => (e?: T) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
callback();
|
||||
};
|
26
src/utils/localRoom/isLocalRoom.ts
Normal file
26
src/utils/localRoom/isLocalRoom.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
|
||||
|
||||
export function isLocalRoom(roomOrID: Room|string): boolean {
|
||||
if (typeof roomOrID === "string") {
|
||||
return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX);
|
||||
}
|
||||
return roomOrID instanceof LocalRoom;
|
||||
}
|
|
@ -35,7 +35,7 @@ export const useMap = ({
|
|||
interactive,
|
||||
bodyId,
|
||||
onError,
|
||||
}: UseMapProps): MapLibreMap => {
|
||||
}: UseMapProps): MapLibreMap | undefined => {
|
||||
const [map, setMap] = useState<MapLibreMap>();
|
||||
|
||||
useEffect(
|
||||
|
|
102
test/Avatar-test.ts
Normal file
102
test/Avatar-test.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { avatarUrlForRoom } from "../src/Avatar";
|
||||
import { Media, mediaFromMxc } from "../src/customisations/Media";
|
||||
import DMRoomMap from "../src/utils/DMRoomMap";
|
||||
|
||||
jest.mock("../src/customisations/Media", () => ({
|
||||
mediaFromMxc: jest.fn(),
|
||||
}));
|
||||
|
||||
const roomId = "!room:example.com";
|
||||
const avatarUrl1 = "https://example.com/avatar1";
|
||||
const avatarUrl2 = "https://example.com/avatar2";
|
||||
|
||||
describe("avatarUrlForRoom", () => {
|
||||
let getThumbnailOfSourceHttp: jest.Mock;
|
||||
let room: Room;
|
||||
let roomMember: RoomMember;
|
||||
let dmRoomMap: DMRoomMap;
|
||||
|
||||
beforeEach(() => {
|
||||
getThumbnailOfSourceHttp = jest.fn();
|
||||
mocked(mediaFromMxc).mockImplementation((): Media => {
|
||||
return {
|
||||
getThumbnailOfSourceHttp,
|
||||
} as unknown as Media;
|
||||
});
|
||||
room = {
|
||||
roomId,
|
||||
getMxcAvatarUrl: jest.fn(),
|
||||
isSpaceRoom: jest.fn(),
|
||||
getAvatarFallbackMember: jest.fn(),
|
||||
} as unknown as Room;
|
||||
dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
roomMember = {
|
||||
getMxcAvatarUrl: jest.fn(),
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
it("should return null for a null room", () => {
|
||||
expect(avatarUrlForRoom(null, 128, 128)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the HTTP source if the room provides a MXC url", () => {
|
||||
mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1);
|
||||
getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
|
||||
expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
|
||||
expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
|
||||
});
|
||||
|
||||
it("should return null for a space room", () => {
|
||||
mocked(room.isSpaceRoom).mockReturnValue(true);
|
||||
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null if the room is not a DM", () => {
|
||||
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue(null);
|
||||
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||
expect(dmRoomMap.getUserIdForRoomId).toHaveBeenCalledWith(roomId);
|
||||
});
|
||||
|
||||
it("should return null if there is no other member in the room", () => {
|
||||
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||
mocked(room.getAvatarFallbackMember).mockReturnValue(null);
|
||||
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null if the other member has no avatar URL", () => {
|
||||
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
|
||||
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the other member's avatar URL", () => {
|
||||
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
|
||||
mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2);
|
||||
getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
|
||||
expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
|
||||
expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
|
||||
});
|
||||
});
|
119
test/Unread-test.ts
Normal file
119
test/Unread-test.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import {
|
||||
MatrixEvent,
|
||||
EventType,
|
||||
MsgType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { haveRendererForEvent } from "../src/events/EventTileFactory";
|
||||
import { getMockClientWithEventEmitter, makeBeaconEvent, mockClientMethodsUser } from "./test-utils";
|
||||
import { eventTriggersUnreadCount } from "../src/Unread";
|
||||
|
||||
jest.mock("../src/events/EventTileFactory", () => ({
|
||||
haveRendererForEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('eventTriggersUnreadCount()', () => {
|
||||
const aliceId = '@alice:server.org';
|
||||
const bobId = '@bob:server.org';
|
||||
|
||||
// mock user credentials
|
||||
getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(bobId),
|
||||
});
|
||||
|
||||
// setup events
|
||||
const alicesMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: aliceId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: 'Hello from Alice',
|
||||
},
|
||||
});
|
||||
|
||||
const bobsMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: bobId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: 'Hello from Bob',
|
||||
},
|
||||
});
|
||||
|
||||
const redactedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: aliceId,
|
||||
});
|
||||
redactedEvent.makeRedacted(redactedEvent);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mocked(haveRendererForEvent).mockClear().mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns false when the event was sent by the current user', () => {
|
||||
expect(eventTriggersUnreadCount(bobsMessage)).toBe(false);
|
||||
// returned early before checking renderer
|
||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns false for a redacted event', () => {
|
||||
expect(eventTriggersUnreadCount(redactedEvent)).toBe(false);
|
||||
// returned early before checking renderer
|
||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns false for an event without a renderer', () => {
|
||||
mocked(haveRendererForEvent).mockReturnValue(false);
|
||||
expect(eventTriggersUnreadCount(alicesMessage)).toBe(false);
|
||||
expect(haveRendererForEvent).toHaveBeenCalledWith(alicesMessage, false);
|
||||
});
|
||||
|
||||
it('returns true for an event with a renderer', () => {
|
||||
mocked(haveRendererForEvent).mockReturnValue(true);
|
||||
expect(eventTriggersUnreadCount(alicesMessage)).toBe(true);
|
||||
expect(haveRendererForEvent).toHaveBeenCalledWith(alicesMessage, false);
|
||||
});
|
||||
|
||||
it('returns false for beacon locations', () => {
|
||||
const beaconLocationEvent = makeBeaconEvent(aliceId);
|
||||
expect(eventTriggersUnreadCount(beaconLocationEvent)).toBe(false);
|
||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const noUnreadEventTypes = [
|
||||
EventType.RoomMember,
|
||||
EventType.RoomThirdPartyInvite,
|
||||
EventType.CallAnswer,
|
||||
EventType.CallHangup,
|
||||
EventType.RoomAliases,
|
||||
EventType.RoomCanonicalAlias,
|
||||
EventType.RoomServerAcl,
|
||||
];
|
||||
|
||||
it.each(noUnreadEventTypes)('returns false without checking for renderer for events with type %s', (eventType) => {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
sender: aliceId,
|
||||
});
|
||||
expect(eventTriggersUnreadCount(event)).toBe(false);
|
||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -16,6 +16,9 @@ limitations under the License.
|
|||
|
||||
import EmojiProvider from '../../src/autocomplete/EmojiProvider';
|
||||
import { mkStubRoom } from '../test-utils/test-utils';
|
||||
import { add } from "../../src/emojipicker/recent";
|
||||
import { stubClient } from "../test-utils";
|
||||
import { MatrixClientPeg } from '../../src/MatrixClientPeg';
|
||||
|
||||
const EMOJI_SHORTCODES = [
|
||||
":+1",
|
||||
|
@ -42,6 +45,8 @@ const TOO_SHORT_EMOJI_SHORTCODE = [
|
|||
|
||||
describe('EmojiProvider', function() {
|
||||
const testRoom = mkStubRoom(undefined, undefined, undefined);
|
||||
stubClient();
|
||||
MatrixClientPeg.get();
|
||||
|
||||
it.each(EMOJI_SHORTCODES)('Returns consistent results after final colon %s', async function(emojiShortcode) {
|
||||
const ep = new EmojiProvider(testRoom);
|
||||
|
@ -64,4 +69,21 @@ describe('EmojiProvider', function() {
|
|||
|
||||
expect(completions[0].completion).toEqual(expectedEmoji);
|
||||
});
|
||||
|
||||
it('Returns correct autocompletion based on recently used emoji', async function() {
|
||||
add("😘"); //kissing_heart
|
||||
add("😘");
|
||||
add("😚"); //kissing_closed_eyes
|
||||
const emojiProvider = new EmojiProvider(null);
|
||||
|
||||
let completionsList = await emojiProvider.getCompletions(":kis", { beginning: true, end: 3, start: 3 });
|
||||
expect(completionsList[0].component.props.title).toEqual(":kissing_heart:");
|
||||
expect(completionsList[1].component.props.title).toEqual(":kissing_closed_eyes:");
|
||||
|
||||
completionsList = await emojiProvider.getCompletions(":kissing_c", { beginning: true, end: 3, start: 3 });
|
||||
expect(completionsList[0].component.props.title).toEqual(":kissing_closed_eyes:");
|
||||
|
||||
completionsList = await emojiProvider.getCompletions(":so", { beginning: true, end: 2, start: 2 });
|
||||
expect(completionsList[0].component.props.title).toEqual(":sob:");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { RoomStatusBarUnsentMessages } from "../../../src/components/structures/RoomStatusBarUnsentMessages";
|
||||
import { StaticNotificationState } from "../../../src/stores/notifications/StaticNotificationState";
|
||||
|
||||
describe("RoomStatusBarUnsentMessages", () => {
|
||||
const title = "test title";
|
||||
const description = "test description";
|
||||
const buttonsText = "test buttons";
|
||||
const buttons = <div>{ buttonsText }</div>;
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={title}
|
||||
description={description}
|
||||
buttons={buttons}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the values passed as props", () => {
|
||||
screen.getByText(title);
|
||||
screen.getByText(description);
|
||||
screen.getByText(buttonsText);
|
||||
// notification state
|
||||
screen.getByText("!");
|
||||
});
|
||||
});
|
|
@ -27,6 +27,7 @@ import { act } from 'react-dom/test-utils';
|
|||
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||
import {
|
||||
findByTestId,
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
|
@ -169,5 +170,30 @@ describe('<BeaconListItem />', () => {
|
|||
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
it('does not call onClick handler when clicking share button', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const component = getComponent({ beacon, onClick });
|
||||
|
||||
act(() => {
|
||||
findByTestId(component, 'open-location-in-osm').at(0).simulate('click');
|
||||
});
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClick handler when clicking outside of share buttons', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const component = getComponent({ beacon, onClick });
|
||||
|
||||
act(() => {
|
||||
// click the beacon name
|
||||
component.find('.mx_BeaconStatus_description').simulate('click');
|
||||
});
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
MatrixClient,
|
||||
|
@ -28,15 +28,18 @@ import maplibregl from 'maplibre-gl';
|
|||
|
||||
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
|
||||
import {
|
||||
findByAttr,
|
||||
findByTestId,
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
makeRoomWithStateEvents,
|
||||
} from '../../../test-utils';
|
||||
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
|
||||
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
||||
import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus';
|
||||
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||
|
||||
describe('<BeaconViewDialog />', () => {
|
||||
// 14.03.2022 16:15
|
||||
|
@ -89,13 +92,18 @@ describe('<BeaconViewDialog />', () => {
|
|||
const getComponent = (props = {}) =>
|
||||
mount(<BeaconViewDialog {...defaultProps} {...props} />);
|
||||
|
||||
const openSidebar = (component: ReactWrapper) => act(() => {
|
||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
maplibregl.AttributionControl = jest.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore();
|
||||
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
@ -225,10 +233,7 @@ describe('<BeaconViewDialog />', () => {
|
|||
beacon.addLocations([location1]);
|
||||
const component = getComponent();
|
||||
|
||||
act(() => {
|
||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
openSidebar(component);
|
||||
|
||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||
});
|
||||
|
@ -240,20 +245,134 @@ describe('<BeaconViewDialog />', () => {
|
|||
const component = getComponent();
|
||||
|
||||
// open the sidebar
|
||||
act(() => {
|
||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
openSidebar(component);
|
||||
|
||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||
|
||||
// now close it
|
||||
act(() => {
|
||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
||||
findByAttr('data-testid')(component, 'dialog-sidebar-close').at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
expect(component.find('DialogSidebar').length).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('focused beacons', () => {
|
||||
const beacon2Event = makeBeaconInfoEvent(bobId,
|
||||
roomId,
|
||||
{ isLive: true },
|
||||
'$bob-room1-2',
|
||||
);
|
||||
|
||||
const location2 = makeBeaconEvent(
|
||||
bobId, { beaconInfoId: beacon2Event.getId(), geoUri: 'geo:33,22', timestamp: now + 1 },
|
||||
);
|
||||
|
||||
const fitBoundsOptions = { maxZoom: 15, padding: 100 };
|
||||
|
||||
it('opens map with both beacons in view on first load without initialFocusedBeacon', () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||
);
|
||||
|
||||
getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// start centered on mid point between both beacons
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 42, lon: 31.5 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fit both beacons, only called once
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||
[22, 33], [41, 51],
|
||||
), fitBoundsOptions);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens map with both beacons in view on first load with an initially focused beacon', () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||
);
|
||||
|
||||
getComponent({ beacons: [beacon1, beacon2], initialFocusedBeacon: beacon1 });
|
||||
|
||||
// start centered on initialFocusedBeacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fit both beacons, only called once
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||
[22, 33], [41, 51],
|
||||
), fitBoundsOptions);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('focuses on beacon location on sidebar list item click', () => {
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||
);
|
||||
|
||||
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// reset call counts on map mocks after initial render
|
||||
jest.clearAllMocks();
|
||||
|
||||
openSidebar(component);
|
||||
|
||||
act(() => {
|
||||
// click on the first beacon in the list
|
||||
component.find(BeaconListItem).at(0).simulate('click');
|
||||
});
|
||||
|
||||
// centered on clicked beacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
// only called once
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||
// bounds fitted just to clicked beacon
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||
[41, 51], [41, 51],
|
||||
), fitBoundsOptions);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refocuses on same beacon when clicking list item again', () => {
|
||||
// test the map responds to refocusing the same beacon
|
||||
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||
);
|
||||
|
||||
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||
|
||||
// reset call counts on map mocks after initial render
|
||||
jest.clearAllMocks();
|
||||
|
||||
openSidebar(component);
|
||||
|
||||
act(() => {
|
||||
// click on the second beacon in the list
|
||||
component.find(BeaconListItem).at(1).simulate('click');
|
||||
});
|
||||
|
||||
const expectedBounds = new maplibregl.LngLatBounds(
|
||||
[22, 33], [22, 33],
|
||||
);
|
||||
|
||||
// date is mocked but this relies on timestamp, manually mock a tick
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(now + 1);
|
||||
|
||||
act(() => {
|
||||
// click on the second beacon in the list
|
||||
component.find(BeaconListItem).at(1).simulate('click');
|
||||
});
|
||||
|
||||
// centered on clicked beacon
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 33, lon: 22 });
|
||||
// bounds fitted just to clicked beacon
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledWith(expectedBounds, fitBoundsOptions);
|
||||
// each called once per click
|
||||
expect(mockMap.setCenter).toHaveBeenCalledTimes(2);
|
||||
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,31 +15,88 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar';
|
||||
import { findByTestId } from '../../../test-utils';
|
||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
mockClientMethodsUser,
|
||||
} from '../../../test-utils';
|
||||
|
||||
describe('<DialogSidebar />', () => {
|
||||
const defaultProps = {
|
||||
beacons: [],
|
||||
requestClose: jest.fn(),
|
||||
onBeaconClick: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<DialogSidebar {...defaultProps} {...props} />);
|
||||
|
||||
it('renders sidebar correctly', () => {
|
||||
const component = getComponent();
|
||||
expect(component).toMatchSnapshot();
|
||||
const now = 1647270879403;
|
||||
|
||||
const roomId = '!room:server.org';
|
||||
const aliceId = '@alice:server.org';
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(aliceId),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
|
||||
const beaconEvent = makeBeaconInfoEvent(aliceId,
|
||||
roomId,
|
||||
{ isLive: true, timestamp: now },
|
||||
'$alice-room1-1',
|
||||
);
|
||||
const location1 = makeBeaconEvent(
|
||||
aliceId, { beaconInfoId: beaconEvent.getId(), geoUri: 'geo:51,41', timestamp: now },
|
||||
);
|
||||
|
||||
const getComponent = (props = {}) => (
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<DialogSidebar {...defaultProps} {...props} />);
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
beforeEach(() => {
|
||||
// mock now so time based text in snapshots is stable
|
||||
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(Date, 'now').mockRestore();
|
||||
});
|
||||
|
||||
it('renders sidebar correctly without beacons', () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders sidebar correctly with beacons', () => {
|
||||
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||
const { container } = render(getComponent({ beacons: [beacon] }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls on beacon click', () => {
|
||||
const onBeaconClick = jest.fn();
|
||||
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
|
||||
|
||||
act(() => {
|
||||
const [listItem] = container.getElementsByClassName('mx_BeaconListItem');
|
||||
fireEvent.click(listItem);
|
||||
});
|
||||
|
||||
expect(onBeaconClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closes on close button click', () => {
|
||||
const requestClose = jest.fn();
|
||||
const component = getComponent({ requestClose });
|
||||
const { getByTestId } = render(getComponent({ requestClose }));
|
||||
|
||||
act(() => {
|
||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
||||
fireEvent.click(getByTestId('dialog-sidebar-close'));
|
||||
});
|
||||
expect(requestClose).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div class=\\"mx_BeaconListItem_interactions\\"><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
||||
|
|
|
@ -1,53 +1,144 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DialogSidebar /> renders sidebar correctly 1`] = `
|
||||
<DialogSidebar
|
||||
beacons={Array []}
|
||||
requestClose={[MockFunction]}
|
||||
>
|
||||
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="mx_DialogSidebar"
|
||||
class="mx_DialogSidebar"
|
||||
>
|
||||
<div
|
||||
className="mx_DialogSidebar_header"
|
||||
class="mx_DialogSidebar_header"
|
||||
>
|
||||
<Heading
|
||||
size="h4"
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
<h4
|
||||
className="mx_Heading_h4"
|
||||
>
|
||||
View List
|
||||
</h4>
|
||||
</Heading>
|
||||
<AccessibleButton
|
||||
className="mx_DialogSidebar_closeButton"
|
||||
data-test-id="dialog-sidebar-close"
|
||||
element="div"
|
||||
onClick={[MockFunction]}
|
||||
View List
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabindex="0"
|
||||
title="Close sidebar"
|
||||
>
|
||||
<div
|
||||
className="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-test-id="dialog-sidebar-close"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Close sidebar"
|
||||
>
|
||||
<div
|
||||
className="mx_DialogSidebar_closeButtonIcon"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ol
|
||||
className="mx_DialogSidebar_list"
|
||||
class="mx_DialogSidebar_list"
|
||||
>
|
||||
<li
|
||||
class="mx_BeaconListItem"
|
||||
>
|
||||
<span
|
||||
class="mx_BaseAvatar mx_BeaconListItem_avatar"
|
||||
role="presentation"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_initial"
|
||||
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_image"
|
||||
src=""
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_BeaconListItem_info"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
@alice:server.org
|
||||
</span>
|
||||
<span
|
||||
class="mx_BeaconStatus_expiryTime"
|
||||
>
|
||||
Live until 16:14
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BeaconListItem_interactions"
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<a
|
||||
data-test-id="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_BeaconListItem_lastUpdated"
|
||||
>
|
||||
Updated a few seconds ago
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DialogSidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_header"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
View List
|
||||
</h4>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="Close sidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ol
|
||||
class="mx_DialogSidebar_list"
|
||||
/>
|
||||
</div>
|
||||
</DialogSidebar>
|
||||
);
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mount } from "enzyme";
|
||||
import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import { mocked } from "jest-mock";
|
||||
import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
@ -23,7 +24,15 @@ import sanitizeHtml from "sanitize-html";
|
|||
|
||||
import SpotlightDialog, { Filter } from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { mkRoom, stubClient } from "../../../test-utils";
|
||||
|
||||
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||
startDmOnFirstMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
interface IUserChunkMember {
|
||||
user_id: string;
|
||||
|
@ -110,10 +119,23 @@ describe("Spotlight Dialog", () => {
|
|||
guest_can_join: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient({ rooms: [testPublicRoom], users: [testPerson] });
|
||||
});
|
||||
let testRoom: Room;
|
||||
let testLocalRoom: LocalRoom;
|
||||
|
||||
let mockedClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedClient = mockClient({ rooms: [testPublicRoom], users: [testPerson] });
|
||||
testRoom = mkRoom(mockedClient, "!test23:example.com");
|
||||
mocked(testRoom.getMyMembership).mockReturnValue("join");
|
||||
testLocalRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test23", mockedClient, mockedClient.getUserId());
|
||||
testLocalRoom.updateMyMembership("join");
|
||||
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom]);
|
||||
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
});
|
||||
describe("should apply filters supplied via props", () => {
|
||||
it("without filter", async () => {
|
||||
const wrapper = mount(
|
||||
|
@ -289,4 +311,38 @@ describe("Spotlight Dialog", () => {
|
|||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("searching for rooms", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
let options: ReactWrapper;
|
||||
|
||||
beforeAll(async () => {
|
||||
wrapper = mount(
|
||||
<SpotlightDialog
|
||||
initialText="test23"
|
||||
onFinished={() => null} />,
|
||||
);
|
||||
await act(async () => {
|
||||
await sleep(200);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
const content = wrapper.find("#mx_SpotlightDialog_content");
|
||||
options = content.find("div.mx_SpotlightDialog_option");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("should find Rooms", () => {
|
||||
expect(options.length).toBe(3);
|
||||
expect(options.first().text()).toContain(testRoom.name);
|
||||
});
|
||||
|
||||
it("should not find LocalRooms", () => {
|
||||
expect(options.length).toBe(3);
|
||||
expect(options.first().text()).not.toContain(testLocalRoom.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
131
test/components/views/messages/EncryptionEvent-test.tsx
Normal file
131
test/components/views/messages/EncryptionEvent-test.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import EncryptionEvent from "../../../../src/components/views/messages/EncryptionEvent";
|
||||
import { createTestClient, mkMessage } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import { LocalRoom } from '../../../../src/models/LocalRoom';
|
||||
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||
|
||||
const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => {
|
||||
render(<MatrixClientContext.Provider value={client}>
|
||||
<EncryptionEvent mxEvent={event} />
|
||||
</MatrixClientContext.Provider>);
|
||||
};
|
||||
|
||||
const checkTexts = (title: string, subTitle: string) => {
|
||||
screen.getByText(title);
|
||||
screen.getByText(subTitle);
|
||||
};
|
||||
|
||||
describe("EncryptionEvent", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const algorithm = "m.megolm.v1.aes-sha2";
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
client = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
event = mkMessage({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
});
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
});
|
||||
|
||||
describe("for an encrypted room", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content.algorithm = algorithm;
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||
const room = new Room(roomId, client, client.getUserId());
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts(
|
||||
"Encryption enabled",
|
||||
"Messages in this room are end-to-end encrypted. "
|
||||
+ "When people join, you can verify them in their profile, just tap on their avatar.",
|
||||
);
|
||||
});
|
||||
|
||||
describe("with same previous algorithm", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(event, "getPrevContent").mockReturnValue({
|
||||
algorithm: algorithm,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts(
|
||||
"Encryption enabled",
|
||||
"Some encryption parameters have been changed.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with unknown algorithm", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content.algorithm = "unknown";
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts("Encryption enabled", "Ignored attempt to disable encryption");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an unencrypted room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(false);
|
||||
renderEncryptionEvent(client, event);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an encrypted local room", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content.algorithm = algorithm;
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||
const localRoom = new LocalRoom(roomId, client, client.getUserId());
|
||||
mocked(client.getRoom).mockReturnValue(localRoom);
|
||||
renderEncryptionEvent(client, event);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
|
||||
});
|
||||
});
|
||||
});
|
78
test/components/views/rooms/NewRoomIntro-test.tsx
Normal file
78
test/components/views/rooms/NewRoomIntro-test.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@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 React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||
import { createTestClient } from "../../../test-utils";
|
||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import NewRoomIntro from "../../../../src/components/views/rooms/NewRoomIntro";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||
|
||||
const renderNewRoomIntro = (client: MatrixClient, room: Room|LocalRoom) => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}>
|
||||
<NewRoomIntro />
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("NewRoomIntro", () => {
|
||||
let client: MatrixClient;
|
||||
const roomId = "!room:example.com";
|
||||
const userId = "@user:example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
describe("for a DM Room", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||
renderNewRoomIntro(client, new Room(roomId, client, client.getUserId()));
|
||||
});
|
||||
|
||||
it("should render the expected intro", () => {
|
||||
const expected = `This is the beginning of your direct message history with ${userId}.`;
|
||||
screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a DM LocalRoom", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||
const localRoom = new LocalRoom(roomId, client, client.getUserId());
|
||||
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
|
||||
renderNewRoomIntro(client, localRoom);
|
||||
});
|
||||
|
||||
it("should render the expected intro", () => {
|
||||
const expected = `Send your first message to invite ${userId} to chat`;
|
||||
screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -127,7 +127,7 @@ describe('RoomHeader', () => {
|
|||
|
||||
it("hides call buttons when the room is tombstoned", () => {
|
||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||
const wrapper = render(room, {
|
||||
const wrapper = render(room, {}, {
|
||||
tombstone: mkEvent({
|
||||
event: true,
|
||||
type: "m.room.tombstone",
|
||||
|
@ -142,6 +142,30 @@ describe('RoomHeader', () => {
|
|||
expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(0);
|
||||
expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should render buttons if not passing showButtons (default true)", () => {
|
||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||
const wrapper = render(room);
|
||||
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should not render buttons if passing showButtons = false", () => {
|
||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||
const wrapper = render(room, { showButtons: false });
|
||||
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
|
||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||
const wrapper = render(room);
|
||||
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should not render the room options context menu if passing enableRoomOptionsMenu = false", () => {
|
||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||
const wrapper = render(room, { enableRoomOptionsMenu: false });
|
||||
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
interface IRoomCreationInfo {
|
||||
|
@ -185,25 +209,28 @@ function createRoom(info: IRoomCreationInfo) {
|
|||
return room;
|
||||
}
|
||||
|
||||
function render(room: Room, roomContext?: Partial<IRoomState>): ReactWrapper {
|
||||
function render(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): ReactWrapper {
|
||||
const props = {
|
||||
room,
|
||||
inRoom: true,
|
||||
onSearchClick: () => {},
|
||||
onInviteClick: null,
|
||||
onForgetClick: () => {},
|
||||
onCallPlaced: (_type) => { },
|
||||
onAppsClick: () => {},
|
||||
e2eStatus: E2EStatus.Normal,
|
||||
appsShown: true,
|
||||
searchInfo: {
|
||||
searchTerm: "",
|
||||
searchScope: SearchScope.Room,
|
||||
searchCount: 0,
|
||||
},
|
||||
...propsOverride,
|
||||
};
|
||||
|
||||
return mount((
|
||||
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
||||
<RoomHeader
|
||||
room={room}
|
||||
inRoom={true}
|
||||
onSearchClick={() => {}}
|
||||
onInviteClick={null}
|
||||
onForgetClick={() => {}}
|
||||
onCallPlaced={(_type) => { }}
|
||||
onAppsClick={() => {}}
|
||||
e2eStatus={E2EStatus.Normal}
|
||||
appsShown={true}
|
||||
searchInfo={{
|
||||
searchTerm: "",
|
||||
searchScope: SearchScope.Room,
|
||||
searchCount: 0,
|
||||
}}
|
||||
/>
|
||||
<RoomHeader {...props} />
|
||||
</RoomContext.Provider>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import request = require('request-promise-native');
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as url from "url";
|
||||
|
||||
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();
|
||||
const u = doc("input[name=u]").val();
|
||||
const h = doc("input[name=h]").val();
|
||||
const formAction = doc("form").attr("action");
|
||||
const absAction = url.resolve(consentUrl, formAction);
|
||||
await request.post(absAction).form({ v, u, h });
|
||||
};
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import request = require('request-promise-native');
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { RestSession } from './session';
|
||||
import { RestMultiSession } from './multi';
|
||||
|
||||
export interface Credentials {
|
||||
accessToken: string;
|
||||
homeServer: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
hsUrl: string;
|
||||
}
|
||||
|
||||
export class RestSessionCreator {
|
||||
constructor(private readonly hsUrl: string, private readonly regSecret: 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);
|
||||
}
|
||||
|
||||
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);
|
||||
return new RestSession(authResult);
|
||||
}
|
||||
|
||||
private async register(username: string, password: string): Promise<void> {
|
||||
// get a nonce
|
||||
const regUrl = `${this.hsUrl}/_synapse/admin/v1/register`;
|
||||
const nonceResp = await request.get({ uri: regUrl, json: true });
|
||||
|
||||
const mac = crypto.createHmac('sha1', this.regSecret).update(
|
||||
`${nonceResp.nonce}\0${username}\0${password}\0notadmin`,
|
||||
).digest('hex');
|
||||
|
||||
await request.post({
|
||||
uri: regUrl,
|
||||
json: true,
|
||||
body: {
|
||||
nonce: nonceResp.nonce,
|
||||
username,
|
||||
password,
|
||||
mac,
|
||||
admin: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async authenticate(username: string, password: string): Promise<Credentials> {
|
||||
const requestBody = {
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": username,
|
||||
},
|
||||
"password": password,
|
||||
};
|
||||
const url = `${this.hsUrl}/_matrix/client/r0/login`;
|
||||
const responseBody = await request.post({ url, json: true, body: requestBody });
|
||||
return {
|
||||
accessToken: responseBody.access_token,
|
||||
homeServer: responseBody.home_server,
|
||||
userId: responseBody.user_id,
|
||||
deviceId: responseBody.device_id,
|
||||
hsUrl: this.hsUrl,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 { Logger } from '../logger';
|
||||
import { RestSession } from "./session";
|
||||
import { RestRoom } from "./room";
|
||||
|
||||
export class RestMultiSession {
|
||||
readonly log: Logger;
|
||||
|
||||
constructor(public readonly sessions: RestSession[], groupName: string) {
|
||||
this.log = new Logger(groupName);
|
||||
}
|
||||
|
||||
public slice(groupName: string, start: number, end?: number): RestMultiSession {
|
||||
return new RestMultiSession(this.sessions.slice(start, end), groupName);
|
||||
}
|
||||
|
||||
public pop(userName: string): RestSession {
|
||||
const idx = this.sessions.findIndex((s) => s.userName() === userName);
|
||||
if (idx === -1) {
|
||||
throw new Error(`user ${userName} not found`);
|
||||
}
|
||||
const session = this.sessions.splice(idx, 1)[0];
|
||||
return session;
|
||||
}
|
||||
|
||||
public async setDisplayName(fn: (s: RestSession) => string): Promise<void> {
|
||||
this.log.step("set their display name");
|
||||
await Promise.all(this.sessions.map(async (s: RestSession) => {
|
||||
s.log.mute();
|
||||
await s.setDisplayName(fn(s));
|
||||
s.log.unmute();
|
||||
}));
|
||||
this.log.done();
|
||||
}
|
||||
|
||||
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();
|
||||
const room = await s.join(roomIdOrAlias);
|
||||
s.log.unmute();
|
||||
return room;
|
||||
}));
|
||||
this.log.done();
|
||||
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
|
||||
}
|
||||
|
||||
public room(roomIdOrAlias: string): RestMultiRoom {
|
||||
const rooms = this.sessions.map(s => s.room(roomIdOrAlias));
|
||||
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
|
||||
}
|
||||
}
|
||||
|
||||
class RestMultiRoom {
|
||||
constructor(public readonly rooms: RestRoom[], private readonly roomIdOrAlias: string,
|
||||
private readonly log: Logger) {}
|
||||
|
||||
public async talk(message: string): Promise<void> {
|
||||
this.log.step(`say "${message}" in ${this.roomIdOrAlias}`);
|
||||
await Promise.all(this.rooms.map(async (r: RestRoom) => {
|
||||
r.log.mute();
|
||||
await r.talk(message);
|
||||
r.log.unmute();
|
||||
}));
|
||||
this.log.done();
|
||||
}
|
||||
|
||||
public async leave() {
|
||||
this.log.step(`leave ${this.roomIdOrAlias}`);
|
||||
await Promise.all(this.rooms.map(async (r) => {
|
||||
r.log.mute();
|
||||
await r.leave();
|
||||
r.log.unmute();
|
||||
}));
|
||||
this.log.done();
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 uuidv4 = require('uuid/v4');
|
||||
|
||||
import { RestSession } from "./session";
|
||||
import { Logger } from "../logger";
|
||||
|
||||
/* no pun intended */
|
||||
export class RestRoom {
|
||||
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
|
||||
|
||||
async talk(message: string): Promise<string> {
|
||||
this.log.step(`says "${message}" in ${this.roomId}`);
|
||||
const txId = uuidv4();
|
||||
const { event_id: eventId } = await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
|
||||
"msgtype": "m.text",
|
||||
"body": message,
|
||||
});
|
||||
this.log.done();
|
||||
return eventId;
|
||||
}
|
||||
|
||||
async leave(): Promise<void> {
|
||||
this.log.step(`leaves ${this.roomId}`);
|
||||
await this.session.post(`/rooms/${this.roomId}/leave`);
|
||||
this.log.done();
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
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].slice(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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,18 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { range } from './util';
|
||||
import { signup } from './usecases/signup';
|
||||
import { toastScenarios } from './scenarios/toast';
|
||||
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 { RestSession } from "./rest/session";
|
||||
|
||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
||||
restCreator: RestSessionCreator): Promise<void> {
|
||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>): Promise<void> {
|
||||
let firstUser = true;
|
||||
async function createUser(username: string) {
|
||||
const session = await createSession(username);
|
||||
|
@ -44,15 +37,4 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
|||
const bob = await createUser("bob");
|
||||
|
||||
await toastScenarios(alice, bob);
|
||||
await e2eEncryptionScenarios(alice, bob);
|
||||
console.log("create REST users:");
|
||||
const charlies = await createRestUsers(restCreator);
|
||||
await lazyLoadingScenarios(alice, bob, charlies);
|
||||
}
|
||||
|
||||
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: RestSession) => `Charly #${s.userName().split('-')[1]}`);
|
||||
return charlies;
|
||||
}
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 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 { strict as assert } from 'assert';
|
||||
|
||||
import { ElementSession } from "../session";
|
||||
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 { measureStart, measureStop } from '../util';
|
||||
|
||||
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 verification
|
||||
bob.log.step(`starts SAS verification with ${alice.username}`);
|
||||
await measureStart(bob, "mx_VerifyE2EEUser");
|
||||
const bobSasPromise = startSasVerification(bob, alice.username);
|
||||
const aliceSasPromise = acceptSasVerification(alice, bob.username);
|
||||
// wait in parallel, so they don't deadlock on each other
|
||||
// the logs get a bit messy here, but that's fine enough for debugging (hopefully)
|
||||
const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]);
|
||||
assert.deepEqual(bobSas, aliceSas);
|
||||
await measureStop(bob, "mx_VerifyE2EEUser");
|
||||
bob.log.done(`done (match for ${bobSas.join(", ")})`);
|
||||
const aliceMessage = "Guess what I just heard?!";
|
||||
await sendMessage(alice, aliceMessage);
|
||||
await receiveMessage(bob, { sender: "alice", body: aliceMessage, encrypted: true });
|
||||
const bobMessage = "You've got to tell me!";
|
||||
await sendMessage(bob, bobMessage);
|
||||
await receiveMessage(alice, { sender: "bob", body: bobMessage, encrypted: true });
|
||||
await setupSecureBackup(alice);
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 { strict as assert } from 'assert';
|
||||
|
||||
import { delay } from '../util';
|
||||
import { join } from '../usecases/join';
|
||||
import { sendMessage } from '../usecases/send-message';
|
||||
import {
|
||||
checkTimelineContains,
|
||||
scrollToTimelineTop,
|
||||
} from '../usecases/timeline';
|
||||
import { createRoom } from '../usecases/create-room';
|
||||
import { getMembersInMemberlist } from '../usecases/memberlist';
|
||||
import { changeRoomSettings } from '../usecases/room-settings';
|
||||
import { RestMultiSession } from "../rest/multi";
|
||||
import { ElementSession } from "../session";
|
||||
|
||||
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);
|
||||
await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5);
|
||||
await checkPaginatedDisplayNames(alice, charly1to5);
|
||||
await checkMemberList(alice, charly1to5);
|
||||
await joinCharliesWhileAliceIsOffline(alice, charly6to10);
|
||||
await checkMemberList(alice, charly6to10);
|
||||
await charlies.room(alias).leave();
|
||||
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: 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"
|
||||
// so the charlies can join it.
|
||||
await bob.delay(500);
|
||||
const charlyMembers = await charlies.join(alias);
|
||||
await charlyMembers.talk(charlyMsg1);
|
||||
await charlyMembers.talk(charlyMsg2);
|
||||
bob.log.step("sends 20 messages").mute();
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
await sendMessage(bob, `I will only say this ${i} time(s)!`);
|
||||
}
|
||||
bob.log.unmute().done();
|
||||
await join(alice, alias);
|
||||
}
|
||||
|
||||
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
|
||||
const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => {
|
||||
return charlies.sessions.reduce((messages, charly) => {
|
||||
return messages.concat({
|
||||
sender: charly.displayName(),
|
||||
body: msgText,
|
||||
});
|
||||
}, messages);
|
||||
}, []);
|
||||
await checkTimelineContains(alice, expectedMessages, charlies.log.username);
|
||||
}
|
||||
|
||||
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"));
|
||||
assert(displayNames.includes("bob"));
|
||||
charlies.sessions.forEach((charly) => {
|
||||
assert(displayNames.includes(charly.displayName()),
|
||||
`${charly.displayName()} should be in the member list, ` +
|
||||
`only have ${displayNames}`);
|
||||
});
|
||||
alice.log.done();
|
||||
}
|
||||
|
||||
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) => {
|
||||
assert(!displayNames.includes(charly.displayName()),
|
||||
`${charly.displayName()} should not be in the member list, ` +
|
||||
`only have ${displayNames}`);
|
||||
});
|
||||
session.log.done();
|
||||
}
|
||||
|
||||
async function joinCharliesWhileAliceIsOffline(alice: ElementSession, charly6to10: RestMultiSession) {
|
||||
await alice.setOffline(true);
|
||||
await delay(1000);
|
||||
const members6to10 = await charly6to10.join(alias);
|
||||
const member6 = members6to10.rooms[0];
|
||||
member6.log.step("sends 20 messages").mute();
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
await member6.talk("where is charly?");
|
||||
}
|
||||
member6.log.unmute().done();
|
||||
const catchupPromise = alice.waitForNextSuccessfulSync();
|
||||
await alice.setOffline(false);
|
||||
await catchupPromise;
|
||||
await delay(2000);
|
||||
}
|
|
@ -118,24 +118,6 @@ export class ElementSession {
|
|||
return await this.page.$$(selector);
|
||||
}
|
||||
|
||||
/** wait for a /sync request started after this call that gets a 200 response */
|
||||
public async waitForNextSuccessfulSync(): Promise<void> {
|
||||
const syncUrls = [];
|
||||
function onRequest(request) {
|
||||
if (request.url().indexOf("/sync") !== -1) {
|
||||
syncUrls.push(request.url());
|
||||
}
|
||||
}
|
||||
|
||||
this.page.on('request', onRequest);
|
||||
|
||||
await this.page.waitForResponse((response) => {
|
||||
return syncUrls.includes(response.request().url()) && response.status() === 200;
|
||||
});
|
||||
|
||||
this.page.off('request', onRequest);
|
||||
}
|
||||
|
||||
public async waitNoSpinner(): Promise<void> {
|
||||
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
||||
}
|
||||
|
@ -152,13 +134,6 @@ export class ElementSession {
|
|||
return delay(ms);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
return this.browser.close();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import { ElementHandle } from "puppeteer";
|
||||
|
||||
import { openRoomSummaryCard } from "./rightpanel";
|
||||
|
@ -29,46 +28,6 @@ export async function openMemberInfo(session: ElementSession, name: String): Pro
|
|||
await matchingLabel.click();
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
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) => {
|
||||
return m.displayName === name;
|
||||
}).map((m) => m.label)[0];
|
||||
await matchingLabel.click();
|
||||
// click verify in member info
|
||||
const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify");
|
||||
await firstVerifyButton.click();
|
||||
// expect "Verify device" dialog and click "Begin Verification"
|
||||
const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title"));
|
||||
assert(dialogHeader, "Verify device");
|
||||
const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary");
|
||||
await beginVerificationButton.click();
|
||||
// get emoji SAS labels
|
||||
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)));
|
||||
console.log("my sas labels", sasLabels);
|
||||
|
||||
const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code");
|
||||
assert.strictEqual(dialogCodeFields.length, 2);
|
||||
const deviceId = await session.innerText(dialogCodeFields[0]);
|
||||
const deviceKey = await session.innerText(dialogCodeFields[1]);
|
||||
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();
|
||||
}
|
||||
|
||||
interface MemberName {
|
||||
label: ElementHandle;
|
||||
displayName: string;
|
||||
|
|
|
@ -20,32 +20,6 @@ import { ElementHandle } from "puppeteer";
|
|||
|
||||
import { ElementSession } from "../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 () => {
|
||||
let timedOut = false;
|
||||
let timeoutHandle = null;
|
||||
// set scrollTop to 0 in a loop and check every 50ms
|
||||
// if content became available (scrollTop not being 0 anymore),
|
||||
// assume everything is loaded after 3s
|
||||
do {
|
||||
const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel");
|
||||
if (timelineScrollView && timelineScrollView.scrollTop !== 0) {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
timeoutHandle = setTimeout(() => timedOut = true, 3000);
|
||||
timelineScrollView.scrollTop = 0;
|
||||
} else {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
} while (!timedOut);
|
||||
});
|
||||
});
|
||||
session.log.done();
|
||||
}
|
||||
|
||||
interface Message {
|
||||
sender: string;
|
||||
encrypted?: boolean;
|
||||
|
@ -79,41 +53,6 @@ export async function receiveMessage(session: ElementSession, expectedMessage: M
|
|||
session.log.done();
|
||||
}
|
||||
|
||||
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: 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: string, m) => {
|
||||
if (m.continuation) {
|
||||
m.sender = prevSender;
|
||||
return prevSender;
|
||||
} else {
|
||||
return m.sender;
|
||||
}
|
||||
}, "");
|
||||
|
||||
expectedMessages.forEach((expectedMessage) => {
|
||||
const foundMessage = timelineMessages.find((message) => {
|
||||
return message.sender === expectedMessage.sender &&
|
||||
message.body === expectedMessage.body;
|
||||
});
|
||||
try {
|
||||
assertMessage(foundMessage, expectedMessage);
|
||||
} catch (err) {
|
||||
console.log("timelineMessages", timelineMessages);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
session.log.done();
|
||||
}
|
||||
|
||||
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
|
||||
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
|
||||
assert.equal(foundMessage.body, expectedMessage.body);
|
||||
|
@ -127,10 +66,6 @@ function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
|
|||
return session.query(".mx_EventTile_last");
|
||||
}
|
||||
|
||||
function getAllEventTiles(session: ElementSession): Promise<ElementHandle[]> {
|
||||
return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
|
||||
}
|
||||
|
||||
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
|
||||
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
|
||||
const className: string = await (await eventTile.getProperty("className")).jsonValue();
|
||||
|
|
|
@ -20,14 +20,6 @@ import { padEnd } from "lodash";
|
|||
|
||||
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));
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
export const delay = function(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
|
|
@ -19,7 +19,6 @@ import { Command } from "commander";
|
|||
|
||||
import { ElementSession } from './src/session';
|
||||
import { scenario } from './src/scenario';
|
||||
import { RestSessionCreator } from './src/rest/creator';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
|
@ -54,12 +53,7 @@ async function runTests() {
|
|||
options['executablePath'] = path;
|
||||
}
|
||||
|
||||
const restCreator = new RestSessionCreator(
|
||||
hsUrl,
|
||||
program.opts().registrationSharedSecret,
|
||||
);
|
||||
|
||||
async function createSession(username) {
|
||||
async function createSession(username: string) {
|
||||
const session = await ElementSession.create(
|
||||
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
|
||||
);
|
||||
|
@ -69,7 +63,7 @@ async function runTests() {
|
|||
|
||||
let failure = false;
|
||||
try {
|
||||
await scenario(createSession, restCreator);
|
||||
await scenario(createSession);
|
||||
} catch (err) {
|
||||
failure = true;
|
||||
console.log('failure: ', err);
|
||||
|
|
88
test/stores/TypingStore-test.ts
Normal file
88
test/stores/TypingStore-test.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import TypingStore from "../../src/stores/TypingStore";
|
||||
import { LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("../../src/settings/SettingsStore", () => ({
|
||||
getValue: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("TypingStore", () => {
|
||||
let typingStore: TypingStore;
|
||||
let mockClient: MatrixClient;
|
||||
const settings = {
|
||||
"sendTypingNotifications": true,
|
||||
"feature_thread": false,
|
||||
};
|
||||
const roomId = "!test:example.com";
|
||||
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
|
||||
|
||||
beforeEach(() => {
|
||||
typingStore = new TypingStore();
|
||||
mockClient = {
|
||||
sendTyping: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
MatrixClientPeg.get = () => mockClient;
|
||||
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
|
||||
return settings[setting];
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSelfTyping", () => {
|
||||
it("shouldn't do anything for a local room", () => {
|
||||
typingStore.setSelfTyping(localRoomId, null, true);
|
||||
expect(mockClient.sendTyping).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("in typing state true", () => {
|
||||
beforeEach(() => {
|
||||
typingStore.setSelfTyping(roomId, null, true);
|
||||
});
|
||||
|
||||
it("should change to false when setting false", () => {
|
||||
typingStore.setSelfTyping(roomId, null, false);
|
||||
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, false, 30000);
|
||||
});
|
||||
|
||||
it("should change to true when setting true", () => {
|
||||
typingStore.setSelfTyping(roomId, null, true);
|
||||
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in typing state false", () => {
|
||||
beforeEach(() => {
|
||||
typingStore.setSelfTyping(roomId, null, false);
|
||||
});
|
||||
|
||||
it("shouldn't change when setting false", () => {
|
||||
typingStore.setSelfTyping(roomId, null, false);
|
||||
expect(mockClient.sendTyping).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should change to true when setting true", () => {
|
||||
typingStore.setSelfTyping(roomId, null, true);
|
||||
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -205,7 +205,11 @@ export const makeRoomWithBeacons = (
|
|||
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
|
||||
const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event)));
|
||||
if (locationEvents) {
|
||||
beacons.forEach(beacon => beacon.addLocations(locationEvents));
|
||||
beacons.forEach(beacon => {
|
||||
// this filtering happens in roomState, which is bypassed here
|
||||
const validLocationEvents = locationEvents?.filter(event => event.getSender() === beacon.beaconInfoOwner);
|
||||
beacon.addLocations(validLocationEvents);
|
||||
});
|
||||
}
|
||||
return beacons;
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
IEventRelation,
|
||||
IUnsigned,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
|
||||
import dis from '../../src/dispatcher/dispatcher';
|
||||
|
@ -389,6 +390,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
|
|||
removeListener: jest.fn(),
|
||||
getDMInviter: jest.fn(),
|
||||
name,
|
||||
normalizedName: normalize(name || ""),
|
||||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
|
|
|
@ -35,7 +35,8 @@ export function untilDispatch(waitForAction: DispatcherAction): Promise<ActionPa
|
|||
});
|
||||
}
|
||||
|
||||
const findByAttr = (attr: string) => (component: ReactWrapper, value: string) => component.find(`[${attr}="${value}"]`);
|
||||
export const findByAttr = (attr: string) => (component: ReactWrapper, value: string) =>
|
||||
component.find(`[${attr}="${value}"]`);
|
||||
export const findByTestId = findByAttr('data-test-id');
|
||||
export const findById = findByAttr('id');
|
||||
export const findByAriaLabel = findByAttr('aria-label');
|
||||
|
|
52
test/utils/localRoom/isLocalRoom-test.ts
Normal file
52
test/utils/localRoom/isLocalRoom-test.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||
import { isLocalRoom } from "../../../src/utils/localRoom/isLocalRoom";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
|
||||
describe("isLocalRoom", () => {
|
||||
let room: Room;
|
||||
let localRoom: LocalRoom;
|
||||
|
||||
beforeEach(() => {
|
||||
const client = createTestClient();
|
||||
room = new Room("!room:example.com", client, client.getUserId());
|
||||
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, client.getUserId());
|
||||
});
|
||||
|
||||
it("should return false for null", () => {
|
||||
expect(isLocalRoom(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a Room", () => {
|
||||
expect(isLocalRoom(room)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a non-local room ID", () => {
|
||||
expect(isLocalRoom(room.roomId)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for LocalRoom", () => {
|
||||
expect(isLocalRoom(localRoom)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for local room ID", () => {
|
||||
expect(isLocalRoom(LOCAL_ROOM_ID_PREFIX + "test")).toBe(true);
|
||||
});
|
||||
});
|
24
yarn.lock
24
yarn.lock
|
@ -2853,10 +2853,12 @@ balanced-match@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||
|
||||
base-x@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
|
||||
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
|
||||
base-x@^3.0.2:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
|
||||
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
|
@ -2974,12 +2976,12 @@ browserslist@^4.20.2, browserslist@^4.21.1:
|
|||
node-releases "^2.0.5"
|
||||
update-browserslist-db "^1.0.4"
|
||||
|
||||
bs58@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
|
||||
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
|
||||
bs58@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
|
||||
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
|
||||
dependencies:
|
||||
base-x "^4.0.0"
|
||||
base-x "^3.0.2"
|
||||
|
||||
bser@2.1.1:
|
||||
version "2.1.1"
|
||||
|
@ -3539,7 +3541,7 @@ csstype@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
cypress-real-events@^1.7.0:
|
||||
cypress-real-events@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
||||
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==
|
||||
|
@ -7284,7 +7286,7 @@ p-map@^4.0.0:
|
|||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
|
||||
p-retry@4:
|
||||
p-retry@^4.5.0:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
|
||||
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
|
||||
|
|
Loading…
Reference in a new issue