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

Programatic dragging #162

Closed
alexreardon opened this issue Nov 4, 2017 · 23 comments
Closed

Programatic dragging #162

alexreardon opened this issue Nov 4, 2017 · 23 comments

Comments

@alexreardon
Copy link
Collaborator

Add the ability to control an entire drag programatically. This opens up a large number of experiences such as:

  • voice controlled dragging
  • virtual reality inputs
  • animated rollbacks of actions
@theneva
Copy link

theneva commented Mar 16, 2018

I'm trying to write an integration test with Cypress that uses events (mousedown, mousemove, and mouseup) to simulate a drag to assert that my form behaves correctly.

I've been completely unable to even get the draggable element to move in the test (and also in more "manual" tests of just firing events from the console in Chrome).

Is what I'm trying to do impossible without this idea implemented?

@CarrierDirectRoss
Copy link

I am unable to programmatically trigger DnD experiences too (in my tests). Are any examples available?

@alexreardon
Copy link
Collaborator Author

Hi @CarrierDirectRoss, interesting link! We have not considered the role of testing in a programatic api. For now you can test it by faking user inputs. We are doing so in some of our tests with puppeteer:

https://github.com/atlassian/react-beautiful-dnd/tree/master/test/browser

@CarrierDirectRoss
Copy link

Thanks for the response @alexreardon. The example definitely helped get me started. My current problem though is that I am able to programmatically initiate a drag on a DragHandle (via spacebar), but cannot move the item using the proper arrow keys for keyboard navigation. Below is a code snippet if you can spot anything wrong (using react-testing-library)

const wrapper = render(<Yard />)
const track1El = wrapper.getByTestId(`droppable-${tracks.t1.id}`)
const beforeDragCars = track1El.querySelectorAll(
  '[data-testid^=draggable-]',
)
const beforeDragFirstText = beforeDragCars[0].textContent
const beforeDragSecondText = beforeDragCars[1].textContent

const dragHandle = getDragHandle(wrapper.container, cars.c1.id)

// initiate drag - This works!
Simulate.keyDown(dragHandle, {
  keyCode: keyCodes.space, // 32
})

const dragCountEl = await waitForElement(() =>
  wrapper.getByTestId('drag-count'),
)
expect(dragCountEl).toHaveTextContent('1')

// move right 1 position in list - This does NOT work
Simulate.keyDown(dragHandle, {
  keyCode: keyCodes.arrowRight, // 39
})

// drop item
Simulate.keyDown(dragHandle, {
  keyCode: keyCodes.space,
})
expect(wrapper.queryByTestId('drag-count')).toBeNull()

track1El = wrapper.getByTestId(`droppable-${tracks.t1.id}`)
const afterDragCars = track1El.querySelectorAll(
  '[data-testid^=draggable-]',
)
const afterDragFirstText = afterDragCars[0].textContent
const afterDragSecondText = afterDragCars[1].textContent

// assert that first & second items have swapped
expect(afterDragSecondText).toBe(beforeDragFirstText)
expect(afterDragFirstText).toBe(beforeDragSecondText)

@gyfchong
Copy link

gyfchong commented May 8, 2018

Would this cover creating our own custom controls that could trigger a DnD experience? ie. up and down arrows which move items up an down a list when clicked.

@alexreardon
Copy link
Collaborator Author

alexreardon commented May 8, 2018 via email

@tim-soft
Copy link

tim-soft commented Jun 7, 2018

This seems like a really cool idea. I'm imagining an API like a chess move i.e. "move a4 to c6"

https://en.m.wikipedia.org/wiki/Algebraic_notation_(chess)

There are really two ideal actions in my mind. You could have the foreshadowing of a drag where the droppable space opens up in the destination list, then commit to the actual drag and the draggable flies into place.

@alexreardon
Copy link
Collaborator Author

Because we have a state driven drag model there are a lot of possibilities

@millarm
Copy link

millarm commented Aug 1, 2018

OK, we hit this same issue this week - we have some complicated business logic that runs when a drop happens, and we wanted to be able to test this programatically with cypress.io

Thumbs up to the idea of providing specific events that enable the scripting of this (in fact, from review yesterday, it looks like this would be reasonably easy - create a new sensor - in drag-handle/sensor that is a "SyntheticEventSensor" that responds to to synthetic events for "dragstart"/"dragmove"/"dragend" which would allow any JS test framework (or application code) that can trigger JS events to simulate dragging, in an explicit way without having to worry about the implementation of the mouse/touch/keyboard interfaces.

Anyway -> for anyone, like me hitting this problem, here's a recipe to make it work - this is now running in our product (test!) environment:

Using cypress.io to simulate a drag event do the following:

(our application is a calendar so we freeze the time of the tests in cypress - hence the use of cy.clock)

    const now = new Date(2018, 4, 11, 9, 0, 0).getTime(); // Freeze the time for the tests
    cy.clock(now);
    cy.visit('/');
    cy
    cy
      .get('.draggable-item-id-from-your-system-here') // or use [data-cy=test-id] for more robust tests
      .trigger('mousedown', { button: 0, clientX: 633, clientY: 440 }) // drag events test for button: 0 and also use the clientX and clientY values - the clientX and clientY values will be specific to your system
      .trigger('mousemove', { button: 0, clientX: 639, clientY: 500 }) // We perform a small move event of > 5 pixels this means we don't get dismissed by the sloppy click detection
      .tick(200); // react-beautiful-dnd has a minimum 150ms timeout before starting a drag operation, so wait at least this long.
    cy
      .get('.top-level-view') // now we perform drags on the whole screen, not just the draggable
      .trigger('mousemove', { button: 0, clientX: 939, clientY: 900 })
      .tick(1); // We've frozen time, so we need at least a 1ms tick to move to lead to the animations being executed. You should see the actual animation in the cypress window
    cy.get('.top-level-view').trigger('mouseup'); // Causes the drop to be run
    // Can now test the application's post DROP state

@CarrierDirectRoss
Copy link

Thank you @millarm. We have been using keyboard events to trigger drags in cypress tests. Now we can finally test all the use cases!

@alexreardon
Copy link
Collaborator Author

Our browser tests are now executing keyboard, mouse and touch dragging in chrome and firefox with test cafe. Feel free to take a look: https://github.com/atlassian/react-beautiful-dnd/blob/master/test/browser/simple-list.js

@alexreardon
Copy link
Collaborator Author

alexreardon commented Aug 1, 2018

Related: #623

@DanceParty
Copy link

Question about dragging while testing...

I am using CRA, and React-Testing-Library fyi

In this example, I can successfully drag and drop an element from the top of a list, to the bottom of the list.

fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Tab',
    keyCode: 9,
    which: 9,
  });

  await wait()

  // click spacebar to grab the item
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Space',
    keyCode: 32,
    which: 32,
  });

  await wait()

  // down arrow to move lower in the list
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'ArrowDown',
    keyCode: 40,
    which: 40,
  });

  await wait();

  // space bar to drop the item
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Space',
    keyCode: 32,
    which: 32,
  });

await wait();

However, I cannot get the item to from from a list into a different list right next to it. (the below code doesn't move the item)

fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Tab',
    keyCode: 9,
    which: 9,
  });

  await wait()

  // click spacebar to grab the item
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Space',
    keyCode: 32,
    which: 32,
  });

  await wait()

  // down arrow to move lower in the list
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'ArrowRight',
    keyCode: 39,
    which: 39,
  });

  await wait();

  // space bar to drop the item
  fireEvent.keyDown(getByTestId('draggable__uniqueId'), {
    key: 'Space',
    keyCode: 32,
    which: 32,
  });

await wait();

@maku-zuhlke
Copy link

OK, we hit this same issue this week - we have some complicated business logic that runs when a drop happens, and we wanted to be able to test this programatically with cypress.io

Thumbs up to the idea of providing specific events that enable the scripting of this (in fact, from review yesterday, it looks like this would be reasonably easy - create a new sensor - in drag-handle/sensor that is a "SyntheticEventSensor" that responds to to synthetic events for "dragstart"/"dragmove"/"dragend" which would allow any JS test framework (or application code) that can trigger JS events to simulate dragging, in an explicit way without having to worry about the implementation of the mouse/touch/keyboard interfaces.

Anyway -> for anyone, like me hitting this problem, here's a recipe to make it work - this is now running in our product (test!) environment:

Using cypress.io to simulate a drag event do the following:

(our application is a calendar so we freeze the time of the tests in cypress - hence the use of cy.clock)

    const now = new Date(2018, 4, 11, 9, 0, 0).getTime(); // Freeze the time for the tests
    cy.clock(now);
    cy.visit('/');
    cy
    cy
      .get('.draggable-item-id-from-your-system-here') // or use [data-cy=test-id] for more robust tests
      .trigger('mousedown', { button: 0, clientX: 633, clientY: 440 }) // drag events test for button: 0 and also use the clientX and clientY values - the clientX and clientY values will be specific to your system
      .trigger('mousemove', { button: 0, clientX: 639, clientY: 500 }) // We perform a small move event of > 5 pixels this means we don't get dismissed by the sloppy click detection
      .tick(200); // react-beautiful-dnd has a minimum 150ms timeout before starting a drag operation, so wait at least this long.
    cy
      .get('.top-level-view') // now we perform drags on the whole screen, not just the draggable
      .trigger('mousemove', { button: 0, clientX: 939, clientY: 900 })
      .tick(1); // We've frozen time, so we need at least a 1ms tick to move to lead to the animations being executed. You should see the actual animation in the cypress window
    cy.get('.top-level-view').trigger('mouseup'); // Causes the drop to be run
    // Can now test the application's post DROP state

Thanks this worked beautifully for me!

@goncy
Copy link

goncy commented Dec 20, 2018

Just for the record, I created a custom command based on @millarm response:

commands/dragAndDrop.js

export default (subject, offset = { x: 0, y: 0 }) => {
  cy.clock(+new Date());

  cy
    .wrap(subject)
    .first()
    .then(element => {
      const coords = element[0].getBoundingClientRect();

      cy
        .wrap(element)
        .trigger("mousedown", {
          button: 0,
          clientX: coords.x,
          clientY: coords.y
        })
        .trigger("mousemove", {
          button: 0,
          clientX: coords.x + 5,
          clientY: coords.y
        })
        .tick(200);

      cy
        .get("body")
        .trigger("mousemove", {
          button: 0,
          clientX: coords.x + offset.x,
          clientY: coords.y + offset.y
        })
        .tick(200);

      cy.get("body").trigger("mouseup");
    });
};

commands.js

Cypress.Commands.add("dragAndDrop", { prevSubject: "optional" }, dragAndDrop);

Usage:

    cy.get("[data-cy=card]").dragAndDrop({
      x: 320,
      y: 0
    });

This command drags an offset, but can be modified to drag to an absolute position

@abzainuddin
Copy link

Tweaked @goncy solution a bit, so it works on elements instead of using offsets (tested on version 10.0.3):

commands/dragAndDrop.js

const sloppyClickThreshold = 5;
const primaryButton = 0;

export function drag(subject) {
    const coords = subject[0].getBoundingClientRect();

    // Hoops we need to jump through to register a mouse drag.
    cy.wrap(subject)
        .trigger('mousedown', {
            // Hoop for checking primary button.
            button: primaryButton,

            // Register clientX + clientY for sloppy click detection in
            // subsequent mousemove.
            clientX: coords.left,
            clientY: coords.top
        })
        .trigger('mousemove', {
            button: primaryButton,

            // Make sure we pass sloppyClickThreshold detection.
            clientX: coords.left + sloppyClickThreshold,
            clientY: coords.top
        });
};

export function drop(subject) {
    cy.wrap(subject)
        .trigger('mousemove', { button: primaryButton })
        .trigger('mouseup');
};

commands.js

import { drag, drop } from './dragAndDrop';

Cypress.Commands.add('drag', { prevSubject: 'element' }, drag);
Cypress.Commands.add('drop', { prevSubject: 'element' }, drop);

Usage:

cy.get('.draggable').drag();
// Do some tests here, for example check change in droppable background colors.
cy.get('.droppable').drop();

@tcoughlin3
Copy link

@goncy's version of @millarm's command is working great for me in the Cypress GUI but fails to move anything in the Cypress Electron browser. Any suggestions are greatly appreciated.

https://stackoverflow.com/questions/54120227/drag-and-drop-cypress-test-fails-in-electron-browser-only

@nmain
Copy link

nmain commented Apr 4, 2019

One potential use case I have that could be covered by this feature: I have a draggable list, and some button outside of that list that sorts it in a particular way. When I click the sort button, I'd like the list to sort, and animate as it sorts. If there was programatic dragging, my sort code could just fire off a series of drags.

There are other libraries that do this, but if I'm already using react-beautiful-dnd and my users are already used to exactly its animations when they drag, why not kill every bird with one giant boulder?

@alexreardon
Copy link
Collaborator Author

It looks like this might be going out as a part of #1225

@alexreardon
Copy link
Collaborator Author

programatic-dragging 2019-05-07 21_44_54

@alexreardon
Copy link
Collaborator Author

alexreardon commented May 9, 2019

Ideas for programmatic API examples

  • Voice sensor. Lower level control for voice dragging. It would be it's own npm package /cc @danieldelcore
  • Higher lever voice sensor for Jira Software boards
  • Webcam sensor to allow minority report style interactions
  • HTML5 file drag and drop experience:
    • Make the whole page a HTML5 drop target
    • When entering the target create a Droppable and Draggable for the dragged item
    • Use a custom sensor to start a drag
    • Use a custom sensor to move the item around in response to user input
  • two seperate DragDropContext applications to facilitate a game of some kind
  • Undo dragging sensor
  • Board onboarding

@alexreardon
Copy link
Collaborator Author

This shipped in 12.0! #1487

@vezaynk
Copy link

vezaynk commented Feb 6, 2020

@alexreardon

programatic-dragging 2019-05-07 21_44_54

Is this implemented in Cypress? I came here looking for how to implement this in a normal context, but this discussion seems to have shifted entirely into Cypress testing, while I just wanted functionality similar to your gif.

Is there code you can show for found it?

Update:
Found it: https://github.com/atlassian/react-beautiful-dnd/blob/cbedc3262e4124deccb436fd966dd837eaf44f2b/stories/src/programmatic/with-controls.jsx

Key highlights:
An api is exposed via the sensor prop callback on DragDropContext, useable through a ref.
Interactions are documented here: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/sensors/sensor-api.md

TypeScript definitions for sensor prop is out of date, I PRed it: DefinitelyTyped/DefinitelyTyped#42167

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