react-beautiful-dnd icon indicating copy to clipboard operation
react-beautiful-dnd copied to clipboard

Add docs on testing

Open alexreardon opened this issue 7 years ago • 20 comments

We are looking to create a markdown file which contains some common testing patterns. Specific focus is around mocking or stubbing out the react-beautiful-dnd components behaviour so that consumers can focus on their own logic

alexreardon avatar Jul 06 '18 03:07 alexreardon

If somebody is keen to pick this up please let me know first to avoid multiple people working on it at the same time 👍

alexreardon avatar Jul 09 '18 04:07 alexreardon

@kentcdodds I thought you might know somebody who would be interested in giving this a crack!

alexreardon avatar Jul 11 '18 05:07 alexreardon

I'd recommend that you actually publish a module that mocks out the API. This way people can contribute and make it better and better :)

kentcdodds avatar Jul 11 '18 05:07 kentcdodds

Can you elaborate a little more?

alexreardon avatar Jul 11 '18 05:07 alexreardon

Or do you have any examples of this?

alexreardon avatar Jul 11 '18 05:07 alexreardon

@alexreardon I am happy to spend some time on it. Currently how do you unit test react-beautiful-dnd wrapped components?

Currently if I do snapshot testing I get

<Connect(Draggable)
  disableInteractiveElementBlocking={false}
  draggableId="id"
  index={0}
  isDragDisabled={true}
>
  <Component />
</Connect(Draggable)>

instead of the Component itself.

Additionally, I would love to override some props like (isDragging) and snapshot that as well

huchenme avatar Nov 07 '18 08:11 huchenme

Feel free to give it a crack @huchenme !

alexreardon avatar Nov 07 '18 08:11 alexreardon

It does not seems like a "good first issue" to me at the moment, looks a bit challenging. Can I have some guides or a list of TODO items? (files I might need to look at / other repositories etc.)

huchenme avatar Nov 07 '18 08:11 huchenme

I have figured out a way to test a draggable:

const component = shallow(<YourComponentWithDraggableInside />);
const draggable = component.find('Connect(Draggable)').first();
const inner = shallow(
  draggable.prop('children')(/* you can put provided and snapshot here */)
).find('YourInnerComponentName');
expect(inner).toMatchSnapshot();

huchenme avatar Nov 09 '18 02:11 huchenme

@alexreardon I agree with @huchenme that this does not seem like the perfect first issue :)

Could you at least provide some ideas on how you test dragging when using this component?

I.e. can you cut and paste some code that shows how you simulate the events needed to drag an element from one draggable and to another (or to an empty one?). From there it is possible to see how to write tests that tests the testes code's interaction with the library.

Regards, tarjei

tarjei avatar Nov 09 '18 12:11 tarjei

Hi again. Some thoughts regarding test strategies and -requirements for testing something that implements RBD. I've been converting old tests from react-dnd today and

  • What you want to test is that your implementation handles anything that comes out of RBD - you should be able to assume that the library itself is bugfree.
  • Thus we want a way to trigger all the states that we can assume will happen during a drag action.

Thus I feel that what is needed is a small set of helpers that ensure that a) RBD is used correctly (f.x. if the correct reference is set - or if the properties have been injected. b) The tester can quicly run through the different relevant states (i.e. dragging, dropping etc).

For a) some simple helpers might be enough - something like assertDraggable(dragableComponent, internalDragableTag).

For b) maybe a context above the DragDropContext that could be used to trigger different phases and drop points. Something like:

const action = myRBDContext.grab('Draggalbe-id-1') // Drop-id-1 is now beeing dragged

// here we can assert that active styles are set as well at that the invocations of onBeforeDragStart and onDragStart do the right things

action.moveTo('DropId')
// triggers onDragUpdate

action.drop('DropId') // triggers the last method

This should be enough to be able to handle various combinations of events that you want to test without tying the events directly to the implementations.

The other strategy would be to generate the correct events that power RBD but I fear that would make everything very complex.



Regards,
Tarjei

tarjei avatar Nov 10 '18 20:11 tarjei

I ended up with this:

export function buildRegistry(page) {
  const registry = {
    droppables: {},
    droppableIds: [],
  }
  page.find(Droppable).forEach(droppable => {
    const { droppableId } = droppable.props()
    registry.droppableIds.push(droppableId)

    const droppableInfo = {
      droppableId,
      draggables: {},
      draggableIds: [],
    }
    registry.droppables[droppableId] = droppableInfo

    droppable.find(Draggable).forEach(draggable => {
      const { draggableId, index, type } = draggable.props()
      const draggableInfo = {
        draggableId,
        index,
        type,
      }
      droppableInfo.draggableIds.push(draggableId)
      droppableInfo.draggables[draggableId] = draggableInfo
    })
  })

  return registry
}

export function simulateDragAndDrop(
  page,
  fromDroppableId,
  draggableIndex,
  toDroppableId,
  toIndex
) {
  const reg = buildRegistry(page)

  if (typeof draggableIndex !== 'number' || typeof toIndex !== 'number') {
    throw new Error('Missing draggableIndex or toIndex')
  }

  if (
    reg.droppableIds.indexOf(fromDroppableId) == -1 ||
    reg.droppableIds.indexOf(toDroppableId) == -1
  ) {
    throw new Error(
      `One of the droppableIds missing in page. Only found these ids: ${reg.droppableIds.join(
        ', '
      )}`
    )
  }

  if (!reg.droppables[fromDroppableId].draggableIds[draggableIndex]) {
    throw new Error(`No element found in index ${draggableIndex}`)
  }
  const draggableId =
    reg.droppables[fromDroppableId].draggableIds[draggableIndex]
  const draggable = reg.droppables[fromDroppableId].draggables[draggableId]
  if (!draggable) {
    throw new Error(
      `No draggable fond for ${draggableId} in fromDroppablas which contain ids : ${Object.keys(
        reg.droppables[fromDroppableId].draggables
      ).join(', ')}`
    )
  }
  const dropResult = {
    draggableId,
    type: draggable.type,
    source: { index: draggableIndex, droppableId: fromDroppableId },
    destination: { droppableId: toDroppableId, index: toIndex },
    reason: 'DROP',
  }
  // yes this is very much against all testing priciples.
  // but it is the best we can do for now :)
  page
    .find(DragDropContext)
    .props()
    .onDragEnd(dropResult)
}

tarjei avatar Nov 12 '18 09:11 tarjei

OK, heres an updated version that also handles nested droppables.

Example usage :

const page = mount(<MyComponent {...props} />)
simulateDragAndDrop(page, 123, 1, 134, 0)

// do asserts here
/* eslint-env jest */
import React from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'

export function withDndContext(element) {
  return <DragDropContext>{element}</DragDropContext>
}

function makeDroppableInfo(droppable) {
  const { droppableId } = droppable.props()
  // console.log('droppableId', droppableId)
  return {
    droppableId,
    draggables: {},
    draggableIds: [],
  }
}

function makeDraggableInfo(draggable) {
  const { draggableId, index, type } = draggable.props()

  const droppable = draggable.closest(Droppable)
  if (droppable.length === 0) {
    throw new Error(`No Droppable found for draggable: ${draggableId}`)
  }

  const { droppableId } = droppable.props()

  // console.log('draggableId', droppableId, draggableId)
  const draggableInfo = {
    droppableId,
    draggableId,
    index,
    type,
  }
  return draggableInfo
}

export function buildRegistry(page) {
  const registry = {
    droppables: {},
    droppableIds: [],
  }
  page.find(Droppable).forEach(droppable => {
    const droppableInfo = makeDroppableInfo(droppable)
    registry.droppableIds.push(droppableInfo.droppableId)
    registry.droppables[droppableInfo.droppableId] = droppableInfo
  })

  page.find(Draggable).forEach(draggable => {
    const draggableInfo = makeDraggableInfo(draggable)
    const { droppableId } = draggableInfo

    registry.droppables[droppableId].draggables[
      draggableInfo.draggableId
    ] = draggableInfo
    registry.droppables[droppableId].draggableIds.push(
      draggableInfo.draggableId
    )
  })

  return registry
}

export function simulateDragAndDrop(
  page,
  fromDroppableId,
  draggableIndex,
  toDroppableId,
  toIndex
) {
  const reg = buildRegistry(page)

  if (
    reg.droppableIds.indexOf(fromDroppableId) == -1 ||
    reg.droppableIds.indexOf(toDroppableId) == -1
  ) {
    throw new Error(
      `One of the droppableIds missing in page. Only found these ids: ${reg.droppableIds.join(
        ', '
      )}`
    )
  }

  if (!reg.droppables[fromDroppableId].draggableIds[draggableIndex]) {
    throw new Error(`No element found in index ${draggableIndex}`)
  }
  const draggableId =
    reg.droppables[fromDroppableId].draggableIds[draggableIndex]
  const draggable = reg.droppables[fromDroppableId].draggables[draggableId]
  if (!draggable) {
    throw new Error(
      `No draggable fond for ${draggableId} in fromDroppablas which contain ids : ${Object.keys(
        reg.droppables[fromDroppableId].draggables
      ).join(', ')}`
    )
  }

  if (typeof draggableId === 'undefined') {
    throw new Error(
      `No draggable found on fromIndex nr ${draggableIndex} index contents:[${reg.droppables[
        fromDroppableId
      ].draggableIds.join(', ')}] `
    )
  }

  const dropResult = {
    draggableId,
    type: draggable.type,
    source: { index: draggableIndex, droppableId: fromDroppableId },
    destination: { droppableId: toDroppableId, index: toIndex },
    reason: 'DROP',
  }
  // yes this is very much against all testing priciples.
  // but it is the best we can do for now :)
  page
    .find(DragDropContext)
    .props()
    .onDragEnd(dropResult)
}

tarjei avatar Nov 13 '18 12:11 tarjei

👋 folks. If you're using react-testing-library, then check out react-beautiful-dnd-test-utils. It currently supports moving a <Draggable /> n positions up or down inside a <Droppable />, which was my use case. Also see react-beautiful-dnd-test-utils-example, which includes an example test. Feedback welcome.

colinrobertbrooks avatar May 09 '19 19:05 colinrobertbrooks

Love this. Can @colinrcummings can you add a PR to include this in the community section?

alexreardon avatar May 10 '19 06:05 alexreardon

Also, this might get a bit easier with our #162 api

alexreardon avatar May 10 '19 06:05 alexreardon

Wrote a few utils for simpler testing, thought I'd share since react-beautiful-dnd is so awesome! You can find it at react-beautiful-dnd-tester.

The idea is to test without having to know the current order.

verticalDrag(thisElement).inFrontOf(thatElement)

Currently doesn't support dragging between lists.

mikewuu avatar Mar 21 '20 17:03 mikewuu

Posting my journey to test RBD components in case it's useful. Rather than testing what happens in my app when drag operations occur, I've been figuring out how to test the other features of my components that are wrapped in DnD.Draggable. If there's going to be comprehensive testing documentation, it would be great to cover both.

My initial solution works like this:

test('renders a Block for each data item', t => {
  const wrapper = shallow(<Editor load_state={State.Loaded} blocks={test_blocks} data={test_data} />);
  const drop_wrapper = wrapper.find('Connect(Droppable)');
  const drop_inner = shallow(drop_wrapper.prop('children')(
    {
      innerRef: '',
      droppableProps: [ ],
    },
    null
  ));

  t.is(test_data.length, drop_inner.at(0).children().length);
});

Here's the relevant app code:

<DnD.DragDropContext onDragEnd={this.cb_reorder}>
  <DnD.Droppable droppableId="d-blocks" type="block">{(prov, snap) => (
    <div ref={prov.innerRef} {...prov.droppableProps}>

      {data.map((data_item, index) => (
        <DnD.Draggable key={`block-${data_item.uid}`} draggableId={`block-${data_item.uid}`} index={index} type="block">{(prov, snap) => (

          <div className="block-list-item" ref={prov.innerRef} {...prov.draggableProps} {...prov.dragHandleProps} style={block_drag_styles(snap, prov)}>
            <Block data_item={data_item} index={index} />
          </div>

        )}</DnD.Draggable>
      ))}

      {prov.placeholder}

    </div>
  )}</DnD.Droppable>
</DnD.DragDropContext>

I was helped by @huchenme's comment above.

I also tried stubbing the relevant components using sinon, but I did not manage to get this to work. A resource explaining if / how stubbing the RBD components is possible would be valuable.

I eventually settled on a simpler approach, because the above can get very for more complex, multiply nested components. I modified the app component to accept mock components as props for DnD.Draggable and ContextConsumer.

function Block(props) {
  const DraggableComponent = props.draggable_component || DnD.Draggable;
  const ContextConsumer = props.consumer_component || MyDataContext;
  const FieldRenderer = props.field_renderer_component || RecursiveFieldRenderer;
  ...
  return (
    <DraggableComponent ...>{(prov, snap) => (
      ...
      <ContextConsumer>((ctx) => (
        <FieldRenderer />
      </ContextConsumer>
      ...
    </DraggableComponent>
  );
}

With a helper to create mock elements, tests are very concise, and much less fragile than the other methods I've tried to test components wrapped in RBD.

function func_stub(child_args) {
  return function ChildFunctionStub(props) {
    return (
      <div>
        {props.children(...child_args)}
      </div>
    );
  };
}

function Stub(props) {
  return (
    <div>
      {props.children}
    </div>
  );
}

function mk_stubbed_block(data_item, blocks) {
  return mount(<Block data_item={data_item} index={0}
                      draggable_component={func_stub([provided, snapshot])}
                      consumer_component={func_stub([{ blocks }])}
                      field_renderer_component={Stub} />);
}

test('Block: warning if invalid block type', t => {
  const wrapper = mk_stubbed_block(test_data[0], [ ]);
  const exp = <h3 className='title is-4'>Warning: invalid block</h3>;
  t.is(true, wrapper.contains(exp));
});

Thanks!

bhallstein avatar Sep 23 '20 12:09 bhallstein

Love react beautiful dnd! However i'm still finding very little to no documentation testing the drag itself.

Seeing this post as a bit old now, anyone have a working example? @mik3u @colinrobertbrooks i've tried both your test util's but did not get it working (tester and test-utils), i wonder if it's just outdated with the newest RBD version or maybe i've done something wrong. My draggable list isn't keyboard accessible at the moment and wonder if that may be the culprit of it not working with your solutions..

I have come across another solution that doesn't look to be included on this thread for anyone that may go this route https://www.freecodecamp.org/news/how-to-write-better-tests-for-drag-and-drop-operations-in-the-browser-f9a131f0b281/

Didn't work for me while testing RBD but maybe it will for others. Will update here if something does end up working!

daanishnasir avatar Jul 19 '22 14:07 daanishnasir

@daanishnasir, react-beautiful-dnd-test-utils relies on RBD's keyboard accessibility.

colinrobertbrooks avatar Jul 19 '22 14:07 colinrobertbrooks