PRACTICAL GUIDE / Playwright locator drop text URI list

Why your dropped URL arrives as empty text

Send and inspect text/uri-list payloads with Playwright, parse multiple dropped links correctly, and diagnose MIME-type or event-handler mismatches.

By The Testing AcademyUpdated August 4, 20269 min read
All field guides
In this guide6 sections
  1. Understand what lives in DataTransfer
  2. Exercise comments, multiple links, and fallback text
  3. Inspect the event before changing the test
  4. Parse the payload as untrusted input
  5. Decide whether source or target is under test
  6. When a URI drop test is the wrong level

What you will learn

  • Understand what lives in DataTransfer
  • Exercise comments, multiple links, and fallback text
  • Inspect the event before changing the test
  • Parse the payload as untrusted input

A link dropped by hand creates a bookmark, but the Playwright test reports "No URL supplied." The target highlighted correctly and the drop action returned, so adding a longer timeout changes nothing. The handler is reading a different DataTransfer type, or it is treating a valid URI list as one malformed URL.

Dropped links are string data, not files. The test has to supply the MIME type the application reads, and the application has to parse the line-based text/uri-list format rather than assuming it always receives one bare string.

Understand what lives in DataTransfer

A DataTransfer can carry files and string entries at the same time. dataTransfer.files exposes dragged File objects during the drop event. dataTransfer.getData(type) retrieves string data registered under a format such as text/plain, text/html, or text/uri-list.

Playwright's locator.drop() accepts those two categories separately:

TypeScript
await page.getByTestId('bookmark-dropzone').drop({
  data: {
    'text/uri-list': 'https://example.com/docs',
    'text/plain': 'https://example.com/docs',
  },
});

The method constructs a synthetic DataTransfer in the page, dispatches dragenter and dragover, then dispatches drop if the target accepts it. As with a file drop, the dragover listener must call preventDefault(). If it does not, Playwright sends dragleave and throws. That exception is evidence about event acceptance, not URI parsing.

A link dragged by a person commonly supplies both text/uri-list and text/plain. The URI-list value is the structured representation; plain text is a compatibility fallback. Sending only plain text to a handler that explicitly reads text/uri-list produces an empty string. Sending only URI-list data to an application intentionally designed for pasted plain text tests the wrong contract in the other direction.

The URI-list format can contain multiple URLs separated by CRLF line breaks. Blank lines are ignorable, and lines beginning with # are comments. A line comment can describe the following URL, so simply passing the entire value to new URL() rejects valid data.

There is also a legacy-looking trap. getData('URL') is treated as a request for URI-list data, but retrieval returns only the first URL. That can be acceptable for a single-bookmark feature. It silently loses data when the interface advertises a multi-link import.

The following test is self-contained and runnable. It accepts HTTP and HTTPS entries, ignores comments, displays every valid URL, and rejects unsupported schemes.

Save it as tests/drop-uri-list.spec.ts:

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

test('imports supported links from a URI list', async ({ page }) => {
  await page.setContent(`
    <main>
      <div
        data-testid="bookmark-dropzone"
        role="button"
        tabindex="0"
        aria-label="Import bookmarks"
      >
        Drop links here
      </div>
      <ul data-testid="result" aria-live="polite"></ul>
    </main>

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

      function parseUriList(value) {
        return value
          .split(/\r?\n/)
          .map(line => line.trim())
          .filter(line => line && !line.startsWith('#'))
          .flatMap(candidate => {
            try {
              const url = new URL(candidate);
              return ['http:', 'https:'].includes(url.protocol)
                ? [url.href]
                : [];
            } catch {
              return [];
            }
          });
      }

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

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

        const uriList = event.dataTransfer.getData('text/uri-list');
        const plainText = event.dataTransfer.getData('text/plain');
        const urls = parseUriList(uriList || plainText);

        result.replaceChildren();

        if (urls.length === 0) {
          result.textContent = 'No supported URL';
          return;
        }

        for (const url of urls) {
          const item = document.createElement('li');
          item.textContent = url;
          result.append(item);
        }
      });
    </script>
  `);

  const dropzone = page.getByTestId('bookmark-dropzone');
  const resultItems = page.getByRole('listitem');

  await dropzone.drop({
    data: {
      'text/uri-list': [
        '# Product references',
        'https://example.com/docs',
        '',
        '# Service status',
        'https://status.example.org/history',
      ].join('\r\n'),
      'text/plain': [
        'https://example.com/docs',
        'https://status.example.org/history',
      ].join('\n'),
    },
  });

  await expect(resultItems).toHaveText([
    'https://example.com/docs',
    'https://status.example.org/history',
  ]);

  await dropzone.drop({
    data: {
      'text/uri-list': 'javascript:alert(1)',
    },
  });

  await expect(page.getByTestId('result')).toHaveText(
    'No supported URL',
  );
});

The first assertion proves more than a happy-path string. It catches parsers that forget comments, support only LF, stop after the first entry, or accidentally use the plain fallback when structured data is present. The negative drop proves that receiving a syntactically valid URL object is not enough; the feature still enforces an allowed protocol.

The example prefers URI-list data whenever it is non-empty. That is a product choice. Another interface might merge entries from both types or accept plain text only when URI-list parsing yields no valid URLs. Write that precedence down, because sending both types can otherwise hide which branch the test exercised.

URL normalization is another trade-off. new URL(candidate).href may normalize host casing, escapes, or a missing trailing slash. That is useful before deduplication and navigation. It is wrong when the feature promises to preserve the exact source string for display. Keep raw and parsed values separately if both behaviors matter.

Inspect the event before changing the test

First classify where the failure occurs.

When drop() throws, inspect event acceptance. Confirm the locator resolves to the element with the dragover listener and that the enabled state calls preventDefault(). No DataTransfer parsing happens after a rejected dragover.

When the action succeeds but the UI says no URL, inspect dataTransfer.types and read the values during the drop event. A temporary capture listener can print both without changing the component:

TypeScript
await page.evaluate(() => {
  document.addEventListener(
    'drop',
    event => {
      const transfer = event.dataTransfer;

      console.log(JSON.stringify({
        types: transfer ? [...transfer.types] : [],
        uriList: transfer?.getData('text/uri-list') ?? '',
        plainText: transfer?.getData('text/plain') ?? '',
      }));
    },
    { capture: true, once: true },
  );
});

Run the focused test with a trace:

Shell
npx playwright test tests/drop-uri-list.spec.ts --trace=on

The trace console shows whether the payload reached the page under the expected keys. Be careful with production-like URLs. Query strings can contain one-time tokens or customer identifiers, so sanitize values before attaching them to a CI report.

If the capture listener sees the expected URI-list but the component sees an empty string, compare when each reads it. Drag data is intended to be read during the drag event. A handler that stores the event and reads getData() in a later timer is relying on data that may no longer be available.

If both listeners read the value but output is wrong, isolate the parser. Feed it the exact logged string in a unit test, including carriage returns, comments, blanks, Unicode, and multiple entries. Browser timing is no longer the leading suspect.

If only the first link appears, search for getData('URL'), split('\n')[0], or a UI contract that intentionally chooses one link. Do not change the expected result until product behavior answers whether multi-link drop is supported.

Parse the payload as untrusted input

A dropped URL is user input. Restricting protocols to HTTP and HTTPS prevents a simple javascript: string from becoming a navigation or stored cross-site scripting path. Depending on the product, the parser may also need to reject embedded credentials, local network addresses, unexpected ports, or hosts outside an allowlist.

Parsing and policy are different steps. Parsing answers whether the text is a URL. Policy answers whether this application may use it. Keeping them separate gives failures useful names and makes the same rules available to paste, API import, and drag-and-drop paths.

Do not use a regular expression as the only URL parser. The platform URL constructor handles encoding, relative components, IPv6 syntax, and other details that hand-written patterns regularly get wrong. Supply an explicit base only if relative URLs are a documented feature; otherwise a relative candidate should fail.

Deduplication needs a product decision too. These strings may or may not identify the same bookmark:

  • https://example.com
  • https://example.com/
  • https://EXAMPLE.com/
  • https://example.com/#overview

Normalizing everything can remove meaningful fragments or query parameters. Keeping every raw string can create duplicates. The browser test should cover the agreed rule, while a table-driven unit test handles the larger URL corpus.

Error handling also has a cost. Silently skipping one malformed line lets valid entries import, which is friendly for bulk drops but may hide data loss. Rejecting the whole payload is safer for transactional imports but frustrating when one comment-like line is malformed. Assert whichever behavior the UI explains to the user.

Decide whether source or target is under test

Direct drop({ data }) controls the payload and exercises the target. It does not prove that a draggable link elsewhere in your application generates the right URI-list during dragstart. This separation is valuable when the target parser is the risk, because the source cannot introduce unrelated instability.

If the requirement is "drag this card onto bookmarks," test the source behavior separately. A dragTo() scenario can cover the page gesture and target transition, while a component or browser integration test inspects what the source placed in DataTransfer. One giant test that tries to prove geometry, source serialization, target parsing, persistence, and navigation will be slow and difficult to diagnose.

External links introduce no source element inside the page. For that user story, drop() is a faithful web boundary. It supplies the same MIME categories the target consumes without pretending to automate Finder, Explorer, or another browser tab.

Script-generated events have isTrusted set to false. If the application rejects untrusted drops as a security boundary, Playwright cannot change that read-only fact. Do not add a test-only production switch that silently bypasses the guard. Extract the URI parser and policy for direct tests, then use a desktop-level or manual check for the trusted gesture if it is genuinely required.

The controlled payload costs realism. The real source may include browser-specific types in a different order or omit the plain fallback. Add a small source-contract test for each supported origin instead of making every target test depend on those variations.

When a URI drop test is the wrong level

Use a normal click when the user selects an in-page link and the requirement is navigation. Drag-and-drop adds no value to that path.

Use a file payload when the target imports .url, .webloc, or another actual file. A string URI-list and a FileList are different browser contracts even if both eventually create bookmarks.

Choose dragTo() for reordering or moving an existing DOM item where pointer movement is the behavior. Use drop({ data }) for external clipboard-like content.

Move most malformed-input combinations to parser tests. Hundreds of URI encodings, host policies, and comment layouts do not need a browser each time. Keep browser coverage for MIME handoff, event acceptance, one representative multi-link payload, and the user-visible result.

Finally, do not use this test to claim cross-application drag support. The target-side JavaScript contract can be correct while an operating system, managed browser policy, or source application supplies different data. That broader claim needs evidence from the actual source and environment.

// 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 developer.mozilla.org reference

    developer.mozilla.org

    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 URL with Playwright?

Pass a `data` object to `locator.drop()` with `text/uri-list` mapped to the URL string. Add `text/plain` as a fallback when the application is meant to accept the payload a browser commonly creates for a dragged link.

Can text/uri-list contain more than one URL?

Separate multiple entries with CRLF line breaks, ignore blank lines, and treat lines beginning with `#` as comments. A parser that reads the whole payload as one URL will fail on valid multi-link data.

Why is dataTransfer.files empty for my dropped link?

String data and file data occupy different parts of DataTransfer. Read a link with `getData('text/uri-list')` or the agreed plain-text fallback rather than expecting a FileList entry.

What does getData URL return for multiple dropped links?

Using the `URL` alias requests `text/uri-list`, but retrieval returns only the first URL. Read and parse the explicit `text/uri-list` value when the feature supports multiple links or comments.

Can Playwright create a trusted drop event?

Script-dispatched events have `isTrusted` set to false, and page code cannot turn one into a genuine user event. Keep a security-sensitive trusted-event check intact and test the underlying parser at a lower level.