Open aidenywl opened 1 year ago
Looked briefly into the source code for 0.11.1, this might be a case of the beforeinput
event somehow not firing in RTL. Codepath: https://github.com/facebook/lexical/blob/c9ab0d10578f610f161a244026d7d1d4194624f2/packages/lexical/src/LexicalEvents.ts#L157
There seems to be a semi-related issue on this here: https://github.com/testing-library/user-event/issues/858
We're currently on an older RTL version ~v12+. I'm not sure whether this is a bug with the library or whether simply upgrading RTL will fix it. If anyone has more information on this please add on!
While I was trying to figure out some way around this problem, I noticed that the react testing library's keyboard was able to write to the contentEditable in this particular case.
it('should type in content editable', async () => {
const App = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
editor.update(() => {
const root = $getRoot()
const text = $createTextNode('existing text')
const paragraph = $createParagraphNode()
paragraph.append(text)
root.getFirstChild()?.replace(paragraph)
})
}, [editor])
return null
}
const component = (
<LexicalComposer initialConfig={{ namespace: 'test', onError: console.error }}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>placeholder</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<App />
</LexicalComposer>
)
render(component)
const container = screen.getByRole('textbox')
screen.debug(container)
await user.click(container)
await user.keyboard('test')
screen.debug(container)
})
It seems that when useEffect called an editor.update()
, the text typed by the keyboard appeared.
hmm @alvarogfn thanks for the code samples! that seems to work though it's because the text is being manually populated on useEffect
by root.getFirstChild()?.replace(paragraph)
(correct me if I'm wrong). It would not work for other nodes that do not have the same replace behaviour.
An FYI is that editors that rely on contenteditable cannot be tested with RTL due to limitations stemming from js-dom.
The usual workaround is to use a browser-automation testing tool such as Cypress or Playwright.
@aiden-low In fact, the editable content just needs to have some content inside it other than the
<div
contenteditable="true"
data-lexical-editor="true"
role="textbox"
spellcheck="true"
style="user-select: text; white-space: pre-wrap; word-break: break-word;"
>
<p>
<br />
</p>
</div>
and then you can "use the type and keyboard events inside the lexical editor to type".
I don't know exactly why of this behavior.
If you initialize the editable content with some content, with a space character for example:
function initialState() {
const root = $getRoot()
const paragraph = $createParagraphNode()
paragraph.append($createTextNode(' '))
root.append(paragraph)
}
const App = () => (
<LexicalComposer initialConfig={{ namespace: 'editor', onError: console.error, editorState: initialState }}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>placeholder</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
</LexicalComposer>
)
render(<App />)
const textbox = await screen.findByRole('textbox')
screen.debug(textbox)
screen debug output:
And using keyboard events with RTL, the content will be typed in that same span.
const textbox = await screen.findByRole('textbox')
await user.type(textbox, 'typing...')
screen.debug(textbox)
screen.debug output (the space is still there):
One difference is that {enter}
events will wrap on the same span
without a br
, in contrast to the lexical in the real editor.
await user.type(textbox, '{enter}{enter}')
browser:
screen.debug output:
The version of packages are:
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.1",
"jest": "^28.1.1",
"lexical": "^0.10.0",
"react": "^18.2.0",
I hope this helps in some way.
I can confirm that with some initial content inside editor, the userevent.type / userevent.keyboard is working.
(edit) Found a workaround, seems like userEvent.type does not trigger input event for contenteditable elements.
const urlInput = await screen.findByRole('textbox')
userEvent.type(urlInput, 'hii')
fireEvent.input(urlInput, { data: 'hii' });
From my testing, using userEvent
in Lexical sometimes modifies the editor and sometimes doesn't. But when it does modify it, it does not do so based on Lexical commands or transformations.
For example, considering this initial state:
<!-- The 'I' represents the cursor -->
<p>aIa</p>
<p>bb</p>
If I run userEvent.keyboard('{enter}');
twice I get:
<!-- in jsdom -->
<p>a
a</p>
<p>bb</p>
<!-- in chromium-->
<p>a</p>
<p></p>
<p>a</p>
<p>bb</p>
If I instead run userEvent.keyboard('{delete}')
twice:
<p>a</p>
<p>bb</p>
It seems that delete doesn't work when it's at the end of a paragraph.
An FYI is that editors that rely on contenteditable cannot be tested with RTL due to limitations stemming from js-dom.
Yes, that's what it seems. But testing in real browsers is too slow. There has to be some way to fix jsdom. I think having userEvent
somehow trigger Lexical commands would be enough. Right now it fires the listeners, but not the commands.
I have written an article on how to write unit or integration tests in the browser in case anyone is interested: https://www.germanjablo.com/p/browser-testing
I think this thread should be closed, as it is a jsdom issue, not a lexical one.
Detailed description is from @doytch here that I'm pasting in this issue.
I've created a new component - let's call it
RichTextEditor
- that renders aLexicalComposer
along with a customEmailAddressPlugin
. Said plugin has a decoratorEmailAddressNode
which tokenizes the email strings like so:Nothing super fancy, think the tokenizing in most email clients' composer's "To" fields.
I'd like to be able to write some tests to verify the functionality from the user's perspective. Eg, select the textarea, press keyboard keys, verify the DOM.
I'm using Remix + Vitest + RTL and quickly ran into issues where it seems like something's not being picked up properly.
The output of
screen.debug()
(an RTL helper) included the following:That output (the
p
andbr
especially) suggests to me that thecontenteditable
is being focused but the keyboard events aren't being registered properly. If I snoop a bit using theOnChangePlugin
, then I can see the onChange is firing four times but there isn't anything in theSerializedEditorState
besides thatp
andbr
either.Question 1: Before I keep digging more into this, is this a known issue with
jsdom
or other headless engines (happy-dom
?)? I noticed that the project uses Cypress and I'm wondering whether that's because the tests I want to write are simply not possible with headless unit tests like I'm using.Question 2: If I am up a creek writing these kinds of unit tests, what's the suggested way of testing custom nodes and plugins? I know there are a bunch of unit tests in the codebase for the builtin nodes/plugins but given the
0.x.y
nature of the project I'm wary about drawing toooo many conclusions (especially after I went down aninert
rabbit hole before reading a PR deprecating it 😅). Are there any exemplars I could pattern my tests off of? If it doesn't exist yet, I'd love to take this as an opportunity to build a "So you want to test your custom node/plugin?" doc.Originally posted by @doytch in https://github.com/facebook/lexical/discussions/2659
Lexical version: 0.11.1, but seems to happen for all previous versions as well. This isn't a recent regression.
Steps To Reproduce
RichTextEditor
componentuserEvent.keyboard
oruserEvent.type
Another example:
The current behavior
The test fails, 'foo' is not found
The expected behavior
'foo' is found in playwright