diff --git a/package.json b/package.json index f71915058e..b874779511 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "@types/escape-html": "^1.0.1", "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", "@types/jest": "^29.2.1", "@types/katex": "^0.14.0", diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index 7dbc8a5406..851aa95df1 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -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"); } else if (err.httpStatus === 404) { 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) { err.message += ` (Status ${err.httpStatus})`; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index dc8413869f..bdc25916e9 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -63,7 +63,7 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... inviteOption = ( = ({ space, hideHeader, onFinished, ... settingsOption = ( = ({ space, hideHeader, onFinished, ... leaveOption = ( = ({ space, hideHeader, onFinished, ... newRoomSection = ( <> -
+
{_t("Add")}
{canAddRooms && ( = ({ space, hideHeader, onFinished, ... )} {canAddVideoRooms && ( = ({ space, hideHeader, onFinished, ... )} {canAddSubSpaces && ( public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number { const { model } = this.props; 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 - let n = 8; + let n = 9; range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; n -= 1; return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type); }); 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("-", ""); // 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()); diff --git a/src/components/views/settings/devices/DeviceSecurityLearnMore.tsx b/src/components/views/settings/devices/DeviceSecurityLearnMore.tsx index cb8c1ec9b3..03ce6fec06 100644 --- a/src/components/views/settings/devices/DeviceSecurityLearnMore.tsx +++ b/src/components/views/settings/devices/DeviceSecurityLearnMore.tsx @@ -73,7 +73,7 @@ const securityCardContent: Record< title: _t("Unverified session"), description: ( <> -

{_t(`This session doesn't support encryption, so it can't be verified.`)}

+

{_t(`This session doesn't support encryption and thus can't be verified.`)}

{_t( `You won't be able to participate in rooms where encryption is enabled when using this session.`, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c7f5eebd17..db38c61560 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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", "Unable to enable Notifications": "Unable to enable Notifications", "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 States": "United States", "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.", "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", - "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.", "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", @@ -1842,7 +1842,6 @@ "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.", "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 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", diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index dc856bd1f8..6ed50406b1 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -62,12 +62,12 @@ interface IStickyRoom { */ export class Algorithm extends EventEmitter { private _cachedRooms: ITagMap = {}; - private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room - private _stickyRoom: IStickyRoom = null; - private _lastStickyRoom: IStickyRoom = null; // only not-null when changing the sticky room - private sortAlgorithms: ITagSortingMap; - private listAlgorithms: IListOrderingMap; - private algorithms: IOrderingAlgorithmMap; + private _cachedStickyRooms: ITagMap | null = {}; // a clone of the _cachedRooms, with the sticky room + private _stickyRoom: IStickyRoom | null = null; + private _lastStickyRoom: IStickyRoom | null = null; // only not-null when changing the sticky room + private sortAlgorithms: ITagSortingMap | null = null; + private listAlgorithms: IListOrderingMap | null = null; + private algorithms: IOrderingAlgorithmMap | null = null; private rooms: Room[] = []; private roomIdsToTags: { [roomId: string]: TagID[]; @@ -86,7 +86,7 @@ export class Algorithm extends EventEmitter { CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls); } - public get stickyRoom(): Room { + public get 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; return this.sortAlgorithms[tagId]; } @@ -132,6 +132,8 @@ export class Algorithm extends EventEmitter { public setTagSorting(tagId: TagID, sort: SortAlgorithm): void { if (!tagId) throw new Error("Tag ID 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; const algorithm: OrderingAlgorithm = this.algorithms[tagId]; @@ -141,7 +143,7 @@ export class Algorithm extends EventEmitter { this.recalculateActiveCallRooms(tagId); } - public getListOrdering(tagId: TagID): ListAlgorithm { + public getListOrdering(tagId: TagID): ListAlgorithm | null { if (!this.listAlgorithms) return null; return this.listAlgorithms[tagId]; } @@ -149,6 +151,9 @@ export class Algorithm extends EventEmitter { public setListOrdering(tagId: TagID, order: ListAlgorithm): void { if (!tagId) throw new Error("Tag ID 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; const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]); @@ -160,12 +165,12 @@ export class Algorithm extends EventEmitter { this.recalculateActiveCallRooms(tagId); } - private updateStickyRoom(val: Room): void { + private updateStickyRoom(val: Room | null): void { this.doUpdateStickyRoom(val); 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") { // no-op sticky rooms for spaces - they're effectively virtual rooms 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 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 // it is still the same room. if (this._stickyRoom) { @@ -284,6 +293,13 @@ export class Algorithm extends EventEmitter { 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 => { // In case we're unsticking a room, sort it back into natural order this.recalculateStickyRoom(); @@ -310,7 +326,7 @@ export class Algorithm extends EventEmitter { * the call. * @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. // 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 @@ -336,14 +352,16 @@ export class Algorithm extends EventEmitter { if (updatedTag) { // Update the tag indicated by the caller, if possible. This is mostly to ensure // our cache is up to date. - this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone + if (this._cachedStickyRooms) { + this._cachedStickyRooms[updatedTag] = [...this.cachedRooms[updatedTag]]; // shallow clone + } } // 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 might have updated from the cache is also our sticky room. 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); } @@ -362,7 +380,7 @@ export class Algorithm extends EventEmitter { * * @param updatedTag The tag that was updated, if possible. */ - protected recalculateActiveCallRooms(updatedTag: TagID = null): void { + protected recalculateActiveCallRooms(updatedTag: TagID | null = null): void { if (!updatedTag) { // Assume all tags need updating // 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) { // We operate on the sticky rooms map 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 activeRooms: Room[] = []; @@ -390,7 +408,7 @@ export class Algorithm extends EventEmitter { } // 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 - if (hasTags && isSticky) { + if (hasTags && isSticky && this._stickyRoom) { // Go directly in and set the sticky room's new reference, being careful not // to trigger a sticky room update ourselves. this._stickyRoom.room = room; diff --git a/test/RoomNotifs-test.ts b/test/RoomNotifs-test.ts index 963bfd77ed..04ee9e693b 100644 --- a/test/RoomNotifs-test.ts +++ b/test/RoomNotifs-test.ts @@ -113,10 +113,10 @@ describe("RoomNotifs test", () => { event: true, type: "m.room.create", room: ROOM_ID, - user: client.getUserId()!, + user: "@zoe:localhost", content: { ...(predecessorId ? { predecessor: { room_id: predecessorId, event_id: "$someevent" } } : {}), - creator: client.getUserId(), + creator: "@zoe:localhost", room_version: "5", }, ts: Date.now(), @@ -128,7 +128,7 @@ describe("RoomNotifs test", () => { event: true, type: EventType.RoomPredecessor, room: ROOM_ID, - user: client.getUserId()!, + user: "@zoe:localhost", skey: "", content: { predecessor_room_id: predecessorId, diff --git a/test/components/views/context_menus/SpaceContextMenu-test.tsx b/test/components/views/context_menus/SpaceContextMenu-test.tsx index f03e7c149b..c508b35290 100644 --- a/test/components/views/context_menus/SpaceContextMenu-test.tsx +++ b/test/components/views/context_menus/SpaceContextMenu-test.tsx @@ -15,16 +15,14 @@ limitations under the License. */ import React from "react"; -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; -import { Room } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; -import { act } from "react-dom/test-utils"; +import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { Mocked, mocked } from "jest-mock"; 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 MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import { findByTestId } from "../../../test-utils"; import { shouldShowSpaceSettings, showCreateNewRoom, @@ -55,9 +53,11 @@ jest.mock("../../../../src/utils/leave-behaviour", () => ({ describe("", () => { const userId = "@test:server"; + const mockClient = { getUserId: jest.fn().mockReturnValue(userId), - }; + } as unknown as Mocked; + const makeMockSpace = (props = {}) => ({ name: "test space", @@ -70,17 +70,18 @@ describe("", () => { getMyMembership: jest.fn(), ...props, } as unknown as Room); + const defaultProps = { space: makeMockSpace(), onFinished: jest.fn(), }; - const getComponent = (props = {}) => - mount(, { - wrappingComponent: MatrixClientContext.Provider, - wrappingComponentProps: { - value: mockClient, - }, - }); + + const renderComponent = (props = {}): RenderResult => + render( + + + , + ); beforeEach(() => { jest.resetAllMocks(); @@ -88,134 +89,135 @@ describe("", () => { }); it("renders menu correctly", () => { - const component = getComponent(); - expect(component).toMatchSnapshot(); + const { baseElement } = renderComponent(); + expect(prettyDOM(baseElement)).toMatchSnapshot(); }); it("renders invite option when space is public", () => { const space = makeMockSpace({ getJoinRule: jest.fn().mockReturnValue("public"), }); - const component = getComponent({ space }); - expect(findByTestId(component, "invite-option").length).toBeTruthy(); + renderComponent({ space }); + expect(screen.getByTestId("invite-option")).toBeInTheDocument(); }); + it("renders invite option when user is has invite rights for space", () => { const space = makeMockSpace({ canInvite: jest.fn().mockReturnValue(true), }); - const component = getComponent({ space }); + renderComponent({ space }); 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({ getJoinRule: jest.fn().mockReturnValue("public"), }); const onFinished = jest.fn(); - const component = getComponent({ space, onFinished }); + renderComponent({ space, onFinished }); - act(() => { - findByTestId(component, "invite-option").at(0).simulate("click"); - }); + await userEvent.click(screen.getByTestId("invite-option")); expect(showSpaceInvite).toHaveBeenCalledWith(space); expect(onFinished).toHaveBeenCalled(); }); + it("renders space settings option when user has rights", () => { mocked(shouldShowSpaceSettings).mockReturnValue(true); - const component = getComponent(); + renderComponent(); 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); const onFinished = jest.fn(); - const component = getComponent({ onFinished }); + renderComponent({ onFinished }); - act(() => { - findByTestId(component, "settings-option").at(0).simulate("click"); - }); + await userEvent.click(screen.getByTestId("settings-option")); expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space); expect(onFinished).toHaveBeenCalled(); }); + it("renders leave option when user does not have rights to see space settings", () => { - const component = getComponent(); - expect(findByTestId(component, "leave-option").length).toBeTruthy(); + renderComponent(); + 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 component = getComponent({ onFinished }); - act(() => { - findByTestId(component, "leave-option").at(0).simulate("click"); - }); + renderComponent({ onFinished }); + await userEvent.click(screen.getByTestId("leave-option")); expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space); expect(onFinished).toHaveBeenCalled(); }); + describe("add children section", () => { const space = makeMockSpace(); + beforeEach(() => { // set space to allow adding children to space mocked(space.currentState.maySendStateEvent).mockReturnValue(true); mocked(shouldShowComponent).mockReturnValue(true); }); + it("does not render section when user does not have permission to add children", () => { mocked(space.currentState.maySendStateEvent).mockReturnValue(false); - const component = getComponent({ space }); + renderComponent({ space }); - expect(findByTestId(component, "add-to-space-header").length).toBeFalsy(); - expect(findByTestId(component, "new-room-option").length).toBeFalsy(); - expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); + expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument(); + expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument(); + expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument(); }); + it("does not render section when UIComponent customisations disable room and space creation", () => { mocked(shouldShowComponent).mockReturnValue(false); - const component = getComponent({ space }); + renderComponent({ space }); expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms); expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces); - expect(findByTestId(component, "add-to-space-header").length).toBeFalsy(); - expect(findByTestId(component, "new-room-option").length).toBeFalsy(); - expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); + expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument(); + expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument(); + expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument(); }); it("renders section with add room button when UIComponent customisation allows CreateRoom", () => { // only allow CreateRoom mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms); - const component = getComponent({ space }); + renderComponent({ space }); - expect(findByTestId(component, "add-to-space-header").length).toBeTruthy(); - expect(findByTestId(component, "new-room-option").length).toBeTruthy(); - expect(findByTestId(component, "new-subspace-option").length).toBeFalsy(); + expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument(); + expect(screen.getByTestId("new-room-option")).toBeInTheDocument(); + expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument(); }); it("renders section with add space button when UIComponent customisation allows CreateSpace", () => { // only allow CreateSpaces mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces); - const component = getComponent({ space }); + renderComponent({ space }); - expect(findByTestId(component, "add-to-space-header").length).toBeTruthy(); - expect(findByTestId(component, "new-room-option").length).toBeFalsy(); - expect(findByTestId(component, "new-subspace-option").length).toBeTruthy(); + expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument(); + expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument(); + 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 component = getComponent({ space, onFinished }); + renderComponent({ space, onFinished }); - act(() => { - findByTestId(component, "new-room-option").at(0).simulate("click"); - }); + await userEvent.click(screen.getByTestId("new-room-option")); expect(showCreateNewRoom).toHaveBeenCalledWith(space); expect(onFinished).toHaveBeenCalled(); }); - it("opens create space dialog on add space button click", () => { - const onFinished = jest.fn(); - const component = getComponent({ space, onFinished }); - act(() => { - findByTestId(component, "new-subspace-option").at(0).simulate("click"); - }); + it("opens create space dialog on add space button click", async () => { + const onFinished = jest.fn(); + renderComponent({ space, onFinished }); + + await userEvent.click(screen.getByTestId("new-subspace-option")); expect(showCreateNewSubspace).toHaveBeenCalledWith(space); expect(onFinished).toHaveBeenCalled(); }); diff --git a/test/components/views/context_menus/__snapshots__/SpaceContextMenu-test.tsx.snap b/test/components/views/context_menus/__snapshots__/SpaceContextMenu-test.tsx.snap index a5ce23451f..bd732f4426 100644 --- a/test/components/views/context_menus/__snapshots__/SpaceContextMenu-test.tsx.snap +++ b/test/components/views/context_menus/__snapshots__/SpaceContextMenu-test.tsx.snap @@ -1,500 +1,98 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders menu correctly 1`] = ` - - - - -

-
- -
-
- } - > - -
-
-
-
-
- test space -
- -
- - - - - - - Space home - -
, - } - } - onClick={[Function]} - onFocus={[Function]} - role="menuitem" - tabIndex={0} - > -
- - - Space home - -
- - - - - - - - - - - Explore rooms - -
, - } - } - onClick={[Function]} - onFocus={[Function]} - role="menuitem" - tabIndex={-1} - > -
- - - Explore rooms - -
- - - - - - - - - - - Preferences - -
, - } - } - onClick={[Function]} - onFocus={[Function]} - role="menuitem" - tabIndex={-1} - > -
- - - Preferences - -
- - - - - - - - - - - Leave space - -
, - } - } - onClick={[Function]} - onFocus={[Function]} - role="menuitem" - tabIndex={-1} - > -
- - - Leave space - -
- - - - -
- -
- - - - - - - +" + 
 +  +  +  +  +  +  + test space + 
 +  +  +  +  + Space home +  +  +  +  +  + Explore rooms +  +  +  +  +  + Preferences +  +  +  +  +  + Leave space +  +  +  +  +  +  +  +" `; diff --git a/test/components/views/elements/LabelledCheckbox-test.tsx b/test/components/views/elements/LabelledCheckbox-test.tsx index eed94ab598..857fcdf1fe 100644 --- a/test/components/views/elements/LabelledCheckbox-test.tsx +++ b/test/components/views/elements/LabelledCheckbox-test.tsx @@ -14,10 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { fireEvent, render, screen } from "@testing-library/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"; @@ -30,32 +28,18 @@ jest.mock("matrix-js-sdk/src/randomstring", () => { describe("", () => { type CompProps = React.ComponentProps; - const getComponent = (props: CompProps) => mount(); - type CompClass = ReturnType; + const getComponent = (props: CompProps) => ; + const getCheckbox = (): HTMLInputElement => screen.getByRole("checkbox"); - const getCheckbox = (component: CompClass) => component.find(`input[type="checkbox"]`); - const getLabel = (component: CompClass) => component.find(`.mx_LabelledCheckbox_label`); - const getByline = (component: CompClass) => component.find(`.mx_LabelledCheckbox_byline`); - - const isChecked = (checkbox: ReturnType) => checkbox.is(`[checked=true]`); - const isDisabled = (checkbox: ReturnType) => checkbox.is(`[disabled=true]`); - const getText = (span: ReturnType) => (span.length > 0 ? span.at(0).text() : null); - - test.each([null, "this is a byline"])("should render with byline of %p", (byline) => { + it.each([undefined, "this is a byline"])("should render with byline of %p", (byline) => { const props: CompProps = { label: "Hello world", value: true, byline: byline, onChange: jest.fn(), }; - const component = getComponent(props); - const checkbox = getCheckbox(component); - - 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); + const renderResult = render(getComponent(props)); + expect(renderResult.asFragment()).toMatchSnapshot(); }); it("should support unchecked by default", () => { @@ -64,9 +48,8 @@ describe("", () => { value: false, onChange: jest.fn(), }; - const component = getComponent(props); - - expect(isChecked(getCheckbox(component))).toBe(false); + render(getComponent(props)); + expect(getCheckbox()).not.toBeChecked(); }); it("should be possible to disable the checkbox", () => { @@ -76,9 +59,8 @@ describe("", () => { disabled: true, onChange: jest.fn(), }; - const component = getComponent(props); - - expect(isDisabled(getCheckbox(component))).toBe(true); + render(getComponent(props)); + expect(getCheckbox()).toBeDisabled(); }); it("should emit onChange calls", () => { @@ -87,15 +69,11 @@ describe("", () => { value: false, onChange: jest.fn(), }; - const component = getComponent(props); + render(getComponent(props)); expect(props.onChange).not.toHaveBeenCalled(); - - act(() => { - getCheckbox(component).simulate("change"); - }); - - expect(props.onChange).toHaveBeenCalledTimes(1); + fireEvent.click(getCheckbox()); + expect(props.onChange).toHaveBeenCalledWith(true); }); it("should react to value and disabled prop changes", () => { @@ -104,16 +82,18 @@ describe("", () => { value: false, onChange: jest.fn(), }; - const component = getComponent(props); - let checkbox = getCheckbox(component); + const { rerender } = render(getComponent(props)); - expect(isChecked(checkbox)).toBe(false); - expect(isDisabled(checkbox)).toBe(false); + let checkbox = getCheckbox(); + expect(checkbox).not.toBeChecked(); + expect(checkbox).not.toBeDisabled(); - component.setProps({ value: true, disabled: true }); - checkbox = getCheckbox(component); // refresh reference to checkbox + props.disabled = true; + props.value = true; + rerender(getComponent(props)); - expect(isChecked(checkbox)).toBe(true); - expect(isDisabled(checkbox)).toBe(true); + checkbox = getCheckbox(); + expect(checkbox).toBeChecked(); + expect(checkbox).toBeDisabled(); }); }); diff --git a/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap b/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap index 286c69a8d0..0ce152015e 100644 --- a/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap @@ -1,106 +1,82 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` should render with byline of "this is a byline" 1`] = ` - + - + `; -exports[` should render with byline of null 1`] = ` - +exports[` should render with byline of undefined 1`] = ` + - + `; diff --git a/test/components/views/rooms/BasicMessageComposer-test.tsx b/test/components/views/rooms/BasicMessageComposer-test.tsx index 3ab85957cb..34d32b627d 100644 --- a/test/components/views/rooms/BasicMessageComposer-test.tsx +++ b/test/components/views/rooms/BasicMessageComposer-test.tsx @@ -24,34 +24,64 @@ import * as TestUtils from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import EditorModel from "../../../../src/editor/model"; import { createPartCreator, createRenderer } from "../../../editor/mock"; +import SettingsStore from "../../../../src/settings/SettingsStore"; describe("BasicMessageComposer", () => { const renderer = createRenderer(); const pc = createPartCreator(); - beforeEach(() => { - TestUtils.stubClient(); - }); + TestUtils.stubClient(); - it("should allow a user to paste a URL without it being mangled", () => { + const client: MatrixClient = MatrixClientPeg.get(); + + const roomId = "!1234567890:domain"; + const userId = client.getSafeUserId(); + 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); - const client: MatrixClient = MatrixClientPeg.get(); - - const roomId = "!1234567890:domain"; - const userId = client.getSafeUserId(); - - const room = new Room(roomId, client, userId); - + render(); const testUrl = "https://element.io"; const mockDataTransfer = generateMockDataTransferForString(testUrl); - - render(); - userEvent.paste(mockDataTransfer); + await userEvent.paste(mockDataTransfer); expect(model.parts).toHaveLength(1); expect(model.parts[0].text).toBe(testUrl); 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(); + + 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 { diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx index aedb96fb13..367ab3085f 100644 --- a/test/components/views/settings/Notifications-test.tsx +++ b/test/components/views/settings/Notifications-test.tsx @@ -226,6 +226,7 @@ describe("", () => { setAccountData: jest.fn(), sendReadReceipt: jest.fn(), supportsThreads: jest.fn().mockReturnValue(true), + isInitialSyncComplete: jest.fn().mockReturnValue(false), }); mockClient.getPushRules.mockResolvedValue(pushRules); diff --git a/test/stores/RoomViewStore-test.ts b/test/stores/RoomViewStore-test.ts index 370fb95bcd..87d549c370 100644 --- a/test/stores/RoomViewStore-test.ts +++ b/test/stores/RoomViewStore-test.ts @@ -94,6 +94,8 @@ describe("RoomViewStore", function () { getDeviceId: jest.fn().mockReturnValue("ABC123"), sendStateEvent: jest.fn().mockResolvedValue({}), supportsThreads: jest.fn(), + isInitialSyncComplete: jest.fn().mockResolvedValue(false), + relations: jest.fn(), }); const room = new Room(roomId, mockClient, userId); const room2 = new Room(roomId2, mockClient, userId); diff --git a/yarn.lock b/yarn.lock index 109e48b48c..bbf10bb70c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2127,11 +2127,12 @@ "@types/fbemitter" "*" "@types/react" "*" -"@types/fs-extra@^9.0.13": - version "9.0.13" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" - integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== +"@types/fs-extra@^11.0.0": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" + integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA== dependencies: + "@types/jsonfile" "*" "@types/node" "*" "@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" 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": version "0.14.0" resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe" @@ -2417,14 +2425,15 @@ integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w== "@typescript-eslint/eslint-plugin@^5.35.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" - integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a" + integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/type-utils" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/type-utils" "5.51.0" + "@typescript-eslint/utils" "5.51.0" debug "^4.3.4" + grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" regexpp "^3.2.0" @@ -2432,71 +2441,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" - integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271" + integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/typescript-estree" "5.51.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" - integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== +"@typescript-eslint/scope-manager@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990" + integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" -"@typescript-eslint/type-utils@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" - integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== +"@typescript-eslint/type-utils@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988" + integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ== dependencies: - "@typescript-eslint/typescript-estree" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/typescript-estree" "5.51.0" + "@typescript-eslint/utils" "5.51.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" - integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== +"@typescript-eslint/types@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90" + integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== -"@typescript-eslint/typescript-estree@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" - integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== +"@typescript-eslint/typescript-estree@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de" + integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/visitor-keys" "5.51.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" - integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== +"@typescript-eslint/utils@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47" + integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.51.0" + "@typescript-eslint/types" "5.51.0" + "@typescript-eslint/typescript-estree" "5.51.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" - integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== +"@typescript-eslint/visitor-keys@5.51.0": + version "5.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87" + integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== dependencies: - "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/types" "5.51.0" eslint-visitor-keys "^3.3.0" "@wojtekmaj/enzyme-adapter-react-17@^0.8.0":