Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lowercase-invite): automatically lowercase field text in invite input #1037

Merged
merged 1 commit into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -37,5 +37,11 @@
"files": [
"src/components/views/voip/LegacyCallView.tsx"
]
},
"lowercase-invite": {
"issue": "https://github.com/tchapgouv/tchap-web-v4/issues/983",
"files": [
"src/components/views/dialogs/InviteDialog.tsx"
]
}
}
187 changes: 187 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,187 @@
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
Loading