ScreenshotNeo

BlogHow-to

How to Fix Cypress Drag-and-Drop Tests That Pass Without Dragging

Cypress can pass a drag test after firing handlers without moving anything. Learn how to match the event model and assert the completed drop.

By the ScreenshotNeo team1 October 20269 min read

A Cypress drag-and-drop test can pass even though the item never moved. The usual cause is that the test dispatched an event, such as drop or mousemove, and your handler ran, but the browser did not perform the drag operation’s default behavior. Cypress documents that .trigger() fires the corresponding event and invokes listeners; it does not reproduce every action a user performs with a mouse.

Fix the test by first identifying whether the component uses mouse or pointer events, the HTML5 drag-and-drop API, or a library-specific abstraction. Then use the matching interaction and assert a durable result: the item is in the destination, has the expected order, or the application state has been persisted.

Why a passing test may not have dragged anything

Cypress’s .trigger() documentation explains the key distinction: the command dispatches an event but does not make the browser carry out the event’s native default action. A listener can therefore set a flag, call a callback, or update a test spy while the draggable element remains in its original location.

Drag implementations also differ:

  • Mouse-driven sortable widgets respond to events such as mousedown, mousemove, and mouseup.
  • HTML5 drag and drop uses dragstart, dragover, drop, and related events, often with DataTransfer.
  • Pointer-based components may require pointer events and pointer coordinates.
  • Framework or library abstractions may translate one event family into another.

A sequence that works for one model may do nothing for another. Read the component’s implementation or documentation before choosing commands.

Start with the assertion

Read the passing assertion literally. These checks do not prove that a drag completed:

cy.get('@dropHandler').should('have.been.called')
cy.get('[data-cy=card]').should('be.visible')
cy.get('[data-cy=drag-status]').should('contain', 'drop received')

They may only prove that a callback ran or that the original element still exists. Prefer an assertion on the outcome a user cares about:

cy.get('[data-cy=done-column] [data-cy=card-42]')
  .should('exist')

cy.get('[data-cy=done-column] [data-cy=card]')
  .then(($cards) => {
    const ids = [...$cards].map((card) => card.getAttribute('data-card-id'))
    expect(ids).to.deep.equal(['card-42', 'card-17'])
  })

cy.request('GET', '/api/board')
  .its('body.columns.done')
  .should('include', 'card-42')

Use a visible destination or ordering assertion when that is the product behavior. If the operation saves to a server, assert the persisted state as well.

Identify the component’s event model

  1. Find the draggable element and drop target in the DOM.
  2. Inspect the component or library documentation for its supported event family.
  3. Search the source for dragstart, dragover, drop, mousedown, mousemove, mouseup, pointerdown, and pointermove.
  4. Check whether the implementation reads coordinates, mouse buttons, or event.dataTransfer.
  5. Confirm that the test’s target is the element that owns the handler, not a covered child or a visual wrapper.

Cypress maintainers have described manually replaying event sequences as implementation-dependent, especially when a component expects data attached to DataTransfer. Treat a hand-written sequence as a test of the component’s current event contract, not as a universal drag simulation.

Mouse-driven sortable components

For a mouse-based sortable widget, send the events the library expects and include the required button and coordinates. Cypress’s documented jQuery UI Sortable example uses this pattern:

describe('reordering cards', () => {
  it('moves a card to the destination position', () => {
    cy.visit('/board')

    cy.get('[data-cy=draggable]').trigger('mousedown', {
      which: 1,
      pageX: 600,
      pageY: 100,
    })

    cy.get('[data-cy=draggable]').trigger('mousemove', {
      which: 1,
      pageX: 600,
      pageY: 600,
    })

    cy.get('[data-cy=draggable]').trigger('mouseup')

    cy.get('[data-cy=destination] [data-cy=draggable]')
      .should('exist')
  })
})

The coordinates and which: 1 value are required by that documented jQuery UI example. They are not universal values. Set the coordinates to positions that are meaningful for your fixture and handlers. If the library listens on a parent or document, yield that element when dispatching the event or use the library’s supported Cypress recipe.

Make the mouse sequence less fragile

cy.get('[data-cy=card-42]')
  .should('be.visible')
  .then(($card) => {
    const rect = $card[0].getBoundingClientRect()
    const startX = Math.round(rect.left + rect.width / 2)
    const startY = Math.round(rect.top + rect.height / 2)

    cy.get('[data-cy=done-column]').then(($target) => {
      const targetRect = $target[0].getBoundingClientRect()
      const endX = Math.round(targetRect.left + targetRect.width / 2)
      const endY = Math.round(targetRect.top + targetRect.height / 2)

      cy.wrap($card).trigger('mousedown', {
        which: 1,
        pageX: startX,
        pageY: startY,
      })
      cy.wrap($card).trigger('mousemove', {
        which: 1,
        pageX: endX,
        pageY: endY,
      })
      cy.wrap($card).trigger('mouseup')
    })
  })

This still depends on the library’s handlers. Keep the final assertion on the destination or order.

HTML5 drag-and-drop

If the component uses the HTML5 drag API, a synthetic event recipe must provide the properties the handlers read. A minimal example is:

const dataTransfer = new DataTransfer()

dataTransfer.setData('text/plain', 'card-42')

cy.get('[data-cy=card-42]').trigger('dragstart', { dataTransfer })
cy.get('[data-cy=done-column]').trigger('dragover', { dataTransfer })
cy.get('[data-cy=done-column]').trigger('drop', { dataTransfer })
cy.get('[data-cy=card-42]').trigger('dragend', { dataTransfer })

cy.get('[data-cy=done-column] [data-cy=card-42]')
  .should('exist')

This dispatches handlers. It does not guarantee that Chromium performed the native HTML5 drag pipeline. If the application depends on browser default behavior, native drag feedback, or a precise DataTransfer lifecycle, a hand-built sequence can remain a false positive or fail for reasons unrelated to the user interaction.

Cypress’s official example recipes include drag-and-drop event examples. The same recipe index notes that .selectFile() is the appropriate tool when the interaction is specifically dropping files; it does not replace a sortable-card test.

When synthetic events are insufficient

Use a browser-driven helper only when the application genuinely requires native HTML5 drag behavior and the synthetic sequence cannot exercise it. The community cypress-real-dnd project describes a Chrome DevTools Protocol technique for the HTML5 dragstart to dragover to drop pipeline. Its documentation targets Chromium-family browsers and Cypress 12 or newer and includes additional setup and rewarm considerations.

Check the project’s current release, browser, Cypress version, and application event model before adopting it. The project is community-maintained, and its HTML5 approach is separate from mouse or pointer-based sortable libraries.

Actionability, targeting, and debugging

Cypress action commands check whether an element is ready to interact with. The actionability guide covers visibility, detachment, animation, coverage, scrolling, and related checks.

cy.get('[data-cy=card-42]')
  .should('be.visible')
  .debug()
  .trigger('mousedown', { which: 1, pageX: 200, pageY: 120 })

Pause in the Cypress runner or add a debugger immediately before the action. Verify that:

  • The query resolves to exactly the intended draggable.
  • The target is visible and has not been detached by a render.
  • A sticky header, overlay, animation, or scroll position is not covering it.
  • The drop target is the element that receives the handler.
  • The event is dispatched on a yielded element, window, or document.

{ force: true } bypasses actionability checks. Cypress describes it as an escape hatch, so use it only when bypassing those checks is intentional and you still assert the resulting state. Otherwise it can hide a test that a real user could not perform.

Reliable assertions for completed drops

Choose an assertion that survives DOM implementation changes while proving the user-visible result:

Outcome Useful assertion What it proves
Move between columns Card exists under destination selector The rendered placement changed
Reorder within a list Read stable IDs and compare order The ordering operation completed
Persist to a backend Query the API or reload and assert placement The saved state matches the UI
Reject an invalid drop Assert original location and validation message The business rule was enforced

Avoid asserting only that an event callback ran. That is useful as a narrow unit-level check, but it does not establish that the drag interaction succeeded.

Troubleshooting common failures

Symptom Likely cause Fix
The test passes but the card stays put An event listener ran without native browser behavior, or the assertion checks only a callback Match the component’s event model and assert destination or order
dragover fires but drop does not change state The handler requires preventDefault(), a specific DataTransfer value, or native drag state Inspect the handler, supply the required data, and use a browser-driven helper if native behavior is required
Mouse sequence has no effect Missing which: 1, coordinates, or the wrong event target Use the library’s documented properties and dispatch on the element that owns the handler
Only headed mode works Timing, animation, scrolling, or an overlay changes actionability Wait for a stable state, inspect coverage, disable test-only animation where appropriate, and assert after the UI settles
cy.get() finds multiple elements The selector is not scoped to the intended card or list Use stable test attributes and scope with .within()
Element is detached during the drag A framework rerender replaced the node Wait for the render to finish and reacquire the element before the next event
force: true makes it pass Actionability was hiding a real targeting or layout problem Remove force, fix visibility or coverage, then keep the outcome assertion
Plugin setup fails in CI Browser or Cypress version is outside the helper’s documented scope, or setup was incomplete Check the current project documentation, browser family, Cypress version, and rewarm/setup steps

Performance, reliability, and maintenance

  • Keep the interaction short. Use one start point and one destination point unless the component requires intermediate movement.
  • Wait on state, not arbitrary sleeps. Wait for the list, target, or network-backed state that makes the drag possible.
  • Use stable selectors. Prefer data-cy or equivalent attributes over CSS classes that describe styling.
  • Separate event-model tests from product tests. A focused synthetic event test can verify handlers; an end-to-end test should verify the user-visible result.
  • Limit native-drag helpers to the cases that need them. They add browser and version compatibility to maintain.
  • Capture diagnostics on failure. Preserve the Cypress video, command log, DOM state, and relevant network response so a layout or event regression is distinguishable from a selector failure.

A practical decision checklist

  1. Does the assertion prove the destination, order, or persisted state?
  2. Does the component use mouse, pointer, HTML5 drag, or a library abstraction?
  3. Are the target, button, coordinates, and DataTransfer properties correct?
  4. Is the element visible, attached, uncovered, and scrolled into a usable position?
  5. Does the synthetic sequence need native browser behavior?
  6. If yes, is a Chromium CDP helper compatible with your Cypress and browser versions?
  7. Does the test still pass after reloading and checking the saved state?

Or skip the browser setup

If you need screenshots of the resulting board, test documentation, or failure states, ScreenshotNeo captures a URL with one GET request. It accepts the cookie or consent banner before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API docs for the complete option list.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

You can also configure full-page capture, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, click actions, waits, blocked resources, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, bulk capture, and usage reporting. Clean shots are the only billable results. The Free plan includes 1,000 screenshots each month without a card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Does .trigger('drop') perform a real drop?

No. It dispatches the event and invokes listeners. It does not reproduce all browser default behavior.

Should every drag test use a native-drag plugin?

No. Use the smallest technique that matches the component. Mouse-driven widgets can use their documented mouse sequence; native helpers are for implementations that require the browser’s HTML5 drag pipeline.

Why are coordinates important?

Some sortable libraries use page coordinates to calculate movement. Cypress’s documented jQuery UI example requires pageX, pageY, and which: 1.

What should I assert after a rejected drop?

Assert that the item remains in its original location and that the UI exposes the expected validation or rejection state.

Can screenshots prove that a drag worked?

A screenshot can document the rendered result, but the test should still assert destination, order, or persisted state directly.