Skip to content

Commit

Permalink
feat(lowercase-invite): automatically lowercase field text in invite …
Browse files Browse the repository at this point in the history
…input
  • Loading branch information
marc.sirisak committed Jun 17, 2024
1 parent 6be037d commit 81796e8
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,10 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
};

private updateFilter = (e: React.ChangeEvent<HTMLInputElement>): void => {
const term = e.target.value;
// :TCHAP: lowercase-invite - const term = e.target.value;
const term = e.target.value?.toLowerCase();
// end :TCHAP:

this.setState({ filterText: term });

// Debounce server lookups to reduce spam. We don't clear the existing server
Expand Down Expand Up @@ -879,8 +882,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
// paste normally.
return;
}

const text = e.clipboardData.getData("text");
// :TCHAP: lowercase-invite - const text = e.clipboardData.getData("text");
const text = e.clipboardData.getData("text")?.toLowerCase();
// end :TCHAP:
const potentialAddresses = this.parseFilter(text);
// one search term which is not a mxid or email address
if (potentialAddresses.length === 1 && !potentialAddresses[0].includes("@")) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"@testing-library/cypress": "^9.0.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.9",
"@types/content-type": "^1.1.8",
"@types/counterpart": "^0.18.4",
Expand Down
6 changes: 6 additions & 0 deletions patches/subtree-modifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,11 @@
"files": [
"src/components/views/spaces/SpacePanel.tsx"
]
},
"lowercase-invite": {
"issue": "https://github.com/tchapgouv/tchap-web-v4/issues/983",
"files": [
"src/components/views/dialogs/InviteDialog.tsx"
]
}
}
198 changes: 198 additions & 0 deletions test/unit-tests/tchap/components/views/dialogs/InviteDialog-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, MatrixError, Room } from "matrix-js-sdk/src/matrix";
import { Mocked } from "jest-mock";

import InviteDialog from "~matrix-react-sdk/src/components/views/dialogs/InviteDialog";
import { InviteKind } from "~matrix-react-sdk/src/components/views/dialogs/InviteDialogTypes";
import DMRoomMap from "~matrix-react-sdk/src/utils/DMRoomMap";
import SdkConfig from "~matrix-react-sdk/src/SdkConfig";
import { ValidatedServerConfig } from "~matrix-react-sdk/src/utils/ValidatedServerConfig";
import { IConfigOptions } from "~matrix-react-sdk/src/IConfigOptions";
import { SdkContextClass } from "~matrix-react-sdk/src/contexts/SDKContext";
import { IProfileInfo } from "~matrix-react-sdk/src/hooks/useProfileInfo";
import Modal from "~matrix-react-sdk/src/Modal";
import {
filterConsole,
flushPromises,
getMockClientWithEventEmitter
} from "~matrix-react-sdk/test/test-utils";

const getSearchField = () => screen.getByTestId("invite-dialog-input");

const enterIntoSearchField = async (value: string) => {
const searchField = getSearchField();
await userEvent.clear(searchField);
await userEvent.type(searchField, value + "{enter}");
};

const pasteIntoSearchField = async (value: string) => {
const searchField = getSearchField();
await userEvent.clear(searchField);
searchField.focus();
await userEvent.paste(value);
await userEvent.type(searchField, value + "{enter}");
};


const roomId = "!111111111111111111:example.org";
const aliceId = "@alice:example.org";
const aliceEmail = "[email protected]";
const aliceUppercaseEmail = "[email protected]";


const aliceProfileInfo: IProfileInfo = {
user_id: aliceId,
display_name: "Alice",
};

const bobId = "@bob:example.org";
const bobProfileInfo: IProfileInfo = {
user_id: bobId,
display_name: "Bob",
};

describe("InviteDialog", () => {
let mockClient: Mocked<MatrixClient>;
let room: Room;

filterConsole(
"Error retrieving profile for userId @carol:example.com",
"Error retrieving profile for userId @localpart:server.tld",
"Error retrieving profile for userId @localpart:server:tld",
"[Invite:Recents] Excluding @alice:example.org from recents",
);

beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(bobId),
getSafeUserId: jest.fn().mockReturnValue(bobId),
isGuest: jest.fn().mockReturnValue(false),
getVisibleRooms: jest.fn().mockReturnValue([]),
getRoom: jest.fn(),
getRooms: jest.fn(),
getAccountData: jest.fn(),
getPushActionsForEvent: jest.fn(),
mxcUrlToHttp: jest.fn().mockReturnValue(""),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getProfileInfo: jest.fn().mockImplementation(async (userId: string) => {
if (userId === aliceId) return aliceProfileInfo;
if (userId === bobId) return bobProfileInfo;

throw new MatrixError({
errcode: "M_NOT_FOUND",
error: "Profile not found",
});
}),
getIdentityServerUrl: jest.fn(),
searchUserDirectory: jest.fn().mockResolvedValue({}),
lookupThreePid: jest.fn(),
registerWithIdentityServer: jest.fn().mockResolvedValue({
access_token: "access_token",
token: "token",
}),
getOpenIdToken: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
supportsThreads: jest.fn().mockReturnValue(false),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
getClientWellKnown: jest.fn().mockResolvedValue({}),
});
SdkConfig.put({ validated_server_config: {} as ValidatedServerConfig } as IConfigOptions);
DMRoomMap.makeShared(mockClient);
jest.clearAllMocks();

room = new Room(roomId, mockClient, mockClient.getSafeUserId());

jest.spyOn(DMRoomMap.shared(), "getUniqueRoomsWithIndividuals").mockReturnValue({
[aliceId]: room,
});
mockClient.getRooms.mockReturnValue([room]);
mockClient.getRoom.mockReturnValue(room);

SdkContextClass.instance.client = mockClient;
});

afterEach(() => {
Modal.closeCurrentModal();
SdkContextClass.instance.onLoggedOut();
SdkContextClass.instance.client = undefined;
});

afterAll(() => {
jest.restoreAllMocks();
});


it("should entered values as lowercase", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});

render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);

const input = getSearchField();
input.focus();

// Type and enter
await enterIntoSearchField(aliceUppercaseEmail);

// Because it has been transoformed to lowercase, it shoyld not be found in the document
expect(screen.queryByText(aliceUppercaseEmail)).not.toBeInTheDocument();

expect(screen.queryByText(aliceEmail)).toBeInTheDocument();

// If it was transformed correctly to a pill, the input should have no value
expect(input).toHaveValue("");
});

it("should add pasted email values as lowercase", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});

render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);

// Juste paste some values without enter
await pasteIntoSearchField(aliceUppercaseEmail);

await flushPromises();

// Because it has been transoformed to lowercase, it shoyld not be found in the document
expect(screen.queryByText(aliceUppercaseEmail)).not.toBeInTheDocument();

// contrary to the entered values, on this paste test we don't enter, so we have multiple result
expect(screen.queryAllByText(aliceEmail)[0]).toBeInTheDocument();

});


it("should not crash if empty values are entered", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});

render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);

const input = getSearchField();
input.focus();

// Type and enter
await enterIntoSearchField("");

expect(input).toHaveValue("");

})

it("should not crash if empty values are pasted", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});

render(<InviteDialog kind={InviteKind.Invite} roomId={roomId} onFinished={jest.fn()} />);
const input = getSearchField();

// Juste paste some values without enter
await pasteIntoSearchField("");

expect(input).toHaveValue("");
})

});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3146,6 +3146,11 @@
"@testing-library/dom" "^8.0.0"
"@types/react-dom" "<18.0.0"

"@testing-library/user-event@^14.5.2":
version "14.5.2"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd"
integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==

"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
Expand Down

0 comments on commit 81796e8

Please sign in to comment.