Rich text Editor: Auto-replace plain text emoticons with emoji (#12828)

* Detect autoReplaceEmoji setting

* Add plain text emoticon to emoji replacement for plain and rich text modes of the RTE.

* Use latest wysiwyg

* lint

* fix existing jest tests and docs

* Add unit tests

* Update wysiwyg to fix flakes.

* fix wording of tests and comments

* use useSettingValue
This commit is contained in:
David Langley 2024-08-07 09:39:55 +01:00 committed by GitHub
parent e6835fe9d2
commit 5d16a38b17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 184 additions and 15 deletions

View file

@ -423,6 +423,30 @@ describe("WysiwygComposer", () => {
});
});
describe("When emoticons should be replaced by emojis", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "MessageComposerInput.autoReplaceEmoji") return true;
});
customRender(onChange, onSend);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
});
it("typing a space to trigger an emoji replacement", async () => {
fireEvent.input(screen.getByRole("textbox"), {
data: ":P",
inputType: "insertText",
});
fireEvent.input(screen.getByRole("textbox"), {
data: " ",
inputType: "insertText",
});
await waitFor(() => expect(onChange).toHaveBeenNthCalledWith(3, expect.stringContaining("😛")));
});
});
describe("When settings require Ctrl+Enter to send", () => {
const onChange = jest.fn();
const onSend = jest.fn();

View file

@ -20,6 +20,7 @@ import {
findSuggestionInText,
getMappedSuggestion,
processCommand,
processEmojiReplacement,
processMention,
processSelectionChange,
} from "../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useSuggestion";
@ -34,6 +35,16 @@ function createMockPlainTextSuggestionPattern(props: Partial<Suggestion> = {}):
};
}
function createMockCustomSuggestionPattern(props: Partial<Suggestion> = {}): Suggestion {
return {
mappedSuggestion: { keyChar: "", type: "custom", text: "🙂", ...props.mappedSuggestion },
node: document.createTextNode(":)"),
startOffset: 0,
endOffset: 2,
...props,
};
}
describe("processCommand", () => {
it("does not change parent hook state if suggestion is null", () => {
// create a mockSuggestion using the text node above
@ -67,6 +78,39 @@ describe("processCommand", () => {
});
});
describe("processEmojiReplacement", () => {
it("does not change parent hook state if suggestion is null", () => {
// create a mockSuggestion using the text node above
const mockSetSuggestion = jest.fn();
const mockSetText = jest.fn();
// call the function with a null suggestion
processEmojiReplacement(null, mockSetSuggestion, mockSetText);
// check that the parent state setter has not been called
expect(mockSetText).not.toHaveBeenCalled();
});
it("can change the parent hook state when required", () => {
// create a div and append a text node to it with some initial text
const editorDiv = document.createElement("div");
const initialText = ":)";
const textNode = document.createTextNode(initialText);
editorDiv.appendChild(textNode);
// create a mockSuggestion using the text node above
const mockSuggestion = createMockCustomSuggestionPattern({ node: textNode });
const mockSetSuggestion = jest.fn();
const mockSetText = jest.fn();
const replacementText = "🙂";
processEmojiReplacement(mockSuggestion, mockSetSuggestion, mockSetText);
// check that the text has changed and includes a trailing space
expect(mockSetText).toHaveBeenCalledWith(replacementText);
});
});
describe("processMention", () => {
// TODO refactor and expand tests when mentions become <a> tags
it("returns early when suggestion is null", () => {
@ -334,6 +378,17 @@ describe("findSuggestionInText", () => {
const mentionWithSpaceAfter = "@ somebody";
expect(findSuggestionInText(mentionWithSpaceAfter, 2, true)).toBeNull();
});
it("returns an object for an emoji suggestion", () => {
const emoiticon = ":)";
const precedingText = "hello ";
const mentionInput = precedingText + emoiticon;
expect(findSuggestionInText(mentionInput, precedingText.length, true, true)).toEqual({
mappedSuggestion: getMappedSuggestion(emoiticon, true),
startOffset: precedingText.length,
endOffset: precedingText.length + emoiticon.length,
});
});
});
describe("getMappedSuggestion", () => {
@ -361,4 +416,12 @@ describe("getMappedSuggestion", () => {
text: "command",
});
});
it("returns the expected mapped suggestion when the text is a plain text emoiticon", () => {
expect(getMappedSuggestion(":)", true)).toEqual({
type: "custom",
keyChar: "",
text: "🙂",
});
});
});