PRACTICAL GUIDE / Playwright locator drop file upload zone

Test the drop zone, not just the hidden file input

Test a true drag-and-drop file path with Playwright, verify the DataTransfer payload, and separate drop-zone bugs from ordinary input uploads.

By The Testing AcademyUpdated August 4, 20269 min read
All field guides
In this guide6 sections
  1. Know which upload path you are exercising
  2. Run a real file through the handler
  3. Find the layer that rejected the file
  4. Make the target and payload unambiguous
  5. Treat synthetic drag as a browser-level contract
  6. When drop is the wrong action

What you will learn

  • Know which upload path you are exercising
  • Run a real file through the handler
  • Find the layer that rejected the file
  • Make the target and payload unambiguous

The drop zone highlights and prints orders.csv, but no upload reaches the server. A test that checks only the highlight calls this a pass, while a test that fills the hidden file input never runs the drop handler at all. Both miss the behavior a user gets when dragging a file from the desktop.

Playwright's locator.drop() gives this path a direct test. The method creates a file-backed DataTransfer, dispatches the drag event sequence, and refuses to continue when the target does not accept the drop.

Know which upload path you are exercising

Three Playwright actions are easy to confuse:

  • locator.setInputFiles() assigns files to an <input type="file">.
  • fileChooser.setFiles() handles a chooser opened by a click.
  • locator.drop() simulates external files or data arriving at a drop target.

Use the first two when the browser file input is the product contract. Use drop() when application code listens for dragenter, dragover, dragleave, or drop and reads event.dataTransfer. A hidden input may eventually receive the same file, but bypassing the event handler can skip MIME validation, hover state, duplicate detection, analytics, or the upload call itself.

locator.dragTo() belongs to another category. It moves the pointer from one DOM element to another with mouse actions. That is right for rearranging a kanban card or moving an item between lists. It does not create the external File objects a desktop drag supplies.

The drop() method was added in Playwright 1.60. It dispatches dragenter, dragover, and drop at the target with a synthetic DataTransfer. If the target's dragover listener does not call preventDefault(), the browser model says the drop was not accepted. Playwright sends dragleave and throws. That early failure is useful evidence, not a reason to force the action.

A browser exposes dataTransfer.files during drop and paste. Code that saves the DataTransfer object and tries to read its files later can see an empty list because the drag data store is protected outside those events. Copy the File references or their required metadata inside the handler before starting asynchronous work.

Run a real file through the handler

This self-contained test builds a small drop zone, supplies a CSV from memory, and verifies what the application read. Save it as tests/drop-file.spec.ts and run it with npx playwright test tests/drop-file.spec.ts.

TypeScript
import { expect, test } from '@playwright/test';

test('reads a CSV delivered through the drop event', async ({ page }) => {
  await page.setContent(`
    <main>
      <div
        data-testid="dropzone"
        role="button"
        tabindex="0"
        aria-label="Upload orders CSV"
      >
        Drop a CSV here
      </div>
      <output data-testid="result">Waiting for a file</output>
    </main>

    <script>
      const zone = document.querySelector('[data-testid="dropzone"]');
      const result = document.querySelector('[data-testid="result"]');

      zone.addEventListener('dragenter', event => {
        event.preventDefault();
        zone.dataset.state = 'over';
      });

      zone.addEventListener('dragover', event => {
        event.preventDefault();
      });

      zone.addEventListener('dragleave', () => {
        zone.dataset.state = 'idle';
      });

      zone.addEventListener('drop', async event => {
        event.preventDefault();
        zone.dataset.state = 'idle';

        const file = event.dataTransfer.files[0];
        if (!file) {
          result.textContent = 'Rejected: no file';
          return;
        }

        if (file.type !== 'text/csv') {
          result.textContent = 'Rejected: expected text/csv';
          return;
        }

        const text = await file.text();
        const rows = text.trim().split(/\r?\n/).length - 1;

        result.textContent = JSON.stringify({
          name: file.name,
          type: file.type,
          rows,
        });
      });
    </script>
  `);

  const zone = page.getByTestId('dropzone');
  const result = page.getByTestId('result');

  await zone.drop({
    files: {
      name: 'orders.csv',
      mimeType: 'text/csv',
      buffer: Buffer.from(
        'id,total\nA-7,19.95\nB-2,8.50\n',
        'utf8',
      ),
    },
  });

  await expect(result).toContainText('"name":"orders.csv"');
  await expect(result).toContainText('"type":"text/csv"');
  await expect(result).toContainText('"rows":2');

  await zone.drop({
    files: {
      name: 'invoice.pdf',
      mimeType: 'application/pdf',
      buffer: Buffer.from('%PDF-invalid-for-this-form', 'utf8'),
    },
  });

  await expect(result).toHaveText('Rejected: expected text/csv');
});

The test makes two different claims. The first drop proves the File name, MIME type, and content reached the handler. The second proves that application validation rejects an unsupported type. Neither claim depends on a fixture file existing at a CI-specific path.

An in-memory file still has a trade-off. It does not prove that your repository copied a sample file into the test image, nor does it exercise MIME detection based on a real filesystem asset. It also puts the full payload in Node memory. For a 500 MB upload, pass a file path and keep the fixture lifecycle explicit.

The sample parses the CSV in the browser so it remains runnable without a backend. In a product test, do not stop at the local output. Wait for the upload response before the drop, then assert its status and the server-assigned identifier:

TypeScript
const uploadResponse = page.waitForResponse(
  response =>
    response.url().endsWith('/uploads') &&
    response.request().method() === 'POST',
);

await page.getByTestId('dropzone').drop({
  files: 'tests/fixtures/orders.csv',
});

expect((await uploadResponse).ok()).toBeTruthy();

Create the response promise before the action so a fast request cannot finish before the listener exists. The cost is coupling the UI test to one network boundary. If the application legitimately changes from direct upload to a signed storage URL, the assertion will need to follow the durable product result instead.

Find the layer that rejected the file

The point of failure narrows the search.

If drop() itself throws that the target rejected the operation, inspect dragover first. A missing preventDefault(), a listener attached to a different element, or a disabled state can stop the event sequence before application parsing begins. Increasing the action timeout will not make a rejecting handler accept a drop.

If drop() returns but files.length is zero in application logs, verify where the code reads the list. Reading later from a stored DataTransfer is a common error. Also check whether the handler uses dataTransfer.items and filters on kind or MIME type before it reaches files.

If the filename appears but no request starts, the event transport worked. Inspect client validation, parsing, and the branch that constructs the request. A visible "selected" state is not proof that the asynchronous upload function ran.

If the request starts with zero bytes, compare the in-memory Buffer with the File's size inside the drop handler. Then inspect request construction. The drop event may be correct while code appends the wrong object or consumes a stream twice.

If the request succeeds but the UI never settles, the problem is after transport: response handling, polling, cache invalidation, or rendering. Assert the server result and UI result separately so the failure names that boundary.

A temporary capture listener can expose what the page received without rewriting the component:

TypeScript
await page.evaluate(() => {
  document.addEventListener(
    'drop',
    event => {
      const transfer = event.dataTransfer;
      console.log(JSON.stringify({
        target: (event.target as HTMLElement).getAttribute('data-testid'),
        types: transfer ? [...transfer.types] : [],
        files: transfer
          ? [...transfer.files].map(file => ({
              name: file.name,
              type: file.type,
              size: file.size,
            }))
          : [],
      }));
    },
    { capture: true, once: true },
  );
});

Run the one test with --trace=on and read the console entry in the trace. Remove the listener after diagnosis. It observes the event but does not establish that the component processed it.

Make the target and payload unambiguous

Locate the element that owns the drop contract, not a decorative child containing the words "Drop file." A stable test ID is appropriate when the drop surface has no natural form label. Keep a role and accessible name in the product markup so keyboard and assistive-technology behavior can be tested separately.

Build payloads that represent real acceptance rules. Include:

  • The exact filename extension the client validates.
  • A MIME type matching the scenario.
  • Non-empty bytes with a minimal valid structure.
  • Multiple files when order or file-count limits matter.
  • A duplicate name when replacement behavior matters.

Do not claim to test content validation with an arbitrary string named report.pdf. The browser will create a File with that name and MIME type, but the bytes are not a valid PDF. That payload is useful for a rejection test, not a successful document-upload test.

Keep one assertion close to each boundary. First verify the handler received the expected File. Next verify validation outcome. Then verify the request or durable server record. Finally verify user-visible completion. A single "Upload complete" assertion can pass from stale state and gives no clue which stage broke.

More assertions increase test length and bind it to observable interfaces. That maintenance is justified for a high-risk upload flow. For a thin wrapper around a well-tested upload component, one integration path plus lower-level parser tests may give better coverage at lower cost.

Treat synthetic drag as a browser-level contract

locator.drop() constructs the DataTransfer in the page context. It does not operate the desktop shell, move a file icon from Finder or Explorer, or open an operating-system chooser. The resulting event is synthetic, but it exercises the browser-side listener with real File objects across supported browser engines.

That is normally the right boundary for web application automation. It is fast, deterministic, and does not depend on screen coordinates. Its limitation is equally clear: it cannot prove that an OS-specific drag source integrates with the browser or that enterprise endpoint software allows the gesture.

Cross-browser execution still matters. Application code sometimes handles items, files, or MIME strings differently by engine. Run the focused drop test in each browser project that the product supports, but share the same behavioral assertions. Browser-specific expected failures should cite a tracked engine issue rather than quietly accepting different product behavior.

Synthetic input can also expose a product guard based on event.isTrusted. Script-created events are not trusted user events, and test code cannot turn that flag on. Do not remove a security-sensitive guard merely to make automation green. Extract and unit-test the validation logic, then cover the trusted gesture at a more appropriate system boundary.

When drop is the wrong action

Use setInputFiles() when the user selects through a file input and the drop zone is only a styled label for it. That route is simpler and more faithful to the implemented contract.

Use dragTo() when both source and target live in the page, such as reordering cards. No external FileList is required there.

Avoid Buffer payloads for large files. A checked-in or generated path keeps memory pressure predictable, though it adds fixture management and cleanup.

Do not use a browser test to validate every malformed file. Content scanners, CSV parsers, archive limits, and antivirus integrations deserve API or component tests with a broad corpus. Keep the browser case for the handoff from drop event to upload result.

Finally, move to desktop or end-to-end system automation if the requirement explicitly covers dragging from the operating system, browser permission prompts, or managed-device policy. locator.drop() proves the web drop target's contract. It makes no claim about software outside the page.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official developer.mozilla.org reference

    developer.mozilla.org

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official developer.mozilla.org reference

    developer.mozilla.org

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

How do I drop a file on a Playwright locator?

Use `locator.drop()` with `files` set to a path, an array of paths, an in-memory file payload, or an array of payloads. An in-memory payload supplies an exact `name`, `mimeType`, and `buffer`.

Why does locator.drop say the target rejected the drop?

That message means the target's `dragover` handling did not call `preventDefault()`. Playwright sends `dragleave` and throws instead of pretending a browser-accepted drop occurred.

Is dragTo the same as dropping a desktop file?

`dragTo()` performs a pointer drag from one page element to another, while `drop()` supplies external files or clipboard-like data through a synthetic `DataTransfer`. They cover different browser contracts.

Should I use a file path or Buffer in the test?

Choose a Buffer for small, generated fixtures that should be hermetic and easy to read in the test. Prefer a path for large files or when the checked-in file itself, including its packaging and extension, is part of the test.

Does a successful drop call prove the upload worked?

A completed drop call proves the target accepted the drag event sequence, not that validation, parsing, or the network upload succeeded. Assert the server response or durable application result as well as the displayed filename.