Git Product home page Git Product logo

slate-test-utils's Introduction

Slate Test Utils

A toolkit to test Slate rich text editors with Jest, React Testing Library, and hyperscript! Write user centric integration tests with ease. Read the announcement.

  • ๐Ÿš€ Works with Jest, React Testing Library, and JSDOM (Create React App and Vite friendly)
  • ๐Ÿ™ Out of the box support for testing: typing, selection, keyboard events, beforeInput events, normalization, history, operations,
  • ๐Ÿฃ Stage editor state using Hyperscript instead of manual mocking or creating a Storybook story per state
  • ๐Ÿ• Stage tests with a mocked collapsed, expanded, or reverse expanded selection
  • โœ… Supports any Slate React editor
  • ๐ŸŽฉ Beautiful diffs on failing tests
  • โš™๏ธ Supports any number of nodes and custom data structures
  • ๐ŸŒŠ Supports emulating Windows and Mac for OS specific testing
  • ๐Ÿ’ƒ Conversational API that makes testing complex workflows easy
  • ๐Ÿฆ† Test variants of your editor with the same test
  • ๐Ÿ“ธ Snapshot testing friendly (if you're into that kinda thing)
  • ๐Ÿ‘” Fully typed with TypeScript

Want to learn more about Slate? Join the newsletter.

https://user-images.githubusercontent.com/13633613/168461296-8e72eae3-9438-4f81-9153-20265e97a3f4.mp4 Created with Wave Snippets

Example

To see full examples go to example/.

/** @jsx jsx */

import { assertOutput, buildTestHarness } from '../../dist/esm'
import { RichTextExample } from './Editor'
import { jsx } from './test-utils'
import { fireEvent } from '@testing-library/dom'

it('user inserts an bulleted list with a few items', async () => {
  const input = (
    <editor>
      <hp>
        <htext>
          <cursor />
        </htext>
      </hp>
    </editor>
  )

  const [
    editor,
    { type, pressEnter, deleteBackward, triggerKeyboardEvent },
    { getByTestId },
  ] = await buildTestHarness(RichTextExample)({
    editor: input,
  })

  // Click the unordered list button in the nav
  const unorderedList = getByTestId('bulleted-list')
  fireEvent.mouseDown(unorderedList)

  await type('๐Ÿฅ•')
  await deleteBackward()
  await type('Carrots')

  assertOutput(
    editor,
    <editor>
      <hbulletedlist>
        <hlistitem>
          <htext>
            Carrots
            <cursor />
          </htext>
        </hlistitem>
      </hbulletedlist>
    </editor>,
  )
})

Motivation

Rich text editors are hard. What makes them harder is being able to test them in a way the gives you confidence that your code works as expected. There's so many user input mechanisms, edge cases, selection, state, normalization, and more to keep in mind when developing.

You could do an end to end testing framework, but even those aren't without struggles, not to mention they're slow and another piece of infrastructure to worry about. Additionally, mocking up every what if scenario becomes difficult because generating the test states takes time. Even if you manage to get it all set up, it's hard to see the diff on your breaking tests unlike Jest that has a fantastic reporter for diffing JSON (what Slate state serializes to by default).

After trying, E2E tests, no tests (don't recommend ๐Ÿ˜…), and unit tests like Slate core nothing seemed to give me enough confidence and convenience that my was code working as intended.

This is where the Slate Test Utils come in! It's an abstraction that uses hyperscript to generate editor states that can be tested in a JSDOM environment with a bit of black magic.

My hope is that by providing a better way to test, everyone can deliver better editor experiences. I also hope that this helps get Slate-React to a stable 1.0 by providing a way to test it internally.

Testing ContentEditable in JSDOM?

It's well documented that JSDOM does not support contenteditable, the API that Slate is built on top of. JSDOM is a mocked DOM that you run your tests again when using Jest. However, since Slate has done an amazing job saving us from the darkness of working with contenteditable directly there's an opportunity to test a large part of the internal Slate-React API and in turn, our code.

That opportunity is what this library takes advantage of. There's some big limitations with this testing approach, but I would estimate that it has covered over 90% of my testing needs and has completely changed how I write Slate code.

Installation

The installation to make this work in your environment is going to be a ๐Ÿป bear, I apologize in advance. Test environments are always difficult to setup.

Prerequisites

Make sure you have Jest, React Testing Library, and React Testing Library DOM configured.

  1. Setup Jest
  2. Setup React Testing Library
  3. Add Patch Package

Install Slate Test Utils

yarn add -D slate-test-utils

# Or

npm install -D slate-test-utils

Now this is where the black magic comes into play. We need to patch your node_modules with some things that will make JSDOM play friendly with our test harness. Go to this repo and find the /patches folder and copy them into a /patches folder at the root of your repo. Once you have done that...

yarn install

# Or

npm install

That should apply your patches to your node_modules. You may get a warning if the versions mismatch, but long as you don't get an error you are good to go. If you get an error you will need to manually create your own patches based off the ones in this repo.

Lastly, you need to add this line to your setupTests.js file for Jest so we can mock things.

import 'slate-test-utils/dist/cjs/mocks'

// or if you are in commonjs

require('slate-test-utils/dist/cjs/mocks')

Configuring Your Hyperscript

The schemaless core of Slate is truly amazing and is fully supported with slate-test-utils. Since we cannot know what your editor's structure is like you need to configure your own hyperscript. Create a file called testUtils or slateTestUtils and fill out what your document looks like.

// @ts-ignore - Imports will be there from the upstream patch
import { createHyperscript, createText } from 'slate-hyperscript'
/**
 * This is the mapping for the JSX that creates editor state. Add to it as needed.
 * The h prefix isn't needed. It's added to be consistent and to let us know it's
 * hyperscript.
 */
export const jsx = createHyperscript({
  elements: {
    // Add any nodes here with any attributes that's required or optional
    hp: { type: 'paragraph' },
    hbulletedlist: { type: 'bulleted-list' },
    hlistitem: { type: 'list-item' },
    inline: { inline: true },
    block: {},
    wrapper: {},
  },
  creators: {
    htext: createText,
  },
})

Typescript

If you are using TypeScript you need to let the compiler know about your custom JSX types. Within your /src directory add a hyperscript.d.ts file.

declare namespace JSX {
  interface Element {}
  interface IntrinsicElements {
    hp: any
    editor: any
    htext: {
      // These optional params will show up in the autocomplete!
      bold?: boolean
      underline?: boolean
      italic?: boolean
      children?: any
    }
    hbulletedlist: any
    hlistitem: any
    cursor: any
    focus: any
    anchor: any
  }
}

Making your Editor Test Friendly

For this to work, your RichTextEditor component has to accept two props:

  • editor: This is an editor singleton that the test harness creates and passed into your editor. It's what the hyperscript creates for you.
  • initialValue: This is the editor.children from the editor singleton.

Your editor call-site will look something like this to make it test friendly:

const emptyEditor: Descendant[] = [
  {
    type: 'paragraph',
    children: [{ text: '' }],
  },
]

export const RichTextExample: FC<{
  editor?: Editor
  initialValue?: Descendant[]
}> = ({
  editor: mockEditor,
  initialValue = emptyEditor,
}) => {
  // Starts with a default value same as usual except now we can stage
  // in one for our testing.
  const [value, setValue] = useState<Descendant[]>(initialValue)
  const editor = useMemo(() => createEditor(), [])

Last step, you need to add a data-testid to your Editable component.

  <Editable
    data-testid="slate-content-editable"

Testing

With your editor configured you should be good to go! Check out /example for a bunch of tests and patterns.

For all tests make sure you add this to the top:

/** @jsx jsx */

import { jsx } from '../test-utils'

The first line sets the pragma that will parse your hyperscript. The second line will import the pragma.

API

The test utils export a few methods that help you create user centric tests for your editor.

BuildTestHarness

A test harness for the RichTextEditor that adds custom queries to assert on, lots of simulated actions, and a custom rerender in case you want to assert on the DOM. In most cases, you'll want to assert directly on the editor state to check that the editor selection and other pieces of the editor are working as intended.

Your first invocation of the test harness needs to be a React component.

const richTextHarness = buildTestHarness(RichTextExample)

Tip! You can partially apply the buildTestHarness function to create a bunch of test harnesses per variant of your editor.

Next, you need to pass in the config to render that component. You must pass an editor anything else is optional. You are returned a tuple of props. The first is going to be the editor you passed into the harness. The second is going to be commands for testing. The third is custom queries for asserting and the bag of props from render in React Testing Library.

Config

Use these properties to customize the testHarness

/**
 * A Slate editor singleton.
 */
editor: any
/**
 * Pretty logs out all operations on the editor so you can see what's going on in tests.
 */
debug?: boolean
/**
 * Ensures Slate content is valid before rendering. This is not turned on by default
 * because you may want to test invalid states for normalization or testing purposes.
 *
 * @default false
 */
strict?: boolean
/**
 * Props you would like to pass down to the element you have passed in to test. This could be disabled states
 * variants, specific styles, or anything else!
 */
componentProps?: any

/**
 * The test ID for the Editable component that is used
 * to run the test harness.
 *
 * @default 'slate-content-editable'
 */
testID?: string
const [editor, { triggerKeyboardEvent, type }] = await buildTestHarness(
  RichTextExample,
)({
  editor: input,
})

Most of your call-sites will look like this:

const [editor, { triggerKeyboardEvent, type }] = await buildTestHarness(
  RichTextExample,
)({
  editor: input,
})

// Or this, same thing except with this you can reuse the first part of the function!
const richTextHarness = buildTestHarness(RichTextExample)

const [editor, { triggerKeyboardEvent, type }] = await richTextHarness({
  editor: input,
})

Commands

These commands are what you can use to interact with your rendered editor

type: (s: string) => Promise<void>
deleteForward: () => Promise<void>
deleteBackward: () => Promise<void>
deleteEntireSoftline: () => Promise<void>
deleteHardLineBackward: () => Promise<void>
deleteSoftLineBackward: () => Promise<void>
deleteHardLineForward: () => Promise<void>
deleteSoftLineForward: () => Promise<void>
deleteWordBackward: () => Promise<void>
deleteWordForward: () => Promise<void>
paste: (payload: string, ?{ types: 'text/html' | 'text/plain' | 'image/png'[] }) => Promise<void>
pressEnter: () => Promise<void>
/**
 * Use a hotkey combination from is-hotkey. See testHarness internals
 * for usage.
 */
triggerKeyboardEvent: (hotkey: string) => Promise<void>
typeSpace: () => Promise<void>
undo: () => Promise<void>
redo: () => Promise<void>
selectAll: () => Promise<void>
isApple: () => boolean
rerender: () => void

Queries

The third param is the bag of props returned from render. It includes some helper queries for Slate and all of the default methods returned from React Testing Library.

Test Runner

The test runner will run your tests simulated in iOS and Windows environments by mocking the user agent. This is useful for testing keyboard events and other OS specific functionality. Refer to example/src/tests/mac-windows.test.tsx for usage.

Running Example Folder

Run the example project to see it in action and get an idea of some fun patterns you can include in your testing.

git clone https://github.com/mwood23/slate-test-utils

cd slate-test-utils

yarn install && yarn build

cd example

yarn install

yarn test

Limitations

There are some big limitations to this approach when testing your editor. You will not be able to test 100% of the behavior of your editor with this framework, so manual testing or E2E tests will be needed depending on your use case.

  • Any contenteditable event that is not handled by your or Slate React will not work. For example, if you fire the key down arrowLeft, nothing will happen unless you handle that event specifically because contenteditable is not fully supported.
  • We are using our own jsx pragma to parse the tests so you will not be able to use React components in the same file. That's the reason in the test harness we have a componentProps field that lets you put in any amount of custom props you need to test.
  • React 17.x will work, but if you use TypeScript, you may run into problems parsing your tests because of how it works. If you do, you will need to create a tsconfig specific for your tests with "jsx": "react".
  • Jest 26/27 are supported depending on your version. It is important to note that since we patch JSDOM, you need to make sure that the patch files will work.
  • Slate 0.70.0 is the only officially supported version although I have tested this all the way to version 0.59.0. Since Slate is beta, your mileage may vary. Please open an issue if you see anything weird.

Unknown Support

There is a lot to support with Slate. I'm not sure if these will work or not because I haven't needed to use them often enough to know. Open to PRs to add this functionality or example usages!

  1. Copy-paste
  2. Void elements

Errors

  • If you get an error about DataTransfer not being defined then you haven't imported the slate-test-utils mock correctly
  • If you get an error about your patch file make sure your versions are consistent with the /example file or create a patch specific to that version
  • If your editor does not appear to be updating from your tests make sure you have made your editor test friendly
  • If you get an error in your test about your hyperscript not being correct or un-parsable make sure you are importing the pragma and your built hyperscript

FAQ

  • Could I use this with ProseMirror? I suppose you could depending on how they handle their events under the hood.

TODO

  • PR the patches to the respective repos, especially the Slate ones.
  • Write tests in Slate-React using the test utils?

Contributing

Any and all PRs, issues, and ideas for improvement welcomes!

slate-test-utils's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

slate-test-utils's Issues

Cannot add property deleteBackward, object is not extensible when creating editor

Thanks for this library - feels like it could be the solution to so many problems ! I'm having trouble getting a minimal test setup with an editor.

When I create the editor using withHistory(withReact(mockEditor ?? createEditor()) I end up getting the error TypeError: Cannot add property deleteBackward, object is not extensible

Any idea what might be happening there ?

Versions:
React: 17.0.2
Slate: 0.78.0
Slate-React: 0.79.0

Cannot find module 'lodash.clonedeep' from 'node_modules/slate-test-utils/dist/cjs/ensureSlateValid.js'

I tried cloning your example, basically as is, and I'm getting errors on npm test.

eg

npm test

> [email protected] test
> jest --verbose

 FAIL  src/tests/ensure-valid.test.tsx
  โ— Test suite failed to run

    Cannot find module 'lodash.clonedeep' from 'node_modules/slate-test-utils/dist/cjs/ensureSlateValid.js'

    Require stack:
      node_modules/slate-test-utils/dist/cjs/ensureSlateValid.js
      node_modules/slate-test-utils/dist/cjs/buildTestHarness.js
      node_modules/slate-test-utils/dist/cjs/index.js
      src/tests/ensure-valid.test.tsx

      at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:324:11)
      at Object.<anonymous> (node_modules/slate-test-utils/dist/cjs/ensureSlateValid.js:5:28)

You can find an example repo here.
https://github.com/rzachariah/slate-example

Paste slate fragments

I have this test:

const input = (
      <editor>
        <hpage>
          <hp>1</hp>
          <hp>
            2<cursor />
          </hp>
        </hpage>
      </editor>
    ) as any as TEditor

    const [editor] = await buildTestHarness(PlateTest)({
      editor: createPlateEditor({
        editor: input,
        plugins: [createPagePlugin()],
      }),
    })

    editor.insertFragment(
      <fragment>
        <hpage>
          <hp>3</hp>
        </hpage>
        <hpage>
          <hp>4</hp>
        </hpage>
      </fragment>,
    )

    const output = (
      <editor>
        <hpage>
          <hp>1</hp>
          <hp>23</hp>
          <hp>4</hp>
        </hpage>
      </editor>
    )

    assertOutput(editor, output)

The editor diff is confusing me:

Expected: null
Received: {"anchor": {"offset": 1, "path": [0, 2, 0]}, "focus": {"offset": 1, "path": [0, 2, 0]}}

Automatic JSX runtime

Hello, thanks for coming up with this clever solution. Testing w/ Slate is critical yet very much unsolved, or at least it was until this came out! Where I work, we've been using Cypress w/ several hacks in place to test Slate. This is much faster, neater abstraction.

I was wondering if you had any idea how to enable the jsx extensions with an already set importSource using the automatic JSX runtime.

We're using Emotion, which requires us to set the pragma importSource to @emotion/react.

It would be so awesome if we could use the JSX using what's described in this packages README in addition to Emotion.

I'm not blocked, I can just not use JSX syntax and instead use the function exported from slate-hyperscript:

    jsx('editor', {}, [
      jsx('element', { type: 'paragraph' }, [
        jsx('text', ['potato']),
        jsx('text', { bold: true }, [' cucumbers', jsx('cursor')]),
      ]),
    ]),

Feel free to close this issue if it's off topic or you're not sure what the solution would look like.

Thanks again for creating and open sourcing this package!

Problems running example tests

Hi! I'm trying to setup this package for Slate tests, following the readme and example I ran into some issues. So I decided to run example tests and adjust my code but they failed with a hook issue:

 FAIL  src/tests/Editor.test.tsx
  Mac
    โœ• user types into an empty editor (72 ms)
    โœ• user types multiple paragraphs into the editor (11 ms)
    โœ• user types with an expanded selection across paragraphs (8 ms)
  Windows
    โœ• user types into an empty editor (10 ms)
    โœ• user types multiple paragraphs into the editor (7 ms)
    โœ• user types with an expanded selection across paragraphs (8 ms)

  โ— Mac โ€บ user types into an empty editor

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

      49 |   initialValue = emptyEditor,
      50 | }) => {
    > 51 |   const [value, setValue] = useState<Descendant[]>(initialValue)
         |                                     ^
      52 |   const renderElement = useCallback((props) => <Element {...props} />, [])
      53 |   const renderLeaf = useCallback((props) => <Leaf {...props} />, [])
      54 |   const editor = useMemo(

      at resolveDispatcher (node_modules/react/cjs/react.development.js:1476:13)
      at useState (node_modules/react/cjs/react.development.js:1507:20)
      at RichTextExample (src/Editor.tsx:46:37)

According to React docs I checked react and react-dom versions - they both are 16.8.0, I tried to upgrade to recent 17.0.2 - still same error. The component is functional and hooks are called on top-level so that one should be fine. For different copies of React I also did suggested check and it also shows everything is fine.

npm ls react
[email protected] >slate-test-utils/example
โ”œโ”€โ”ฌ @testing-library/[email protected]
โ”‚ โ”œโ”€โ”ฌ [email protected]
โ”‚ โ”‚ โ””โ”€โ”€ [email protected] deduped
โ”‚ โ””โ”€โ”€ [email protected]
โ””โ”€โ”ฌ [email protected]
  โ””โ”€โ”€ [email protected] deduped

Not sure what is going on, maybe you have any ideas?

Is this still being maintained?

Hey, love the project, it's very much needed in the slatejs space. I was just wondering if this is still being actively maintained? I've recently updated to jest 28 with jest-environment-jsdom and it completely broke the editor tests. Are there any plans for supporting newer versions of jest?

Support @testing-library/react v13.x

[email protected] depends on @testing-library/react v12. Trying to install slate-test-utils with @testing-library/react v13 fails.

$ npm i --save-dev slate-test-utils                                                                                                                     
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: [email protected]
npm ERR! Found: @testing-library/[email protected]
npm ERR! node_modules/@testing-library/react
npm ERR!   dev @testing-library/react@"13.3.0" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer @testing-library/react@"^12.1.2" from [email protected]
npm ERR! node_modules/slate-test-utils
npm ERR!   dev slate-test-utils@"*" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
  • slate-test-utils: 1.3.2
  • testing-library/react: 13.3.0
  • node: 14.18.2
  • npm: 7.24.2

Image test

Hi, first I like the way I can test snapshots with this framework.

How can I test user interaction involving adding image to the editor being tested?

Upgrade slate and slate-react version

Hello! First of all, congratulations and thank you very much for this utility. The library looks really promising for writing meaningful test easily, specially when compared with the alternatives.

Opening this issue because it would be nice to keep the slate and slate-react dependencies versions updated. I skimmed through the code and Slate's changelog and since 0.70.0 (current dependency version) there doesn't seem to be any critical changes for this library. Might be useful to use hyphen ranges 0.70.0-0.72.x to keep supporting at least the most recent patch on current minor version.

Thanks again for open sourcing this!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.