Merge branch 'develop' into johannes/find-myself

This commit is contained in:
Johannes Marbach 2023-02-10 08:26:16 +01:00
commit 6ee6accfc6
16 changed files with 392 additions and 774 deletions

View file

@ -152,7 +152,7 @@
"@types/escape-html": "^1.0.1", "@types/escape-html": "^1.0.1",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^11.0.0",
"@types/geojson": "^7946.0.8", "@types/geojson": "^7946.0.8",
"@types/jest": "^29.2.1", "@types/jest": "^29.2.1",
"@types/katex": "^0.14.0", "@types/katex": "^0.14.0",

View file

@ -146,7 +146,7 @@ export default class PasswordReset {
err.message = _t("Failed to verify email address: make sure you clicked the link in the email"); err.message = _t("Failed to verify email address: make sure you clicked the link in the email");
} else if (err.httpStatus === 404) { } else if (err.httpStatus === 404) {
err.message = _t( err.message = _t(
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.", "Your email address does not appear to be associated with a Matrix ID on this homeserver.",
); );
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`; err.message += ` (Status ${err.httpStatus})`;

View file

@ -63,7 +63,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
inviteOption = ( inviteOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="invite-option" data-testid="invite-option"
className="mx_SpacePanel_contextMenu_inviteButton" className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite" iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite")} label={_t("Invite")}
@ -85,7 +85,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
settingsOption = ( settingsOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="settings-option" data-testid="settings-option"
iconClassName="mx_SpacePanel_iconSettings" iconClassName="mx_SpacePanel_iconSettings"
label={_t("Settings")} label={_t("Settings")}
onClick={onSettingsClick} onClick={onSettingsClick}
@ -102,7 +102,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
leaveOption = ( leaveOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="leave-option" data-testid="leave-option"
iconClassName="mx_SpacePanel_iconLeave" iconClassName="mx_SpacePanel_iconLeave"
className="mx_IconizedContextMenu_option_red" className="mx_IconizedContextMenu_option_red"
label={_t("Leave space")} label={_t("Leave space")}
@ -172,12 +172,12 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
newRoomSection = ( newRoomSection = (
<> <>
<div data-test-id="add-to-space-header" className="mx_SpacePanel_contextMenu_separatorLabel"> <div data-testid="add-to-space-header" className="mx_SpacePanel_contextMenu_separatorLabel">
{_t("Add")} {_t("Add")}
</div> </div>
{canAddRooms && ( {canAddRooms && (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="new-room-option" data-testid="new-room-option"
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
label={_t("Room")} label={_t("Room")}
onClick={onNewRoomClick} onClick={onNewRoomClick}
@ -185,7 +185,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
)} )}
{canAddVideoRooms && ( {canAddVideoRooms && (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="new-video-room-option" data-testid="new-video-room-option"
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
label={_t("Video room")} label={_t("Video room")}
onClick={onNewVideoRoomClick} onClick={onNewVideoRoomClick}
@ -195,7 +195,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
)} )}
{canAddSubSpaces && ( {canAddSubSpaces && (
<IconizedContextMenuOption <IconizedContextMenuOption
data-test-id="new-subspace-option" data-testid="new-subspace-option"
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
label={_t("Space")} label={_t("Space")}
onClick={onNewSubspaceClick} onClick={onNewSubspaceClick}

View file

@ -191,16 +191,19 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number { public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
const { model } = this.props; const { model } = this.props;
const range = model.startRange(caretPosition); const range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition, // expand range max 9 characters backwards from caretPosition,
// as a space to look for an emoticon // as a space to look for an emoticon
let n = 8; let n = 9;
range.expandBackwardsWhile((index, offset) => { range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index]; const part = model.parts[index];
n -= 1; n -= 1;
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type); return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
}); });
const emoticonMatch = regex.exec(range.text); const emoticonMatch = regex.exec(range.text);
if (emoticonMatch) { // ignore matches at start of proper substrings
// so xd will not match if the string was "mixd 123456"
// and we are lookinh at xd 123456 part of the string
if (emoticonMatch && (n >= 0 || emoticonMatch.index !== 0)) {
const query = emoticonMatch[1].replace("-", ""); const query = emoticonMatch[1].replace("-", "");
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p // try both exact match and lower-case, this means that xd won't match xD but :P will match :p
const data = EMOTICON_TO_EMOJI.get(query) || EMOTICON_TO_EMOJI.get(query.toLowerCase()); const data = EMOTICON_TO_EMOJI.get(query) || EMOTICON_TO_EMOJI.get(query.toLowerCase());

View file

@ -73,7 +73,7 @@ const securityCardContent: Record<
title: _t("Unverified session"), title: _t("Unverified session"),
description: ( description: (
<> <>
<p>{_t(`This session doesn't support encryption, so it can't be verified.`)}</p> <p>{_t(`This session doesn't support encryption and thus can't be verified.`)}</p>
<p> <p>
{_t( {_t(
`You won't be able to participate in rooms where encryption is enabled when using this session.`, `You won't be able to participate in rooms where encryption is enabled when using this session.`,

View file

@ -117,7 +117,7 @@
"%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again",
"Unable to enable Notifications": "Unable to enable Notifications", "Unable to enable Notifications": "Unable to enable Notifications",
"This email address was not found": "This email address was not found", "This email address was not found": "This email address was not found",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", "Your email address does not appear to be associated with a Matrix ID on this homeserver.": "Your email address does not appear to be associated with a Matrix ID on this homeserver.",
"United Kingdom": "United Kingdom", "United Kingdom": "United Kingdom",
"United States": "United States", "United States": "United States",
"Afghanistan": "Afghanistan", "Afghanistan": "Afghanistan",
@ -1826,7 +1826,7 @@
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
"Unverified session": "Unverified session", "Unverified session": "Unverified session",
"This session doesn't support encryption, so it can't be verified.": "This session doesn't support encryption, so it can't be verified.", "This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.",
"You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.", "You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.",
"For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.", "For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.",
"Inactive sessions": "Inactive sessions", "Inactive sessions": "Inactive sessions",
@ -1842,7 +1842,6 @@
"Your current session is ready for secure messaging.": "Your current session is ready for secure messaging.", "Your current session is ready for secure messaging.": "Your current session is ready for secure messaging.",
"This session is ready for secure messaging.": "This session is ready for secure messaging.", "This session is ready for secure messaging.": "This session is ready for secure messaging.",
"Verified session": "Verified session", "Verified session": "Verified session",
"This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.",
"Verify your current session for enhanced secure messaging.": "Verify your current session for enhanced secure messaging.", "Verify your current session for enhanced secure messaging.": "Verify your current session for enhanced secure messaging.",
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
"Verify session": "Verify session", "Verify session": "Verify session",

View file

@ -62,12 +62,12 @@ interface IStickyRoom {
*/ */
export class Algorithm extends EventEmitter { export class Algorithm extends EventEmitter {
private _cachedRooms: ITagMap = {}; private _cachedRooms: ITagMap = {};
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room private _cachedStickyRooms: ITagMap | null = {}; // a clone of the _cachedRooms, with the sticky room
private _stickyRoom: IStickyRoom = null; private _stickyRoom: IStickyRoom | null = null;
private _lastStickyRoom: IStickyRoom = null; // only not-null when changing the sticky room private _lastStickyRoom: IStickyRoom | null = null; // only not-null when changing the sticky room
private sortAlgorithms: ITagSortingMap; private sortAlgorithms: ITagSortingMap | null = null;
private listAlgorithms: IListOrderingMap; private listAlgorithms: IListOrderingMap | null = null;
private algorithms: IOrderingAlgorithmMap; private algorithms: IOrderingAlgorithmMap | null = null;
private rooms: Room[] = []; private rooms: Room[] = [];
private roomIdsToTags: { private roomIdsToTags: {
[roomId: string]: TagID[]; [roomId: string]: TagID[];
@ -86,7 +86,7 @@ export class Algorithm extends EventEmitter {
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls); CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
} }
public get stickyRoom(): Room { public get stickyRoom(): Room | null {
return this._stickyRoom ? this._stickyRoom.room : null; return this._stickyRoom ? this._stickyRoom.room : null;
} }
@ -124,7 +124,7 @@ export class Algorithm extends EventEmitter {
} }
} }
public getTagSorting(tagId: TagID): SortAlgorithm { public getTagSorting(tagId: TagID): SortAlgorithm | null {
if (!this.sortAlgorithms) return null; if (!this.sortAlgorithms) return null;
return this.sortAlgorithms[tagId]; return this.sortAlgorithms[tagId];
} }
@ -132,6 +132,8 @@ export class Algorithm extends EventEmitter {
public setTagSorting(tagId: TagID, sort: SortAlgorithm): void { public setTagSorting(tagId: TagID, sort: SortAlgorithm): void {
if (!tagId) throw new Error("Tag ID must be defined"); if (!tagId) throw new Error("Tag ID must be defined");
if (!sort) throw new Error("Algorithm must be defined"); if (!sort) throw new Error("Algorithm must be defined");
if (!this.sortAlgorithms) throw new Error("this.sortAlgorithms must be defined before calling setTagSorting");
if (!this.algorithms) throw new Error("this.algorithms must be defined before calling setTagSorting");
this.sortAlgorithms[tagId] = sort; this.sortAlgorithms[tagId] = sort;
const algorithm: OrderingAlgorithm = this.algorithms[tagId]; const algorithm: OrderingAlgorithm = this.algorithms[tagId];
@ -141,7 +143,7 @@ export class Algorithm extends EventEmitter {
this.recalculateActiveCallRooms(tagId); this.recalculateActiveCallRooms(tagId);
} }
public getListOrdering(tagId: TagID): ListAlgorithm { public getListOrdering(tagId: TagID): ListAlgorithm | null {
if (!this.listAlgorithms) return null; if (!this.listAlgorithms) return null;
return this.listAlgorithms[tagId]; return this.listAlgorithms[tagId];
} }
@ -149,6 +151,9 @@ export class Algorithm extends EventEmitter {
public setListOrdering(tagId: TagID, order: ListAlgorithm): void { public setListOrdering(tagId: TagID, order: ListAlgorithm): void {
if (!tagId) throw new Error("Tag ID must be defined"); if (!tagId) throw new Error("Tag ID must be defined");
if (!order) throw new Error("Algorithm must be defined"); if (!order) throw new Error("Algorithm must be defined");
if (!this.sortAlgorithms) throw new Error("this.sortAlgorithms must be defined before calling setListOrdering");
if (!this.listAlgorithms) throw new Error("this.listAlgorithms must be defined before calling setListOrdering");
if (!this.algorithms) throw new Error("this.algorithms must be defined before calling setListOrdering");
this.listAlgorithms[tagId] = order; this.listAlgorithms[tagId] = order;
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]); const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
@ -160,12 +165,12 @@ export class Algorithm extends EventEmitter {
this.recalculateActiveCallRooms(tagId); this.recalculateActiveCallRooms(tagId);
} }
private updateStickyRoom(val: Room): void { private updateStickyRoom(val: Room | null): void {
this.doUpdateStickyRoom(val); this.doUpdateStickyRoom(val);
this._lastStickyRoom = null; // clear to indicate we're done changing this._lastStickyRoom = null; // clear to indicate we're done changing
} }
private doUpdateStickyRoom(val: Room): void { private doUpdateStickyRoom(val: Room | null): void {
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") { if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
// no-op sticky rooms for spaces - they're effectively virtual rooms // no-op sticky rooms for spaces - they're effectively virtual rooms
val = null; val = null;
@ -237,6 +242,10 @@ export class Algorithm extends EventEmitter {
// Lie to the algorithm and remove the room from it's field of view // Lie to the algorithm and remove the room from it's field of view
this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved); this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
// handleRoomUpdate may have modified this._stickyRoom. Convince the
// compiler of this fact.
this._stickyRoom = this.stickyRoomMightBeModified();
// Check for tag & position changes while we're here. We also check the room to ensure // Check for tag & position changes while we're here. We also check the room to ensure
// it is still the same room. // it is still the same room.
if (this._stickyRoom) { if (this._stickyRoom) {
@ -284,6 +293,13 @@ export class Algorithm extends EventEmitter {
this.emit(LIST_UPDATED_EVENT); this.emit(LIST_UPDATED_EVENT);
} }
/**
* Hack to prevent Typescript claiming this._stickyRoom is always null.
*/
private stickyRoomMightBeModified(): IStickyRoom | null {
return this._stickyRoom;
}
private onActiveCalls = (): void => { private onActiveCalls = (): void => {
// In case we're unsticking a room, sort it back into natural order // In case we're unsticking a room, sort it back into natural order
this.recalculateStickyRoom(); this.recalculateStickyRoom();
@ -310,7 +326,7 @@ export class Algorithm extends EventEmitter {
* the call. * the call.
* @param updatedTag The tag that was updated, if possible. * @param updatedTag The tag that was updated, if possible.
*/ */
protected recalculateStickyRoom(updatedTag: TagID = null): void { protected recalculateStickyRoom(updatedTag: TagID | null = null): void {
// 🐉 Here be dragons. // 🐉 Here be dragons.
// This function does far too much for what it should, and is called by many places. // This function does far too much for what it should, and is called by many places.
// Not only is this responsible for ensuring the sticky room is held in place at all // Not only is this responsible for ensuring the sticky room is held in place at all
@ -336,14 +352,16 @@ export class Algorithm extends EventEmitter {
if (updatedTag) { if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure // Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date. // our cache is up to date.
if (this._cachedStickyRooms) {
this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone
} }
}
// Now try to insert the sticky room, if we need to. // Now try to insert the sticky room, if we need to.
// We need to if there's no updated tag (we regenned the whole cache) or if the tag // We need to if there's no updated tag (we regenned the whole cache) or if the tag
// we might have updated from the cache is also our sticky room. // we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom; const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) { if (sticky && (!updatedTag || updatedTag === sticky.tag) && this._cachedStickyRooms) {
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room); this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
} }
@ -362,7 +380,7 @@ export class Algorithm extends EventEmitter {
* *
* @param updatedTag The tag that was updated, if possible. * @param updatedTag The tag that was updated, if possible.
*/ */
protected recalculateActiveCallRooms(updatedTag: TagID = null): void { protected recalculateActiveCallRooms(updatedTag: TagID | null = null): void {
if (!updatedTag) { if (!updatedTag) {
// Assume all tags need updating // Assume all tags need updating
// We're not modifying the map here, so can safely rely on the cached values // We're not modifying the map here, so can safely rely on the cached values
@ -379,7 +397,7 @@ export class Algorithm extends EventEmitter {
if (CallStore.instance.activeCalls.size) { if (CallStore.instance.activeCalls.size) {
// We operate on the sticky rooms map // We operate on the sticky rooms map
if (!this._cachedStickyRooms) this.initCachedStickyRooms(); if (!this._cachedStickyRooms) this.initCachedStickyRooms();
const rooms = this._cachedStickyRooms[updatedTag]; const rooms = this._cachedStickyRooms![updatedTag];
const activeRoomIds = new Set([...CallStore.instance.activeCalls].map((call) => call.roomId)); const activeRoomIds = new Set([...CallStore.instance.activeCalls].map((call) => call.roomId));
const activeRooms: Room[] = []; const activeRooms: Room[] = [];
@ -390,7 +408,7 @@ export class Algorithm extends EventEmitter {
} }
// Stick rooms with active calls to the top // Stick rooms with active calls to the top
this._cachedStickyRooms[updatedTag] = [...activeRooms, ...inactiveRooms]; this._cachedStickyRooms![updatedTag] = [...activeRooms, ...inactiveRooms];
} }
} }
@ -638,7 +656,7 @@ export class Algorithm extends EventEmitter {
} }
// Like above, update the reference to the sticky room if we need to // Like above, update the reference to the sticky room if we need to
if (hasTags && isSticky) { if (hasTags && isSticky && this._stickyRoom) {
// Go directly in and set the sticky room's new reference, being careful not // Go directly in and set the sticky room's new reference, being careful not
// to trigger a sticky room update ourselves. // to trigger a sticky room update ourselves.
this._stickyRoom.room = room; this._stickyRoom.room = room;

View file

@ -113,10 +113,10 @@ describe("RoomNotifs test", () => {
event: true, event: true,
type: "m.room.create", type: "m.room.create",
room: ROOM_ID, room: ROOM_ID,
user: client.getUserId()!, user: "@zoe:localhost",
content: { content: {
...(predecessorId ? { predecessor: { room_id: predecessorId, event_id: "$someevent" } } : {}), ...(predecessorId ? { predecessor: { room_id: predecessorId, event_id: "$someevent" } } : {}),
creator: client.getUserId(), creator: "@zoe:localhost",
room_version: "5", room_version: "5",
}, },
ts: Date.now(), ts: Date.now(),
@ -128,7 +128,7 @@ describe("RoomNotifs test", () => {
event: true, event: true,
type: EventType.RoomPredecessor, type: EventType.RoomPredecessor,
room: ROOM_ID, room: ROOM_ID,
user: client.getUserId()!, user: "@zoe:localhost",
skey: "", skey: "",
content: { content: {
predecessor_room_id: predecessorId, predecessor_room_id: predecessorId,

View file

@ -15,16 +15,14 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
// eslint-disable-next-line deprecate/import import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { mount } from "enzyme"; import { Mocked, mocked } from "jest-mock";
import { Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { act } from "react-dom/test-utils";
import "focus-visible"; // to fix context menus import "focus-visible"; // to fix context menus
import { prettyDOM, render, RenderResult, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import SpaceContextMenu from "../../../../src/components/views/context_menus/SpaceContextMenu"; import SpaceContextMenu from "../../../../src/components/views/context_menus/SpaceContextMenu";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { findByTestId } from "../../../test-utils";
import { import {
shouldShowSpaceSettings, shouldShowSpaceSettings,
showCreateNewRoom, showCreateNewRoom,
@ -55,9 +53,11 @@ jest.mock("../../../../src/utils/leave-behaviour", () => ({
describe("<SpaceContextMenu />", () => { describe("<SpaceContextMenu />", () => {
const userId = "@test:server"; const userId = "@test:server";
const mockClient = { const mockClient = {
getUserId: jest.fn().mockReturnValue(userId), getUserId: jest.fn().mockReturnValue(userId),
}; } as unknown as Mocked<MatrixClient>;
const makeMockSpace = (props = {}) => const makeMockSpace = (props = {}) =>
({ ({
name: "test space", name: "test space",
@ -70,17 +70,18 @@ describe("<SpaceContextMenu />", () => {
getMyMembership: jest.fn(), getMyMembership: jest.fn(),
...props, ...props,
} as unknown as Room); } as unknown as Room);
const defaultProps = { const defaultProps = {
space: makeMockSpace(), space: makeMockSpace(),
onFinished: jest.fn(), onFinished: jest.fn(),
}; };
const getComponent = (props = {}) =>
mount(<SpaceContextMenu {...defaultProps} {...props} />, { const renderComponent = (props = {}): RenderResult =>
wrappingComponent: MatrixClientContext.Provider, render(
wrappingComponentProps: { <MatrixClientContext.Provider value={mockClient}>
value: mockClient, <SpaceContextMenu {...defaultProps} {...props} />
}, </MatrixClientContext.Provider>,
}); );
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
@ -88,134 +89,135 @@ describe("<SpaceContextMenu />", () => {
}); });
it("renders menu correctly", () => { it("renders menu correctly", () => {
const component = getComponent(); const { baseElement } = renderComponent();
expect(component).toMatchSnapshot(); expect(prettyDOM(baseElement)).toMatchSnapshot();
}); });
it("renders invite option when space is public", () => { it("renders invite option when space is public", () => {
const space = makeMockSpace({ const space = makeMockSpace({
getJoinRule: jest.fn().mockReturnValue("public"), getJoinRule: jest.fn().mockReturnValue("public"),
}); });
const component = getComponent({ space }); renderComponent({ space });
expect(findByTestId(component, "invite-option").length).toBeTruthy(); expect(screen.getByTestId("invite-option")).toBeInTheDocument();
}); });
it("renders invite option when user is has invite rights for space", () => { it("renders invite option when user is has invite rights for space", () => {
const space = makeMockSpace({ const space = makeMockSpace({
canInvite: jest.fn().mockReturnValue(true), canInvite: jest.fn().mockReturnValue(true),
}); });
const component = getComponent({ space }); renderComponent({ space });
expect(space.canInvite).toHaveBeenCalledWith(userId); expect(space.canInvite).toHaveBeenCalledWith(userId);
expect(findByTestId(component, "invite-option").length).toBeTruthy(); expect(screen.getByTestId("invite-option")).toBeInTheDocument();
}); });
it("opens invite dialog when invite option is clicked", () => {
it("opens invite dialog when invite option is clicked", async () => {
const space = makeMockSpace({ const space = makeMockSpace({
getJoinRule: jest.fn().mockReturnValue("public"), getJoinRule: jest.fn().mockReturnValue("public"),
}); });
const onFinished = jest.fn(); const onFinished = jest.fn();
const component = getComponent({ space, onFinished }); renderComponent({ space, onFinished });
act(() => { await userEvent.click(screen.getByTestId("invite-option"));
findByTestId(component, "invite-option").at(0).simulate("click");
});
expect(showSpaceInvite).toHaveBeenCalledWith(space); expect(showSpaceInvite).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
}); });
it("renders space settings option when user has rights", () => { it("renders space settings option when user has rights", () => {
mocked(shouldShowSpaceSettings).mockReturnValue(true); mocked(shouldShowSpaceSettings).mockReturnValue(true);
const component = getComponent(); renderComponent();
expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space); expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
expect(findByTestId(component, "settings-option").length).toBeTruthy(); expect(screen.getByTestId("settings-option")).toBeInTheDocument();
}); });
it("opens space settings when space settings option is clicked", () => {
it("opens space settings when space settings option is clicked", async () => {
mocked(shouldShowSpaceSettings).mockReturnValue(true); mocked(shouldShowSpaceSettings).mockReturnValue(true);
const onFinished = jest.fn(); const onFinished = jest.fn();
const component = getComponent({ onFinished }); renderComponent({ onFinished });
act(() => { await userEvent.click(screen.getByTestId("settings-option"));
findByTestId(component, "settings-option").at(0).simulate("click");
});
expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space); expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
}); });
it("renders leave option when user does not have rights to see space settings", () => { it("renders leave option when user does not have rights to see space settings", () => {
const component = getComponent(); renderComponent();
expect(findByTestId(component, "leave-option").length).toBeTruthy(); expect(screen.getByTestId("leave-option")).toBeInTheDocument();
}); });
it("leaves space when leave option is clicked", () => {
it("leaves space when leave option is clicked", async () => {
const onFinished = jest.fn(); const onFinished = jest.fn();
const component = getComponent({ onFinished }); renderComponent({ onFinished });
act(() => { await userEvent.click(screen.getByTestId("leave-option"));
findByTestId(component, "leave-option").at(0).simulate("click");
});
expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space); expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space);
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
}); });
describe("add children section", () => { describe("add children section", () => {
const space = makeMockSpace(); const space = makeMockSpace();
beforeEach(() => { beforeEach(() => {
// set space to allow adding children to space // set space to allow adding children to space
mocked(space.currentState.maySendStateEvent).mockReturnValue(true); mocked(space.currentState.maySendStateEvent).mockReturnValue(true);
mocked(shouldShowComponent).mockReturnValue(true); mocked(shouldShowComponent).mockReturnValue(true);
}); });
it("does not render section when user does not have permission to add children", () => { it("does not render section when user does not have permission to add children", () => {
mocked(space.currentState.maySendStateEvent).mockReturnValue(false); mocked(space.currentState.maySendStateEvent).mockReturnValue(false);
const component = getComponent({ space }); renderComponent({ space });
expect(findByTestId(component, "add-to-space-header").length).toBeFalsy(); expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
expect(findByTestId(component, "new-room-option").length).toBeFalsy(); expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
}); });
it("does not render section when UIComponent customisations disable room and space creation", () => { it("does not render section when UIComponent customisations disable room and space creation", () => {
mocked(shouldShowComponent).mockReturnValue(false); mocked(shouldShowComponent).mockReturnValue(false);
const component = getComponent({ space }); renderComponent({ space });
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms); expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms);
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces); expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
expect(findByTestId(component, "add-to-space-header").length).toBeFalsy(); expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
expect(findByTestId(component, "new-room-option").length).toBeFalsy(); expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
}); });
it("renders section with add room button when UIComponent customisation allows CreateRoom", () => { it("renders section with add room button when UIComponent customisation allows CreateRoom", () => {
// only allow CreateRoom // only allow CreateRoom
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms); mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms);
const component = getComponent({ space }); renderComponent({ space });
expect(findByTestId(component, "add-to-space-header").length).toBeTruthy(); expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
expect(findByTestId(component, "new-room-option").length).toBeTruthy(); expect(screen.getByTestId("new-room-option")).toBeInTheDocument();
expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
}); });
it("renders section with add space button when UIComponent customisation allows CreateSpace", () => { it("renders section with add space button when UIComponent customisation allows CreateSpace", () => {
// only allow CreateSpaces // only allow CreateSpaces
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces); mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces);
const component = getComponent({ space }); renderComponent({ space });
expect(findByTestId(component, "add-to-space-header").length).toBeTruthy(); expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
expect(findByTestId(component, "new-room-option").length).toBeFalsy(); expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
expect(findByTestId(component, "new-subspace-option").length).toBeTruthy(); expect(screen.getByTestId("new-subspace-option")).toBeInTheDocument();
}); });
it("opens create room dialog on add room button click", () => { it("opens create room dialog on add room button click", async () => {
const onFinished = jest.fn(); const onFinished = jest.fn();
const component = getComponent({ space, onFinished }); renderComponent({ space, onFinished });
act(() => { await userEvent.click(screen.getByTestId("new-room-option"));
findByTestId(component, "new-room-option").at(0).simulate("click");
});
expect(showCreateNewRoom).toHaveBeenCalledWith(space); expect(showCreateNewRoom).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
}); });
it("opens create space dialog on add space button click", () => {
const onFinished = jest.fn();
const component = getComponent({ space, onFinished });
act(() => { it("opens create space dialog on add space button click", async () => {
findByTestId(component, "new-subspace-option").at(0).simulate("click"); const onFinished = jest.fn();
}); renderComponent({ space, onFinished });
await userEvent.click(screen.getByTestId("new-subspace-option"));
expect(showCreateNewSubspace).toHaveBeenCalledWith(space); expect(showCreateNewSubspace).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
}); });

View file

@ -1,500 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpaceContextMenu /> renders menu correctly 1`] = ` exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
<SpaceContextMenu "<body>
onFinished={[MockFunction]} <div />
space={ <div
{ id="mx_ContextualMenu_Container"
"canInvite": [MockFunction] { >
"calls": [ <div
[ class="mx_ContextualMenu_wrapper"
"@test:server", >
], <div
], class="mx_ContextualMenu_background"
"results": [ />
{ <div
"type": "return", class="mx_ContextualMenu"
"value": undefined, role="menu"
}, >
], <div
}, class="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"
"client": { >
"getUserId": [MockFunction] { <div
"calls": [ class="mx_SpacePanel_contextMenu_header"
[], >
], test space
"results": [ </div>
{ <div
"type": "return", class="mx_IconizedContextMenu_optionList"
"value": "@test:server", >
}, <div
], aria-label="Space home"
}, class="mx_AccessibleButton mx_IconizedContextMenu_item focus-visible"
}, data-focus-visible-added=""
"currentState": { role="menuitem"
"maySendStateEvent": [MockFunction] { tabindex="0"
"calls": [ >
[ <span
"m.space.child", class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
"@test:server", />
], <span
], class="mx_IconizedContextMenu_label"
"results": [ >
{ Space home
"type": "return", </span>
"value": undefined, </div>
}, <div
], aria-label="Explore rooms"
}, class="mx_AccessibleButton mx_IconizedContextMenu_item"
}, role="menuitem"
"getJoinRule": [MockFunction] { tabindex="-1"
"calls": [ >
[], <span
], class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
"results": [ />
{ <span
"type": "return", class="mx_IconizedContextMenu_label"
"value": undefined, >
}, Explore rooms
], </span>
}, </div>
"getMyMembership": [MockFunction], <div
"name": "test space", aria-label="Preferences"
} class="mx_AccessibleButton mx_IconizedContextMenu_item"
} role="menuitem"
> tabindex="-1"
<IconizedContextMenu >
className="mx_SpacePanel_contextMenu" <span
compact={true} class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
onFinished={[MockFunction]} />
> <span
<ContextMenu class="mx_IconizedContextMenu_label"
chevronFace="none" >
hasBackground={true} Preferences
managed={true} </span>
onFinished={[MockFunction]} </div>
> <div
<Portal aria-label="Leave space"
containerInfo={ class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
<div data-testid="leave-option"
id="mx_ContextualMenu_Container" role="menuitem"
> tabindex="-1"
<div >
class="mx_ContextualMenu_wrapper" <span
> class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
<div />
class="mx_ContextualMenu_background" <span
/> class="mx_IconizedContextMenu_label"
<div >
class="mx_ContextualMenu" Leave space
role="menu" </span>
> </div>
<div </div>
class="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact" </div>
> </div>
<div </div>
class="mx_SpacePanel_contextMenu_header" </div>
> </body>"
test space
</div>
<div
class="mx_IconizedContextMenu_optionList"
>
<div
aria-label="Space home"
class="mx_AccessibleButton mx_IconizedContextMenu_item focus-visible"
data-focus-visible-added=""
role="menuitem"
tabindex="0"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
class="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>
<div
aria-label="Explore rooms"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
class="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>
<div
aria-label="Preferences"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
class="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>
<div
aria-label="Leave space"
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
class="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>
</div>
</div>
</div>
</div>
</div>
}
>
<RovingTabIndexProvider
handleHomeEnd={true}
handleUpDown={true}
onKeyDown={[Function]}
>
<div
className="mx_ContextualMenu_wrapper"
onClick={[Function]}
onContextMenu={[Function]}
onKeyDown={[Function]}
style={
{
"bottom": undefined,
"right": undefined,
}
}
>
<div
className="mx_ContextualMenu_background"
onClick={[Function]}
onContextMenu={[Function]}
style={{}}
/>
<div
className="mx_ContextualMenu"
role="menu"
style={{}}
>
<div
className="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"
>
<div
className="mx_SpacePanel_contextMenu_header"
>
test space
</div>
<IconizedContextMenuOptionList
first={true}
>
<div
className="mx_IconizedContextMenu_optionList"
>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label="Space home"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Space home"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Space home"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Space home"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
{
"current": <div
aria-label="Space home"
class="mx_AccessibleButton mx_IconizedContextMenu_item focus-visible"
data-focus-visible-added=""
role="menuitem"
tabindex="0"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
class="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={0}
>
<div
aria-label="Space home"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={0}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
className="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label="Explore rooms"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Explore rooms"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Explore rooms"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Explore rooms"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
{
"current": <div
aria-label="Explore rooms"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
class="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Explore rooms"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
className="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPreferences"
label="Preferences"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Preferences"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Preferences"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Preferences"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
{
"current": <div
aria-label="Preferences"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
class="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Preferences"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
className="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
className="mx_IconizedContextMenu_option_red"
data-test-id="leave-option"
iconClassName="mx_SpacePanel_iconLeave"
label="Leave space"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
label="Leave space"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Leave space"
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Leave space"
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
element="div"
inputRef={
{
"current": <div
aria-label="Leave space"
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
class="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Leave space"
className="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
className="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
</div>
</IconizedContextMenuOptionList>
</div>
</div>
</div>
</RovingTabIndexProvider>
</Portal>
</ContextMenu>
</IconizedContextMenu>
</SpaceContextMenu>
`; `;

View file

@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
// eslint-disable-next-line deprecate/import
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import LabelledCheckbox from "../../../../src/components/views/elements/LabelledCheckbox"; import LabelledCheckbox from "../../../../src/components/views/elements/LabelledCheckbox";
@ -30,32 +28,18 @@ jest.mock("matrix-js-sdk/src/randomstring", () => {
describe("<LabelledCheckbox />", () => { describe("<LabelledCheckbox />", () => {
type CompProps = React.ComponentProps<typeof LabelledCheckbox>; type CompProps = React.ComponentProps<typeof LabelledCheckbox>;
const getComponent = (props: CompProps) => mount(<LabelledCheckbox {...props} />); const getComponent = (props: CompProps) => <LabelledCheckbox {...props} />;
type CompClass = ReturnType<typeof getComponent>; const getCheckbox = (): HTMLInputElement => screen.getByRole("checkbox");
const getCheckbox = (component: CompClass) => component.find(`input[type="checkbox"]`); it.each([undefined, "this is a byline"])("should render with byline of %p", (byline) => {
const getLabel = (component: CompClass) => component.find(`.mx_LabelledCheckbox_label`);
const getByline = (component: CompClass) => component.find(`.mx_LabelledCheckbox_byline`);
const isChecked = (checkbox: ReturnType<typeof getCheckbox>) => checkbox.is(`[checked=true]`);
const isDisabled = (checkbox: ReturnType<typeof getCheckbox>) => checkbox.is(`[disabled=true]`);
const getText = (span: ReturnType<typeof getLabel>) => (span.length > 0 ? span.at(0).text() : null);
test.each([null, "this is a byline"])("should render with byline of %p", (byline) => {
const props: CompProps = { const props: CompProps = {
label: "Hello world", label: "Hello world",
value: true, value: true,
byline: byline, byline: byline,
onChange: jest.fn(), onChange: jest.fn(),
}; };
const component = getComponent(props); const renderResult = render(getComponent(props));
const checkbox = getCheckbox(component); expect(renderResult.asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
expect(isChecked(checkbox)).toBe(true);
expect(isDisabled(checkbox)).toBe(false);
expect(getText(getLabel(component))).toBe(props.label);
expect(getText(getByline(component))).toBe(byline);
}); });
it("should support unchecked by default", () => { it("should support unchecked by default", () => {
@ -64,9 +48,8 @@ describe("<LabelledCheckbox />", () => {
value: false, value: false,
onChange: jest.fn(), onChange: jest.fn(),
}; };
const component = getComponent(props); render(getComponent(props));
expect(getCheckbox()).not.toBeChecked();
expect(isChecked(getCheckbox(component))).toBe(false);
}); });
it("should be possible to disable the checkbox", () => { it("should be possible to disable the checkbox", () => {
@ -76,9 +59,8 @@ describe("<LabelledCheckbox />", () => {
disabled: true, disabled: true,
onChange: jest.fn(), onChange: jest.fn(),
}; };
const component = getComponent(props); render(getComponent(props));
expect(getCheckbox()).toBeDisabled();
expect(isDisabled(getCheckbox(component))).toBe(true);
}); });
it("should emit onChange calls", () => { it("should emit onChange calls", () => {
@ -87,15 +69,11 @@ describe("<LabelledCheckbox />", () => {
value: false, value: false,
onChange: jest.fn(), onChange: jest.fn(),
}; };
const component = getComponent(props); render(getComponent(props));
expect(props.onChange).not.toHaveBeenCalled(); expect(props.onChange).not.toHaveBeenCalled();
fireEvent.click(getCheckbox());
act(() => { expect(props.onChange).toHaveBeenCalledWith(true);
getCheckbox(component).simulate("change");
});
expect(props.onChange).toHaveBeenCalledTimes(1);
}); });
it("should react to value and disabled prop changes", () => { it("should react to value and disabled prop changes", () => {
@ -104,16 +82,18 @@ describe("<LabelledCheckbox />", () => {
value: false, value: false,
onChange: jest.fn(), onChange: jest.fn(),
}; };
const component = getComponent(props); const { rerender } = render(getComponent(props));
let checkbox = getCheckbox(component);
expect(isChecked(checkbox)).toBe(false); let checkbox = getCheckbox();
expect(isDisabled(checkbox)).toBe(false); expect(checkbox).not.toBeChecked();
expect(checkbox).not.toBeDisabled();
component.setProps({ value: true, disabled: true }); props.disabled = true;
checkbox = getCheckbox(component); // refresh reference to checkbox props.value = true;
rerender(getComponent(props));
expect(isChecked(checkbox)).toBe(true); checkbox = getCheckbox();
expect(isDisabled(checkbox)).toBe(true); expect(checkbox).toBeChecked();
expect(checkbox).toBeDisabled();
}); });
}); });

View file

@ -1,106 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LabelledCheckbox /> should render with byline of "this is a byline" 1`] = ` exports[`<LabelledCheckbox /> should render with byline of "this is a byline" 1`] = `
<LabelledCheckbox <DocumentFragment>
byline="this is a byline"
label="Hello world"
onChange={[MockFunction]}
value={true}
>
<label <label
className="mx_LabelledCheckbox" class="mx_LabelledCheckbox"
>
<StyledCheckbox
checked={true}
className=""
onChange={[Function]}
> >
<span <span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid" class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
> >
<input <input
checked={true} checked=""
id="checkbox_abdefghi" id="checkbox_abdefghi"
onChange={[Function]}
type="checkbox" type="checkbox"
/> />
<label <label
htmlFor="checkbox_abdefghi" for="checkbox_abdefghi"
> >
<div <div
className="mx_Checkbox_background" class="mx_Checkbox_background"
> >
<div <div
className="mx_Checkbox_checkmark" class="mx_Checkbox_checkmark"
/> />
</div> </div>
</label> </label>
</span> </span>
</StyledCheckbox>
<div <div
className="mx_LabelledCheckbox_labels" class="mx_LabelledCheckbox_labels"
> >
<span <span
className="mx_LabelledCheckbox_label" class="mx_LabelledCheckbox_label"
> >
Hello world Hello world
</span> </span>
<span <span
className="mx_LabelledCheckbox_byline" class="mx_LabelledCheckbox_byline"
> >
this is a byline this is a byline
</span> </span>
</div> </div>
</label> </label>
</LabelledCheckbox> </DocumentFragment>
`; `;
exports[`<LabelledCheckbox /> should render with byline of null 1`] = ` exports[`<LabelledCheckbox /> should render with byline of undefined 1`] = `
<LabelledCheckbox <DocumentFragment>
byline={null}
label="Hello world"
onChange={[MockFunction]}
value={true}
>
<label <label
className="mx_LabelledCheckbox" class="mx_LabelledCheckbox"
>
<StyledCheckbox
checked={true}
className=""
onChange={[Function]}
> >
<span <span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid" class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
> >
<input <input
checked={true} checked=""
id="checkbox_abdefghi" id="checkbox_abdefghi"
onChange={[Function]}
type="checkbox" type="checkbox"
/> />
<label <label
htmlFor="checkbox_abdefghi" for="checkbox_abdefghi"
> >
<div <div
className="mx_Checkbox_background" class="mx_Checkbox_background"
> >
<div <div
className="mx_Checkbox_checkmark" class="mx_Checkbox_checkmark"
/> />
</div> </div>
</label> </label>
</span> </span>
</StyledCheckbox>
<div <div
className="mx_LabelledCheckbox_labels" class="mx_LabelledCheckbox_labels"
> >
<span <span
className="mx_LabelledCheckbox_label" class="mx_LabelledCheckbox_label"
> >
Hello world Hello world
</span> </span>
</div> </div>
</label> </label>
</LabelledCheckbox> </DocumentFragment>
`; `;

View file

@ -24,34 +24,64 @@ import * as TestUtils from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import EditorModel from "../../../../src/editor/model"; import EditorModel from "../../../../src/editor/model";
import { createPartCreator, createRenderer } from "../../../editor/mock"; import { createPartCreator, createRenderer } from "../../../editor/mock";
import SettingsStore from "../../../../src/settings/SettingsStore";
describe("BasicMessageComposer", () => { describe("BasicMessageComposer", () => {
const renderer = createRenderer(); const renderer = createRenderer();
const pc = createPartCreator(); const pc = createPartCreator();
beforeEach(() => {
TestUtils.stubClient(); TestUtils.stubClient();
});
it("should allow a user to paste a URL without it being mangled", () => {
const model = new EditorModel([], pc, renderer);
const client: MatrixClient = MatrixClientPeg.get(); const client: MatrixClient = MatrixClientPeg.get();
const roomId = "!1234567890:domain"; const roomId = "!1234567890:domain";
const userId = client.getSafeUserId(); const userId = client.getSafeUserId();
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
it("should allow a user to paste a URL without it being mangled", async () => {
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
const testUrl = "https://element.io"; const testUrl = "https://element.io";
const mockDataTransfer = generateMockDataTransferForString(testUrl); const mockDataTransfer = generateMockDataTransferForString(testUrl);
await userEvent.paste(mockDataTransfer);
render(<BasicMessageComposer model={model} room={room} />);
userEvent.paste(mockDataTransfer);
expect(model.parts).toHaveLength(1); expect(model.parts).toHaveLength(1);
expect(model.parts[0].text).toBe(testUrl); expect(model.parts[0].text).toBe(testUrl);
expect(screen.getByText(testUrl)).toBeInTheDocument(); expect(screen.getByText(testUrl)).toBeInTheDocument();
}); });
it("should replaceEmoticons properly", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
return settingName === "MessageComposerInput.autoReplaceEmoji";
});
userEvent.setup();
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
const tranformations = [
{ before: "4:3 video", after: "4:3 video" },
{ before: "regexp 12345678", after: "regexp 12345678" },
{ before: "--:--)", after: "--:--)" },
{ before: "we <3 matrix", after: "we ❤️ matrix" },
{ before: "hello world :-)", after: "hello world 🙂" },
{ before: ":) hello world", after: "🙂 hello world" },
{ before: ":D 4:3 video :)", after: "😄 4:3 video 🙂" },
{ before: ":-D", after: "😄" },
{ before: ":D", after: "😄" },
{ before: ":3", after: "😽" },
];
const input = screen.getByRole("textbox");
for (const { before, after } of tranformations) {
await userEvent.clear(input);
//add a space after the text to trigger the replacement
await userEvent.type(input, before + " ");
const transformedText = model.parts.map((part) => part.text).join("");
expect(transformedText).toBe(after + " ");
}
});
}); });
function generateMockDataTransferForString(string: string): DataTransfer { function generateMockDataTransferForString(string: string): DataTransfer {

View file

@ -226,6 +226,7 @@ describe("<Notifications />", () => {
setAccountData: jest.fn(), setAccountData: jest.fn(),
sendReadReceipt: jest.fn(), sendReadReceipt: jest.fn(),
supportsThreads: jest.fn().mockReturnValue(true), supportsThreads: jest.fn().mockReturnValue(true),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
}); });
mockClient.getPushRules.mockResolvedValue(pushRules); mockClient.getPushRules.mockResolvedValue(pushRules);

View file

@ -94,6 +94,8 @@ describe("RoomViewStore", function () {
getDeviceId: jest.fn().mockReturnValue("ABC123"), getDeviceId: jest.fn().mockReturnValue("ABC123"),
sendStateEvent: jest.fn().mockResolvedValue({}), sendStateEvent: jest.fn().mockResolvedValue({}),
supportsThreads: jest.fn(), supportsThreads: jest.fn(),
isInitialSyncComplete: jest.fn().mockResolvedValue(false),
relations: jest.fn(),
}); });
const room = new Room(roomId, mockClient, userId); const room = new Room(roomId, mockClient, userId);
const room2 = new Room(roomId2, mockClient, userId); const room2 = new Room(roomId2, mockClient, userId);

109
yarn.lock
View file

@ -2127,11 +2127,12 @@
"@types/fbemitter" "*" "@types/fbemitter" "*"
"@types/react" "*" "@types/react" "*"
"@types/fs-extra@^9.0.13": "@types/fs-extra@^11.0.0":
version "9.0.13" version "11.0.1"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5"
integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==
dependencies: dependencies:
"@types/jsonfile" "*"
"@types/node" "*" "@types/node" "*"
"@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.8": "@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.8":
@ -2200,6 +2201,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/jsonfile@*":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b"
integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==
dependencies:
"@types/node" "*"
"@types/katex@^0.14.0": "@types/katex@^0.14.0":
version "0.14.0" version "0.14.0"
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe" resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
@ -2417,14 +2425,15 @@
integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==
"@typescript-eslint/eslint-plugin@^5.35.1": "@typescript-eslint/eslint-plugin@^5.35.1":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a"
integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "5.48.1" "@typescript-eslint/scope-manager" "5.51.0"
"@typescript-eslint/type-utils" "5.48.1" "@typescript-eslint/type-utils" "5.51.0"
"@typescript-eslint/utils" "5.48.1" "@typescript-eslint/utils" "5.51.0"
debug "^4.3.4" debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0" ignore "^5.2.0"
natural-compare-lite "^1.4.0" natural-compare-lite "^1.4.0"
regexpp "^3.2.0" regexpp "^3.2.0"
@ -2432,71 +2441,71 @@
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/parser@^5.6.0": "@typescript-eslint/parser@^5.6.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271"
integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "5.48.1" "@typescript-eslint/scope-manager" "5.51.0"
"@typescript-eslint/types" "5.48.1" "@typescript-eslint/types" "5.51.0"
"@typescript-eslint/typescript-estree" "5.48.1" "@typescript-eslint/typescript-estree" "5.51.0"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/scope-manager@5.48.1": "@typescript-eslint/scope-manager@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990"
integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==
dependencies: dependencies:
"@typescript-eslint/types" "5.48.1" "@typescript-eslint/types" "5.51.0"
"@typescript-eslint/visitor-keys" "5.48.1" "@typescript-eslint/visitor-keys" "5.51.0"
"@typescript-eslint/type-utils@5.48.1": "@typescript-eslint/type-utils@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988"
integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==
dependencies: dependencies:
"@typescript-eslint/typescript-estree" "5.48.1" "@typescript-eslint/typescript-estree" "5.51.0"
"@typescript-eslint/utils" "5.48.1" "@typescript-eslint/utils" "5.51.0"
debug "^4.3.4" debug "^4.3.4"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/types@5.48.1": "@typescript-eslint/types@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90"
integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==
"@typescript-eslint/typescript-estree@5.48.1": "@typescript-eslint/typescript-estree@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de"
integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==
dependencies: dependencies:
"@typescript-eslint/types" "5.48.1" "@typescript-eslint/types" "5.51.0"
"@typescript-eslint/visitor-keys" "5.48.1" "@typescript-eslint/visitor-keys" "5.51.0"
debug "^4.3.4" debug "^4.3.4"
globby "^11.1.0" globby "^11.1.0"
is-glob "^4.0.3" is-glob "^4.0.3"
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/utils@5.48.1": "@typescript-eslint/utils@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47"
integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==
dependencies: dependencies:
"@types/json-schema" "^7.0.9" "@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12" "@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.48.1" "@typescript-eslint/scope-manager" "5.51.0"
"@typescript-eslint/types" "5.48.1" "@typescript-eslint/types" "5.51.0"
"@typescript-eslint/typescript-estree" "5.48.1" "@typescript-eslint/typescript-estree" "5.51.0"
eslint-scope "^5.1.1" eslint-scope "^5.1.1"
eslint-utils "^3.0.0" eslint-utils "^3.0.0"
semver "^7.3.7" semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.48.1": "@typescript-eslint/visitor-keys@5.51.0":
version "5.48.1" version "5.51.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87"
integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==
dependencies: dependencies:
"@typescript-eslint/types" "5.48.1" "@typescript-eslint/types" "5.51.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@wojtekmaj/enzyme-adapter-react-17@^0.8.0": "@wojtekmaj/enzyme-adapter-react-17@^0.8.0":