atlassian / react-beautiful-dnd

Beautiful and accessible drag and drop for lists with React
https://react-beautiful-dnd.netlify.app
Other
33.36k stars 2.56k forks source link

Add docs on testing #623

Open alexreardon opened 6 years ago

alexreardon commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

kentcdodds commented 6 years ago

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 :)

alexreardon commented 6 years ago

Can you elaborate a little more?

alexreardon commented 6 years ago

Or do you have any examples of this?

huchenme commented 5 years ago

@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

alexreardon commented 5 years ago

Feel free to give it a crack @huchenme !

huchenme commented 5 years ago

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 commented 5 years ago

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();
tarjei commented 5 years ago

@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 commented 5 years ago

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

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 commented 5 years ago

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 commented 5 years ago

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)
}
colinrobertbrooks commented 5 years ago

👋 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.

alexreardon commented 5 years ago

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

alexreardon commented 5 years ago

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

mikewuu commented 4 years ago

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.

bhallstein commented 4 years ago

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!

daanishnasir commented 2 years ago

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!

colinrobertbrooks commented 2 years ago

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