Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs on testing #623

Open
alexreardon opened this issue Jul 6, 2018 · 20 comments
Open

Add docs on testing #623

alexreardon opened this issue Jul 6, 2018 · 20 comments

Comments

@alexreardon
Copy link
Collaborator

alexreardon commented Jul 6, 2018

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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

@kentcdodds
Copy link

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
Copy link
Collaborator Author

Can you elaborate a little more?

@alexreardon
Copy link
Collaborator Author

Or do you have any examples of this?

@huchenme
Copy link

huchenme commented Nov 7, 2018

@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
Copy link
Collaborator Author

Feel free to give it a crack @huchenme !

@huchenme
Copy link

huchenme commented Nov 7, 2018

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
Copy link

huchenme commented Nov 9, 2018

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
Copy link

tarjei commented Nov 9, 2018

@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
Copy link

tarjei commented Nov 10, 2018

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
Copy link

tarjei commented Nov 12, 2018

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
Copy link

tarjei commented Nov 13, 2018

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
Copy link
Contributor

👋 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
Copy link
Collaborator Author

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

@alexreardon
Copy link
Collaborator Author

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

@mikewuu
Copy link

mikewuu commented Mar 21, 2020

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
Copy link

bhallstein commented Sep 23, 2020

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
Copy link

daanishnasir commented Jul 19, 2022

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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants