Files
Harrison HealeyandGitHub f0815ae9f8 MM-66659/MM-66832 Remove caretPosition state from AdvancedTextEditor (#34643)
* Mock react-virtualized-auto-sizer in all unit tests to ensure contents are mounted

Without mocking that, anything inside of an `AutoSizer` wouldn't be rendered
at all, leading to weird behaviour like the emoji picker being empty in tests.

* Fix EmojiPickerCategoryRow not having a defaultMessage

This caused any unit tests involving the emoji picker to error unless you
also provided an intlMessages object which is hard to do due to how Jest's
`moduleNameMapper` option is configured currently.

* Fixed incorrect usage of useCallback in EmojiPickerItem

The way this was previously written, we weren't specifying dependencies
properly for those callbacks which lead to some actual performance issues
because of how heavy the emoji picker is, and it also lead to issues where
click handlers would have to be triggered multiple times in unit tests
for some reason.

* MM-66659 Stop using caretPosition in useEditorEmojiPicker

This bug seems to be caused by a race condition between React and Chrome which
only occurs on Windows and only now that we've upgraded to React 18. That race
condition occurs with how we map the selection range in the post textbox to
its `caretPosition` state. That state has always been pretty janky, so I've
decided to remove it.

This also has some other benefits like improving UX by using `execCommand`
which preserves the undo/redo stack when using the emoji picker and simplifying
the code because we no longer have to manually mess with the cursor position.
Also, while I was in there, I fixed some minor weirdness around how whitespace
is handled when adding those emojis.

* MM-66659 Stop using caretPosition in useKeyHandler

* MM-66659 Stop using caretPosition for prefill message

* MM-66659 Remove remaining traces of caretPosition in AdvancedTextEditor

* Fix exclusive test

* Fix "Fixed incorrect usage of useCallback in EmojiPickerItem"

* Fixed incorrect usage of FixedSizeList in emoji picker

This is the actual fix to the issue I thought I fixed previously by changing
how `useCallback` was called in EmojiPickerItem.

It turns out the actual issue is that we were passing a new function as the
child of FixedSizeList on every render cycle which caused it to re-render all
of the rows which somehow also assigned them new DOM Nodes as well. The
EmojiPicker re-renders whenever we mouse over an emoji, so it would re-render
the FixedSizeList and all of its children frequently when mousing over things.

Apart from causing performance issues, that also seems to cause RTL to fail to
click on an emoji the first time you call `userAgent.click` because the first
call would move the mouse which would trigger a re-render, and that would
seemingly cause RTL to forget what to click on.

To fix that, I used a new React context to pass the extra props to the
EmojiPickerCategoryOrEmojiRow. I could've also done that by adding them to the
FixedSizeList's `itemData` prop, but that already contains a lot of heavy data,
and I didn't want to add to that.

* Revert changes to postMessageOnKeyPress

* Run Prettier in playwright folder

* Added docs to new Playwright test

* Address feedback
2025-12-09 14:10:06 +00:00

101 lines
4.3 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator} from '@playwright/test';
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that using the emoji picker adds emojis to the correct place in the post textbox based on the
* location of the keyboard caret, that keyboard focus is moved back to the post textbox correctly, and that the
* keyboard caret is placed in the correct place.
*/
test(
'Should add emoji to post textbox correctly and handle focus/selection correctly',
{tag: '@emoji_picker'},
async ({pw}) => {
// # Initialize a test user
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {channelsPage} = await pw.testBrowser.login(user);
const {emojiGifPickerPopup} = channelsPage;
const {postCreate} = channelsPage.centerView;
// # Navigate to default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open emoji picker from center textbox
await postCreate.openEmojiPicker();
// * Verify emoji picker popup appears
await emojiGifPickerPopup.toBeVisible();
// # Click on an emoji
await emojiGifPickerPopup.clickEmoji('slightly smiling face');
// * Verify emoji picker popup disappears
await emojiGifPickerPopup.notToBeVisible();
// * Verify that the emoji was correctly added to the post textbox, followed by a space
await expectPostCreateState(postCreate.input, ':slightly_smiling_face: ', '');
// # Repeat those steps with another emoji
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('upside down face');
// * Verify that the second emoji was correctly added to the post textbox, also followed by a space
await expectPostCreateState(postCreate.input, ':slightly_smiling_face: :upside_down_face: ', '');
// # Clear the textbox and replace it with some text
await postCreate.writeMessage('ab');
// # Move left so that the caret is in the middle of the text
await postCreate.input.press('ArrowLeft');
// * Verify that the caret is in the right place
await expectPostCreateState(postCreate.input, 'a', 'b');
// # Open the emoji picker again and select another emoji
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('face with raised eyebrow');
// * Verify that the emoji was added with surrounding whitespace and that the caret is placed after that
await expectPostCreateState(postCreate.input, 'a :face_with_raised_eyebrow: ', 'b');
// # Clear the textbox and replace it with some words
await postCreate.writeMessage('this is a test');
// # Move left again so that the caret is between words but after a space
await postCreate.input.press('ArrowLeft');
await postCreate.input.press('ArrowLeft');
await postCreate.input.press('ArrowLeft');
await postCreate.input.press('ArrowLeft');
// * Again, verify that the caret is in the right place
await expectPostCreateState(postCreate.input, 'this is a ', 'test');
// # Open the emoji picker again and select another emoji
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('neutral face');
// * Verify that the emoji was added without an extra space before it
await expectPostCreateState(postCreate.input, 'this is a :neutral_face: ', 'test');
},
);
async function expectPostCreateState(input: Locator, textBeforeCaret: string, textAfterCaret: string) {
// * Verify that the post textbox is focused
await expect(input).toBeFocused();
// * Verify that the text in it is as expected
await expect(input).toHaveValue(textBeforeCaret + textAfterCaret);
// * Verify that the keyboard caret is in the correct place
const selectionStart = await input.evaluate((element: HTMLTextAreaElement) => element.selectionStart);
expect(selectionStart).toEqual(textBeforeCaret.length);
const selectionEnd = await input.evaluate((element: HTMLTextAreaElement) => element.selectionEnd);
expect(selectionEnd).toEqual(textBeforeCaret.length);
}