Skip to content

fix: normalize DataTransfer format aliases - #1326

Merged
snowystinger merged 4 commits into
testing-library:mainfrom
dylanpulver:fix/datatransfer-format-aliases
Sep 2, 2026
Merged

fix: normalize DataTransfer format aliases#1326
snowystinger merged 4 commits into
testing-library:mainfrom
dylanpulver:fix/datatransfer-format-aliases

Conversation

@dylanpulver

Copy link
Copy Markdown
Contributor

What

DataTransfer.getData(), setData() and clearData() now normalize their format argument the way the HTML spec requires. The format is converted to ASCII lowercase, and the shorthands text and url are replaced with text/plain and text/uri-list.

Fixes #1269

Why

userEvent.paste('foo') builds its DataTransfer with setData('text', ...), and the stub stored that string verbatim as the item type. A paste handler reading event.clipboardData.types therefore saw ['text'], so the check reported in #1269, types.includes('text/plain'), was false. No browser produces a text type on a clipboard event, so handlers written against real clipboard data did not match.

Two related cases were wrong in the same way on main. setData('url', ...) stored the type url, which left getData('text/uri-list') returning an empty string. Separately, clearData('text') failed to remove an item stored as text/plain, and a format containing uppercase letters was stored and looked up as a type distinct from its lowercase spelling.

The spec puts this mapping inside each of the three methods. setData() converts the format to ASCII lowercase, then changes text to text/plain and url to text/uri-list, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdata. getData() repeats both steps before looking the item up, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-getdata, and clearData() does the same before removing one, at https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-cleardata.

How

The mapping lives in a normalizeFormat() helper in src/utils/dataTransfer/DataTransfer.ts, and all three methods run their format argument through it.

It belongs there rather than in src/clipboard/paste.ts because the type is decided by setData(), not by its callers, so fixing it at the method covers every caller at once. copySelection() and any DataTransfer a user builds and hands to userEvent.paste() reach the same methods, and a change confined to paste.ts would have left the url shorthand and the case handling broken for all of them. paste.ts is untouched and now yields text/plain on its own, which is what the added end to end test asserts.

One part of the spec algorithm is deliberately left out. getData('url') is also supposed to parse a text/uri-list body down to its first URL, which is a separate step from format normalization and is not included here.

tests/utils/dataTransfer/DataTransfer.ts gains a setData and getData round trip through both shorthands, the same round trip written in mixed case, an overwrite where an item declared under one spelling is replaced by writing the other, and a clearData() call that removes an item by its shorthand. tests/clipboard/paste.ts gains the reported scenario, asserting that a paste event built from a string exposes text/plain in types and returns the string from getData('text/plain'). Every added test fails on main and passes with this change.

The whole Jest suite passes at 514 tests in 54 files, npm run validate reports no type errors, and eslint reports no new problems on the changed files.

Checklist

  • Documentation
  • Tests
  • Ready to be merged

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, went down a bit of a rabbit hole looking into this.
setData and getData appear to work consistently across the browsers. I learned during this process though that add, which has a similar spec, works completely differently FF/Safari/Chrome https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransferitemlist-add

This does mean that this combination wouldn't work anymore

dt.items.add('foo', 'TEXT/PLAIN')
dt.getData('TEXT/PLAIN')

I am a little torn if we should match the spec for the add function just in case. Or not, because all the browsers disagree.

Will think on it some more tonight. If you have any thoughts, let me know.

The spec lowercases the type in DataTransferItemList.add() but, unlike
setData(), does not replace the text and url shorthands.
@dylanpulver

Copy link
Copy Markdown
Contributor Author

The spec is unambiguous on the half your example needs: add() converts the type to ASCII lowercase, but unlike setData() it does not replace the text/url shorthands. That mapping only exists in setData/getData/clearData.

Worth knowing before you decide: main isn't a working baseline here either. It works for exactly one spelling, and this branch moved which one.

main    add('foo','TEXT/PLAIN'); getData('TEXT/PLAIN') -> 'foo'
main    add('foo','text/plain'); getData('TEXT/PLAIN') -> ''
branch  add('foo','TEXT/PLAIN'); getData('TEXT/PLAIN') -> ''
branch  add('foo','text/plain'); getData('TEXT/PLAIN') -> 'foo'

Both store and look up verbatim, so the case has to agree on the two sides by luck. Lowercasing in add() resolves all four.

Pushed that as eb51950, kept as its own commit so it's one revert if you'd rather leave add alone. add('foo','TEXT') still stores text rather than text/plain, matching the spec's asymmetry. 516 tests pass.

@snowystinger

Copy link
Copy Markdown
Contributor

Recording output for tests for items.add in different browsers.

Real browser tests are unaffected, UserEvent's stub only applies if window.DataTransfer is undefined, such as in jsdom.

For the tested items.add() cases, Safari matches the current HTML Standard. I think that's enough reason for us to make the code in User Event spec compliant. If it breaks people, then likely their code is only working in one browser anyways. This change should continue to match more browsers as they converge on the spec. If someone needs to jsdom test a specific browser with different behaviour, they can define their own DataTransfer on the window, we'll defer to that.
Or for user.paste they can just pass their own DataTransfer.

const dt = new MyDataTransfer()
dt.setData('TEXT', 'value')

await user.paste(dt)

add

Chrome

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN TEXT/PLAIN ["TEXT/PLAIN"] "" ""
TEXT TEXT ["TEXT"] "" ""
URL URL ["URL"] "" ""
MiXeD/Type MiXeD/Type ["MiXeD/Type"] "" ""

Safari

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN text/plain ["text/plain"] value value
TEXT text ["text"] "" ""
URL url ["url"] "" ""
MiXeD/Type mixed/type ["mixed/type"] value value

Firefox

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN text/plain ["text/plain"] value value
TEXT text/plain ["text/plain"] value value
URL text/uri-list ["text/uri-list"] value value
MiXeD/Type mixed/type ["mixed/type"] value value
Code used to test
(() => {
  const test = type => {
    const dt = new DataTransfer()

    try {
      const item = dt.items.add('value', type)

      return {
        input: type,
        itemType: item?.type,
        dataTransferTypes: [...dt.types],
        getOriginal: dt.getData(type),
        getLowercase: dt.getData(type.toLowerCase()),
      }
    } catch (error) {
      return {
        input: type,
        error: `${error.name}: ${error.message}`,
      }
    }
  }

  const results = [
    'TEXT/PLAIN',
    'TEXT',
    'URL',
    'MiXeD/Type',
  ].map(test)

  console.log(navigator.userAgent)
  console.table(results)
  return results
})()

setData

Chrome

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN text/plain ["text/plain"] value value
TEXT text/plain ["text/plain"] value value
URL text/uri-list ["text/uri-list"] https://example.com/path https://example.com/path
MiXeD/Type mixed/type ["mixed/type"] value value

Safari

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN text/plain ["text/plain"] value value
TEXT text/plain ["text/plain"] value value
URL text/uri-list ["text/uri-list"] https://example.com/path https://example.com/path
MiXeD/Type mixed/type ["mixed/type"] value value

Firefox

Input item.type DataTransfer.types getData(original) getData(lowercase)
TEXT/PLAIN text/plain ["text/plain"] value value
TEXT text/plain ["text/plain"] value value
URL text/uri-list ["text/uri-list"] https://example.com/path https://example.com/path
MiXeD/Type mixed/type ["mixed/type"] value value

@snowystinger
snowystinger merged commit 1e0020b into testing-library:main Sep 2, 2026
2 checks passed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 14.6.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing string to userEvent.paste() uses invalid MIME type "text" instead of "text/plain"

2 participants