DEV Community

Cover image for Testing React DnD by React Testing Library
Ryota Murakami
Ryota Murakami

Posted on

Testing React DnD by React Testing Library

This post show how to testing React DnD Chessboard App with React Testing Library.

Alt Text

full code example is here.
https://github.com/laststance/react-testing-library-react-dnd-chessboard-example

Example Code

  • Knight.tsx
import React from 'react'
import { ItemTypes knightImage} from './Game'
import { useDrag, DragPreviewImage } from 'react-dnd'

const Knight: React.FC = () => {
  const [{ isDragging }, drag, preview] = useDrag({
    item: { type: ItemTypes.KNIGHT },
    collect: (monitor) => ({
      isDragging: !!monitor.isDragging(),
    }),
  })

  return (
    <>
      <DragPreviewImage connect={preview} src={knightImage} />
      <div
        ref={drag}
        style={{
          display: 'block',
          opacity: isDragging ? 0.5 : 1,
          fontSize: '64px',
          fontWeight: 'bold',
          cursor: 'move',
        }}
      ></div>
    </>
  )
}

export default Knight
  • BoardSquare.tsx
Drop area side
import React from 'react'
import Square from './Square'
import Overlay from './Overlay'
import { canMoveKnight, moveKnight, X, Y } from './Game'
import { ItemTypes } from './Game'
import { useDrop } from 'react-dnd'

interface Props {
  x: X
  y: Y
  index: number
}

const BoardSquare: React.FC<Props> = ({ x, y, index, children }) => {
  const black = (x + y) % 2 === 1
  const [{ isOver, canDrop }, drop] = useDrop({
    accept: ItemTypes.KNIGHT,
    drop: () => moveKnight(x, y),
    canDrop: () => canMoveKnight(x, y),
    collect: (monitor) => ({
      isOver: !!monitor.isOver(),
      canDrop: !!monitor.canDrop(),
    }),
  })

  return (
    <div
      role="gridcell"
      ref={drop}
      data-testid={children ? 'KnightPosition: ' + index : index}
      style={{
        position: 'relative',
        width: '100%',
        height: '100%',
      }}
    >
      <Square black={black}>{children}</Square>
      {isOver && !canDrop && <Overlay color="red" data-testid="RedOverlay" />}
      {!isOver && canDrop && (
        <Overlay color="yellow" data-testid="YellowOverlay" />
      )}
      {isOver && canDrop && (
        <Overlay color="green" data-testid="GreenOverlay" />
      )}
    </div>
  )
}

export default BoardSquare
  • integration.test.tsx
import React from 'react'
import '../index.css'
import { render, screen, fireEvent } from '@testing-library/react'
import Board from '../Board'
import { observe, KnightPosition, releaseObserver } from '../Game'

function dragAndDrop(knight: HTMLElement, cell: HTMLElement) {
  fireEvent.dragStart(knight)
  fireEvent.dragEnter(cell)
  fireEvent.dragOver(cell)
  fireEvent.drop(cell)
}

function dragHold(knight: HTMLElement, cell: HTMLElement) {
  fireEvent.dragStart(knight)
  fireEvent.dragEnter(cell)
  fireEvent.dragOver(cell)
}

beforeEach(() => {
  /*
   * Every time Knight initial position: "57"
   * and Knight droppable positions are "40", "42", "51"
   * when you got all cells with screen.getAllByRole('gridcell')
   */
  observe((knightPosition: KnightPosition) =>
    render(<Board knightPosition={knightPosition} />)
  )
})

afterEach(() => {
  releaseObserver()
})

test('should exist Knight with certain visual on board', () => {
  const Knight = screen.getByText('')

  const display = window.getComputedStyle(Knight).getPropertyValue('display')
  const opacity = window.getComputedStyle(Knight).getPropertyValue('opacity')
  const fontSize = window.getComputedStyle(Knight).getPropertyValue('font-size')
  const fontWeight = window
    .getComputedStyle(Knight)
    .getPropertyValue('font-weight')
  const cursor = window.getComputedStyle(Knight).getPropertyValue('cursor')

  expect({
    display: display,
    opacity: opacity,
    fontSize: fontSize,
    fontWeight: fontWeight,
    cursor: cursor,
  }).toStrictEqual({
    display: 'block',
    opacity: '1',
    fontSize: '64px',
    fontWeight: 'bold',
    cursor: 'move',
  })
})

test('should board have 64 cells', () => {
  const boardSquares = screen.getAllByRole('gridcell')
  expect(boardSquares.length).toBe(64) // chessboard ragnge is 8 * 8
})

test("Knight initial position is 'index 57' of all cell array", () => {
  expect(screen.getByTestId('KnightPosition: 57')).toHaveTextContent('')
})

test('testing the moment of dragging hold', () => {
  const knight = screen.getByText('')
  const boardSquares = screen.getAllByRole('gridcell')
  const knightPosition = boardSquares[57]

  dragHold(knight, knightPosition)

  // Yellow cell is knight moving range
  const KnightDropableSquares = screen.getAllByTestId('YellowOverlay')

  // Initially knight can move to 3 position
  expect(KnightDropableSquares.length).toBe(3)

  // Yellow color css check
  KnightDropableSquares.forEach((square) => {
    expect(square).toHaveStyle('backgroundColor: yellow')
  })

  // Red cell is current knight position when hold dragging
  expect(screen.getByTestId('RedOverlay')).toHaveStyle('backgroundColor: red')
})

describe('Knight can drag and drop initial moving range', () => {
  // Knight initially has moving position 'index: 40 42 51' of 64 cell array
  test('gridcell[40]', () => {
    const knight = screen.getByText('')
    const yellowCell40 = screen.getAllByRole('gridcell')[40]
    dragAndDrop(knight, yellowCell40)
    expect(screen.getByTestId('KnightPosition: 40')).toHaveTextContent('')
  })

  test('gridcell[42]', () => {
    const knight = screen.getByText('')
    const yellowCell42 = screen.getAllByRole('gridcell')[42]
    dragAndDrop(knight, yellowCell42)
    expect(screen.getByTestId('KnightPosition: 42')).toHaveTextContent('')
  })

  test('gridcell[51]', () => {
    const knight = screen.getByText('')
    const yellowCell51 = screen.getAllByRole('gridcell')[51]
    dragAndDrop(knight, yellowCell51)
    expect(screen.getByTestId('KnightPosition: 51')).toHaveTextContent('')
  })
})

test('Knight can not drop not yellow cell', () => {
  const knight = screen.getByText('')
  const whiteCell = screen.getByTestId('0')
  const blackCell = screen.getByTestId('1')
  expect(whiteCell.firstChild).toHaveStyle('background-color: white;')
  expect(blackCell.firstChild).toHaveStyle('background-color: black;')

  dragAndDrop(knight, whiteCell)

  expect(screen.getByTestId('KnightPosition: 57')).toHaveTextContent('')

  dragAndDrop(knight, blackCell)

  expect(screen.getByTestId('KnightPosition: 57')).toHaveTextContent('')
})

Problem

React DnD abstract Standard Drag Web API but React Testing Library using Standard Web API to testing browser event(see Firing Event)

I don't know which React DnD API tied together Web API(onDragstart etc),
so might be we need infer those mapping or check by Chrome Devtools debug.

Let's Debug which Browser Event firing

Describe with Chessboard Example App and Chrome Devtools.

1. Open Source tab on Chrome Devtools

And then find out Event Listener Breakpoints pan from furthermost to the right.

Alt Text

2. Check the Event that you should debug

In the following image dragEnd is selected.

This meant setup ready breakpoint whole screen dragEnd event.

Alt Text

3. Doing your debug target action on the Browser

In the following image Browser stopped by breakpoint when you start and stop drag item, and showing triggered Event Listener.

Alt Text

Alt Text

Therefore when you have any behavior that implement by React DnD and you want to write testing with React Testing Library,
you have to investigate which Event Listener tied together with that.

Conclusion

I think this post is a little bit niche topic though, I'm glad to could be helpful for someone who has relevant issue.

And I've thought through that important knowledge is fundamentally and basics things of web technology rather than specific library.

Thank you for reading the article!
See you next time! 🤗

Discussion (0)