Search code examples
reactjsunit-testingtestingjestjsreact-testing-library

No text displays after simulating typing change on textarea in Jest/Testing-library


I am testing a component that has a textarea element. I grab the element using screen.getByRole('textbox') and attempt to type into it using await userEvent.type(textbox, loremText) where loremText is a variable holding text.

The Jest test passes, but the text is never entered into the textarea element. Why is this happening?

Here is the test:

import { screen, render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import TextEditor from '../components/textEditor'

const loremText =
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'

const renderTextEditor = (content, setContent, autoFocus) => {
    return render(
        <TextEditor
            content={content}
            setContent={setContent}
            autoFocus={autoFocus}
        />
    )
}

test('type text into textbox', async () => {
    renderTextEditor('content', jest.fn(), true)
    const textbox = screen.getByRole('textbox')
    await userEvent.type(textbox, loremText)
    screen.debug(textbox)
})

Here is the Component that has the textarea element:

import { useState } from 'react'
import MarkdownIt from 'markdown-it'

type Props = {
    content: string
    setContent: (value: string) => void
    autoFocus?: boolean
}

enum TextStyle {
    Bold,
    Italic,
    Strike,
    Code,
    Quote,
    List,
}

const markdown = new MarkdownIt({
    breaks: true,
    html: false,
    linkify: true,
})

const TextEditor = ({ content, setContent, autoFocus }: Props) => {
    const [isPreview, setIsPreview] = useState<boolean>(false)
    const [contentHtml, setContentHtml] = useState<string>(
        markdown.render(content)
    )

    const handleContentInput = (event: any) => {
        setContent(event.target.value)
        setContentHtml(markdown.render(event.target.value))
    }

    const addListItem = (
        previousText: string,
        text: string,
        start: number,
        end: number
    ): string => {
        const listIndex = text.indexOf('\n')

        if (end === 0) {
            return previousText + text
        }
        if (listIndex === -1 || listIndex > end) {
            return previousText + '* ' + text
        }
        if (listIndex < start) {
            return addListItem(
                previousText + text.substring(0, listIndex + 1),
                text.substring(listIndex + 1),
                Math.max(start - listIndex - 1, 0),
                Math.max(end - listIndex - 1, 0)
            )
        }
        return addListItem(
            previousText + '* ' + text.substring(0, listIndex + 1),
            text.substring(listIndex + 1),
            Math.max(start - listIndex - 1, 0),
            Math.max(end - listIndex - 1, 0)
        )
    }

    const addStyle = (style: TextStyle) => {
        const sel = document.getElementById(
            'inputTextArea'
        ) as HTMLTextAreaElement
        const start = sel.selectionStart
        const end = sel.selectionEnd

        let newContent: string = content
        if (style === TextStyle.Italic) {
            newContent =
                content.substring(0, start) +
                '*' +
                content.substring(start, end) +
                '*' +
                content.substring(end)
        } else if (style === TextStyle.Bold) {
            newContent =
                content.substring(0, start) +
                '**' +
                content.substring(start, end) +
                '**' +
                content.substring(end)
        } else if (style === TextStyle.Strike) {
            newContent =
                content.substring(0, start) +
                '~~' +
                content.substring(start, end) +
                '~~' +
                content.substring(end)
        } else if (style === TextStyle.Code) {
            newContent =
                content.substring(0, start) +
                '`' +
                content.substring(start, end) +
                '`' +
                content.substring(end)
        } else if (style === TextStyle.Quote) {
            newContent =
                content.substring(0, start) +
                '> ' +
                content.substring(start, end) +
                '\n' +
                content.substring(end)
        } else if (style === TextStyle.List) {
            newContent = addListItem('', content, start, end)
        }
        setContent(newContent)
        setContentHtml(markdown.render(newContent))
    }

    const insertImage = () => {
        const sel = document.getElementById(
            'inputTextArea'
        ) as HTMLTextAreaElement
        const start = sel.selectionStart
        const end = sel.selectionEnd
        const newContent =
            content.substring(0, start) +
            `![${content.substring(
                start,
                end
            )}](https://paste-image-url-here.jpg|png|svg|gif)` +
            content.substring(end)
        setContent(newContent)
        setContentHtml(markdown.render(newContent))
    }

    const insertLink = () => {
        const sel = document.getElementById(
            'inputTextArea'
        ) as HTMLTextAreaElement
        const start = sel.selectionStart
        const end = sel.selectionEnd
        const newContent =
            content.substring(0, start) +
            `[${content.substring(start, end)}](https://)` +
            content.substring(end)
        setContent(newContent)
        setContentHtml(markdown.render(newContent))
    }

    return (
        <div>
            <div>
                <div className="buttons">
                    <div className="group basic">
                        <button onClick={() => addStyle(TextStyle.Bold)}>
                            <img
                                src={require('../../public/images/bold.svg')}
                            />
                        </button>
                        <button onClick={() => addStyle(TextStyle.Italic)}>
                            <img
                                src={require('../../public/images/italic.svg')}
                            />
                        </button>
                        <button onClick={() => addStyle(TextStyle.Strike)}>
                            <img
                                src={require('../../public/images/strike.svg')}
                            />
                        </button>
                        <button onClick={() => addStyle(TextStyle.Code)}>
                            <img
                                src={require('../../public/images/codeblock.svg')}
                            />
                        </button>
                        <button onClick={() => addStyle(TextStyle.Quote)}>
                            <img
                                src={require('../../public/images/quote.svg')}
                            />
                        </button>
                        <button onClick={() => addStyle(TextStyle.List)}>
                            <img
                                src={require('../../public/images/bullet.svg')}
                            />
                        </button>
                        <button onClick={insertLink}>
                            <img
                                src={require('../../public/images/link.svg')}
                            />
                        </button>
                        <button onClick={insertImage}>
                            <img src={require('../../public/images/img.svg')} />
                        </button>
                    </div>
                    <div className="group">
                        <button
                            onClick={() =>
                                window.open(
                                    'https://commonmark.org/help/',
                                    '__blank'
                                )
                            }
                        >
                            <img
                                src={require('../../public/images/info_small.svg')}
                            />
                        </button>
                        <button
                            className="button-border"
                            onClick={() => setIsPreview(!isPreview)}
                        >
                            {isPreview ? 'Edit' : 'Preview'}
                        </button>
                    </div>
                </div>
                {isPreview && (
                    <div className="block-content preview-box">
                        <div className="content">
                            <div
                                dangerouslySetInnerHTML={{
                                    __html: contentHtml,
                                }}
                            />
                        </div>
                    </div>
                )}
                {!isPreview && (
                    <textarea
                        id="inputTextArea"
                        onChange={handleContentInput}
                        value={content ?? ''}
                        autoFocus={autoFocus}
                    />
                )}
            </div>
        </div>
    )
}

export default TextEditor

When I use screen.debug(textbox) to see if the text was entered the terminal out is:

<textarea
      id="inputTextArea"
    >
      content
    </textarea>

Solution

  • Here is a cleaner implementation using promises:

    const loremText =
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
    
    const TestWrapper = () => {
        const [content, setContent] = useState('')
        return (
            <TextEditor
                content={content}
                setContent={setContent}
                autoFocus={true}
            />
        )
    }
    
    const renderTextEditor = () => {
        return render(<TestWrapper />)
    }
    
    test('type text into textbox', () => {
        renderTextEditor()
        const textbox = screen.getByRole('textbox')
        // using promises...
        return userEvent
            .type(textbox, loremText)
            .then(() => expect(textbox).toHaveValue(loremText))
    })