[Directory Upload] Extend drag and drop support to iOS
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Sep 2017 20:12:18 +0000 (20:12 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 7 Sep 2017 20:12:18 +0000 (20:12 +0000)
https://bugs.webkit.org/show_bug.cgi?id=176492
<rdar://problem/34291584>

Reviewed by Tim Horton.

Source/WebCore:

Adds support for accepting dropped folders on iOS.

Tests: DataInteractionTests.ExternalSourceDataTransferItemGetFolderAsEntry
       DataInteractionTests.ExternalSourceDataTransferItemGetPlainTextFileAsEntry

* platform/ios/PasteboardIOS.mm:
(WebCore::Pasteboard::supportedFileUploadPasteboardTypes):

Add "public.folder" as a compatible pasteboard type for drops on iOS. This means file inputs and custom drop
targets that preventDefault() will, by default, be able to accept incoming folders.

* platform/ios/WebItemProviderPasteboard.mm:
(linkTemporaryItemProviderFilesToDropStagingDirectory):

Tweak temporaryFileURLForDataInteractionContent to also hard link UIKit's temporary files instead, and return
a non-null destination URL only if the necessary file operations succeeded. Also renames this helper to
linkTemporaryItemProviderFilesToDropStagingDirectory to better reflect its new purpose. This makes logic much
cleaner at the call site, which no longer checks against various conditions before proceeding to set the data
transfer URL.

(-[WebItemProviderPasteboard doAfterLoadingProvidedContentIntoFileURLs:synchronousTimeout:]):
(temporaryFileURLForDataInteractionContent): Deleted.

Tools:

Adds two new iOS drag and drop unit tests, which both exercise the DataTransferItem.webKitGetAsEntry codepath
upon drop. (...)GetFolderAsEntry creates a new folder in the temporary directory and uses it to generate an item
provider. This item provider is then dropped over a custom drop handling element, which writes information about
the exposed FileSystemEntries into a textarea. (...)ExternalSourceDataTransferItemGetPlainTextFileAsEntry does
something similar, except that it only drops a plain text file instead.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/DataTransferItem-getAsEntry.html: Added.

Introduce a new test page that dumps information about DataTransferItems' file system entries upon drop.

* TestWebKitAPI/Tests/ios/DataInteractionTests.mm:
(runTestWithTemporaryTextFile):
(runTestWithTemporaryFolder):

Introduce helpers to set up and tear down temporary files and folders over the duration of a test.

(TestWebKitAPI::setUpTestWebViewForDataTransferItems):
(TestWebKitAPI::TEST):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@221751 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/platform/ios/PasteboardIOS.mm
Source/WebCore/platform/ios/WebItemProviderPasteboard.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/DataTransferItem-getAsEntry.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/ios/DataInteractionTests.mm

index d6d493c..d8c2805 100644 (file)
@@ -1,3 +1,34 @@
+2017-09-07  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Directory Upload] Extend drag and drop support to iOS
+        https://bugs.webkit.org/show_bug.cgi?id=176492
+        <rdar://problem/34291584>
+
+        Reviewed by Tim Horton.
+
+        Adds support for accepting dropped folders on iOS.
+
+        Tests: DataInteractionTests.ExternalSourceDataTransferItemGetFolderAsEntry
+               DataInteractionTests.ExternalSourceDataTransferItemGetPlainTextFileAsEntry
+
+        * platform/ios/PasteboardIOS.mm:
+        (WebCore::Pasteboard::supportedFileUploadPasteboardTypes):
+
+        Add "public.folder" as a compatible pasteboard type for drops on iOS. This means file inputs and custom drop
+        targets that preventDefault() will, by default, be able to accept incoming folders.
+
+        * platform/ios/WebItemProviderPasteboard.mm:
+        (linkTemporaryItemProviderFilesToDropStagingDirectory):
+
+        Tweak temporaryFileURLForDataInteractionContent to also hard link UIKit's temporary files instead, and return
+        a non-null destination URL only if the necessary file operations succeeded. Also renames this helper to
+        linkTemporaryItemProviderFilesToDropStagingDirectory to better reflect its new purpose. This makes logic much
+        cleaner at the call site, which no longer checks against various conditions before proceeding to set the data
+        transfer URL.
+
+        (-[WebItemProviderPasteboard doAfterLoadingProvidedContentIntoFileURLs:synchronousTimeout:]):
+        (temporaryFileURLForDataInteractionContent): Deleted.
+
 2017-09-07  Alex Christensen  <achristensen@webkit.org>
 
         Modernize Geolocation code
index 299715d..731782c 100644 (file)
@@ -306,7 +306,7 @@ NSArray *Pasteboard::supportedWebContentPasteboardTypes()
 
 NSArray *Pasteboard::supportedFileUploadPasteboardTypes()
 {
-    return @[ (NSString *)kUTTypeContent, (NSString *)kUTTypeZipArchive ];
+    return @[ (NSString *)kUTTypeContent, (NSString *)kUTTypeZipArchive, (NSString *)kUTTypeFolder ];
 }
 
 bool Pasteboard::hasData()
index 392d5b0..8296559 100644 (file)
@@ -413,9 +413,10 @@ static BOOL typeConformsToTypes(NSString *type, NSArray *conformsToTypes)
     return numberOfFiles;
 }
 
-static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *suggestedName)
+static NSURL *linkTemporaryItemProviderFilesToDropStagingDirectory(NSURL *url, NSString *suggestedName, NSString *typeIdentifier)
 {
-    static NSString *defaultDataInteractionFileName = @"file";
+    static NSString *defaultDropFolderName = @"folder";
+    static NSString *defaultDropFileName = @"file";
     static NSString *dataInteractionDirectoryPrefix = @"data-interaction";
     if (!url)
         return nil;
@@ -424,11 +425,18 @@ static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *su
     if (!temporaryDataInteractionDirectory)
         return nil;
 
-    suggestedName = suggestedName ?: defaultDataInteractionFileName;
-    if (![suggestedName containsString:@"."])
+    NSURL *destination = nil;
+    BOOL isFolder = UTTypeConformsTo((CFStringRef)typeIdentifier, kUTTypeFolder);
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+
+    if (!suggestedName)
+        suggestedName = url.lastPathComponent ?: (isFolder ? defaultDropFolderName : defaultDropFileName);
+
+    if (![suggestedName containsString:@"."] && !isFolder)
         suggestedName = [suggestedName stringByAppendingPathExtension:url.pathExtension];
 
-    return [NSURL fileURLWithPath:[temporaryDataInteractionDirectory stringByAppendingPathComponent:suggestedName ?: url.lastPathComponent]];
+    destination = [NSURL fileURLWithPath:[temporaryDataInteractionDirectory stringByAppendingPathComponent:suggestedName]];
+    return [fileManager linkItemAtURL:url toURL:destination error:nil] ? destination : nil;
 }
 
 - (NSString *)typeIdentifierToLoadForRegisteredTypeIdentfiers:(NSArray<NSString *> *)registeredTypeIdentifiers
@@ -484,14 +492,13 @@ static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *su
         RetainPtr<NSString> suggestedName = [itemProvider suggestedName];
         dispatch_group_enter(fileLoadingGroup.get());
         dispatch_group_enter(synchronousFileLoadingGroup.get());
-        [itemProvider loadFileRepresentationForTypeIdentifier:typeIdentifier.get() completionHandler:[synchronousFileLoadingGroup, setFileURLsLock, indexInItemProviderArray, suggestedName, typeIdentifier, typeToFileURLMaps, fileLoadingGroup] (NSURL *url, NSError *error) {
+        [itemProvider loadFileRepresentationForTypeIdentifier:typeIdentifier.get() completionHandler:[synchronousFileLoadingGroup, setFileURLsLock, indexInItemProviderArray, suggestedName, typeIdentifier, typeToFileURLMaps, fileLoadingGroup] (NSURL *url, NSError *) {
             // After executing this completion block, UIKit removes the file at the given URL. However, we need this data to persist longer for the web content process.
             // To address this, we hard link the given URL to a new temporary file in the temporary directory. This follows the same flow as regular file upload, in
             // WKFileUploadPanel.mm. The temporary files are cleaned up by the system at a later time.
-            RetainPtr<NSURL> destinationURL = temporaryFileURLForDataInteractionContent(url, suggestedName.get());
-            if (destinationURL && !error && [[NSFileManager defaultManager] linkItemAtURL:url toURL:destinationURL.get() error:nil]) {
+            if (NSURL *destination = linkTemporaryItemProviderFilesToDropStagingDirectory(url, suggestedName.get(), typeIdentifier.get())) {
                 [setFileURLsLock lock];
-                [typeToFileURLMaps setObject:[NSDictionary dictionaryWithObject:destinationURL.get() forKey:typeIdentifier.get()] atIndexedSubscript:indexInItemProviderArray];
+                [typeToFileURLMaps setObject:[NSDictionary dictionaryWithObject:destination forKey:typeIdentifier.get()] atIndexedSubscript:indexInItemProviderArray];
                 [setFileURLsLock unlock];
             }
             dispatch_group_leave(fileLoadingGroup.get());
index 0ce384c..637e13a 100644 (file)
@@ -1,3 +1,31 @@
+2017-09-07  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Directory Upload] Extend drag and drop support to iOS
+        https://bugs.webkit.org/show_bug.cgi?id=176492
+        <rdar://problem/34291584>
+
+        Reviewed by Tim Horton.
+
+        Adds two new iOS drag and drop unit tests, which both exercise the DataTransferItem.webKitGetAsEntry codepath
+        upon drop. (...)GetFolderAsEntry creates a new folder in the temporary directory and uses it to generate an item
+        provider. This item provider is then dropped over a custom drop handling element, which writes information about
+        the exposed FileSystemEntries into a textarea. (...)ExternalSourceDataTransferItemGetPlainTextFileAsEntry does
+        something similar, except that it only drops a plain text file instead.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/DataTransferItem-getAsEntry.html: Added.
+
+        Introduce a new test page that dumps information about DataTransferItems' file system entries upon drop.
+
+        * TestWebKitAPI/Tests/ios/DataInteractionTests.mm:
+        (runTestWithTemporaryTextFile):
+        (runTestWithTemporaryFolder):
+
+        Introduce helpers to set up and tear down temporary files and folders over the duration of a test.
+
+        (TestWebKitAPI::setUpTestWebViewForDataTransferItems):
+        (TestWebKitAPI::TEST):
+
 2017-09-07  Filip Pizlo  <fpizlo@apple.com>
 
         WSL should check recursion
index 6fca682..558e1ef 100644 (file)
                F44D06451F395C26001A0E29 /* editor-state-test-harness.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F44D06441F395C0D001A0E29 /* editor-state-test-harness.html */; };
                F44D06471F39627A001A0E29 /* EditorStateTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44D06461F395C4D001A0E29 /* EditorStateTests.mm */; };
                F44D064A1F3962F2001A0E29 /* EditingTestHarness.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44D06491F3962E3001A0E29 /* EditingTestHarness.mm */; };
+               F4512E131F60C44600BB369E /* DataTransferItem-getAsEntry.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4512E121F60C43400BB369E /* DataTransferItem-getAsEntry.html */; };
                F4538EF71E8473E600B5C953 /* large-red-square.png in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4538EF01E846B4100B5C953 /* large-red-square.png */; };
                F45B63FB1F197F4A009D38B9 /* image-map.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F45B63FA1F197F33009D38B9 /* image-map.html */; };
                F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */; };
                                5C2936961D5C00ED00DEAB1E /* CookieMessage.html in Copy Resources */,
                                7AEAD4811E20122700416EFE /* CrossPartitionFileSchemeAccess.html in Copy Resources */,
                                290F4275172A221C00939FF0 /* custom-protocol-sync-xhr.html in Copy Resources */,
+                               F4512E131F60C44600BB369E /* DataTransferItem-getAsEntry.html in Copy Resources */,
                                C07E6CB213FD73930038B22B /* devicePixelRatio.html in Copy Resources */,
                                0799C34B1EBA3301003B7532 /* disableGetUserMedia.html in Copy Resources */,
                                F41AB9A21EF4696B0083FA08 /* div-and-large-image.html in Copy Resources */,
                F44D06461F395C4D001A0E29 /* EditorStateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EditorStateTests.mm; sourceTree = "<group>"; };
                F44D06481F3962E3001A0E29 /* EditingTestHarness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EditingTestHarness.h; sourceTree = "<group>"; };
                F44D06491F3962E3001A0E29 /* EditingTestHarness.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EditingTestHarness.mm; sourceTree = "<group>"; };
+               F4512E121F60C43400BB369E /* DataTransferItem-getAsEntry.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "DataTransferItem-getAsEntry.html"; sourceTree = "<group>"; };
                F4538EF01E846B4100B5C953 /* large-red-square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "large-red-square.png"; sourceTree = "<group>"; };
                F45B63FA1F197F33009D38B9 /* image-map.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "image-map.html"; sourceTree = "<group>"; };
                F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ActionSheetTests.mm; sourceTree = "<group>"; };
                                F4A32ECA1F0642F40047C544 /* contenteditable-in-iframe.html */,
                                A16F66B91C40EA2000BD4D24 /* ContentFiltering.html */,
                                5C2936941D5BFD1900DEAB1E /* CookieMessage.html */,
+                               F4512E121F60C43400BB369E /* DataTransferItem-getAsEntry.html */,
                                0799C34A1EBA32F4003B7532 /* disableGetUserMedia.html */,
                                F41AB99E1EF4692C0083FA08 /* div-and-large-image.html */,
                                837A35F01D9A1E6400663C57 /* DownloadRequestBlobURL.html */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/DataTransferItem-getAsEntry.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/DataTransferItem-getAsEntry.html
new file mode 100644 (file)
index 0000000..ac90c15
--- /dev/null
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+html, body {
+    font-family: -apple-system;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    font-size: 1em;
+}
+
+#droparea {
+    margin-top: 1em;
+    width: 100%;
+    height: 200px;
+    top: 0;
+    left: 0;
+}
+
+#output {
+    width: 100%;
+    height: calc(100% - 200px);
+    font-family: monospace;
+}
+</style>
+
+<div id="droparea">
+    <div>To manually test, drop something into this area and observe the output below.</div>
+</div>
+<textarea id="output"></textarea>
+
+<script>
+    function getChildEntries(entry) {
+        if (!entry.isDirectory)
+            return Promise.resolve([]);
+
+        return new Promise((resolve, reject) => {
+            let result = [];
+            let reader = entry.createReader();
+            let doBatch = () => {
+                reader.readEntries(entries => {
+                    if (entries.length > 0) {
+                        entries.forEach(e => result.push(e));
+                        doBatch();
+                    } else
+                        resolve(result);
+                }, reject);
+            };
+            doBatch();
+        });
+    }
+
+    droparea.addEventListener("dragenter", event => event.preventDefault());
+    droparea.addEventListener("dragover", event => event.preventDefault());
+    droparea.addEventListener("drop", handleDrop);
+
+    async function handleDrop(event)
+    {
+        await logItemAndFileEntryInformation(event.dataTransfer.items);
+        event.preventDefault();
+        webkit.messageHandlers.testHandler.postMessage("dropped");
+    }
+
+    function fileFromFileSystemFileEntry(fileEntry)
+    {
+        return new Promise(resolve => fileEntry.file(file => resolve(file)));
+    }
+
+    async function representationForFileSystemEntry(entry)
+    {
+        let representation = "";
+        if (entry.isDirectory) {
+            representation += `DIR: ${entry.fullPath}\n`;
+            for (let child of await getChildEntries(entry))
+                representation += await representationForFileSystemEntry(child);
+        } else if (entry.isFile) {
+            let file = await fileFromFileSystemFileEntry(entry);
+            representation += `FILE: ${entry.fullPath} ('${file.type}', ${file.size} bytes)\n`;
+        }
+        return representation;
+    }
+
+    async function logItemAndFileEntryInformation(items)
+    {
+        output.value = "";
+        for (let index = 0; index < items.length; index++) {
+            let item = items.item(index);
+            output.value += `Found data transfer item (kind: '${item.kind}', type: '${item.type}')\n`;
+            let entry = item.webkitGetAsEntry();
+            if (entry)
+                output.value += await representationForFileSystemEntry(entry);
+        }
+    }
+</script>
+</html>
index 55b0bbd..244a285 100644 (file)
@@ -34,6 +34,7 @@
 #import <MobileCoreServices/MobileCoreServices.h>
 #import <UIKit/NSItemProvider+UIKitAdditions.h>
 #import <WebKit/WKPreferencesPrivate.h>
+#import <WebKit/WKPreferencesRefPrivate.h>
 #import <WebKit/WKProcessPoolPrivate.h>
 #import <WebKit/WKWebViewConfigurationPrivate.h>
 #import <WebKit/WebItemProviderPasteboard.h>
@@ -164,6 +165,55 @@ static void checkDragCaretRectIsContainedInRect(CGRect caretRect, CGRect contain
         NSLog(@"Expected caret rect: %@ to fit within container rect: %@", NSStringFromCGRect(caretRect), NSStringFromCGRect(containerRect));
 }
 
+static void runTestWithTemporaryTextFile(void(^runTest)(NSURL *fileURL))
+{
+    NSString *fileName = [NSString stringWithFormat:@"drag-drop-text-file-%@.txt", [NSUUID UUID].UUIDString];
+    RetainPtr<NSURL> temporaryFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName] isDirectory:NO];
+    [[NSFileManager defaultManager] removeItemAtURL:temporaryFile.get() error:nil];
+
+    NSError *error = nil;
+    [@"This is a tiny blob of text." writeToURL:temporaryFile.get() atomically:YES encoding:NSUTF8StringEncoding error:&error];
+
+    if (error)
+        NSLog(@"Error writing temporary file: %@", error);
+
+    @try {
+        runTest(temporaryFile.get());
+    } @finally {
+        [[NSFileManager defaultManager] removeItemAtURL:temporaryFile.get() error:nil];
+    }
+}
+
+static void runTestWithTemporaryFolder(void(^runTest)(NSURL *folderURL))
+{
+    NSString *folderName = [NSString stringWithFormat:@"some.directory-%@", [NSUUID UUID].UUIDString];
+    RetainPtr<NSURL> temporaryFolder = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:folderName] isDirectory:YES];
+    [[NSFileManager defaultManager] removeItemAtURL:temporaryFolder.get() error:nil];
+
+    NSError *error = nil;
+    NSFileManager *defaultManager = [NSFileManager defaultManager];
+    [defaultManager createDirectoryAtURL:temporaryFolder.get() withIntermediateDirectories:NO attributes:nil error:&error];
+    [UIImagePNGRepresentation(testIconImage()) writeToURL:[temporaryFolder.get() URLByAppendingPathComponent:@"icon.png" isDirectory:NO] atomically:YES];
+    [testZIPArchive() writeToURL:[temporaryFolder.get() URLByAppendingPathComponent:@"archive.zip" isDirectory:NO] atomically:YES];
+
+    NSURL *firstSubdirectory = [temporaryFolder.get() URLByAppendingPathComponent:@"subdirectory1" isDirectory:YES];
+    [defaultManager createDirectoryAtURL:firstSubdirectory withIntermediateDirectories:NO attributes:nil error:&error];
+    [@"I am a text file in the first subdirectory." writeToURL:[firstSubdirectory URLByAppendingPathComponent:@"text-file-1.txt" isDirectory:NO] atomically:YES encoding:NSUTF8StringEncoding error:&error];
+
+    NSURL *secondSubdirectory = [temporaryFolder.get() URLByAppendingPathComponent:@"subdirectory2" isDirectory:YES];
+    [defaultManager createDirectoryAtURL:secondSubdirectory withIntermediateDirectories:NO attributes:nil error:&error];
+    [@"I am a text file in the second subdirectory." writeToURL:[secondSubdirectory URLByAppendingPathComponent:@"text-file-2.txt" isDirectory:NO] atomically:YES encoding:NSUTF8StringEncoding error:&error];
+
+    if (error)
+        NSLog(@"Error writing temporary file: %@", error);
+
+    @try {
+        runTest(temporaryFolder.get());
+    } @finally {
+        [[NSFileManager defaultManager] removeItemAtURL:temporaryFolder.get() error:nil];
+    }
+}
+
 namespace TestWebKitAPI {
 
 TEST(DataInteractionTests, ImageToContentEditable)
@@ -877,6 +927,88 @@ TEST(DataInteractionTests, ExternalSourceOverrideDropFileUpload)
     EXPECT_WK_STREQ("text/html", outputValue.UTF8String);
 }
 
+static RetainPtr<TestWKWebView> setUpTestWebViewForDataTransferItems()
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView synchronouslyLoadTestPageNamed:@"DataTransferItem-getAsEntry"];
+
+    auto preferences = (WKPreferencesRef)[[webView configuration] preferences];
+    WKPreferencesSetDataTransferItemsEnabled(preferences, true);
+    WKPreferencesSetDirectoryUploadEnabled(preferences, true);
+
+    return webView;
+}
+
+TEST(DataInteractionTests, ExternalSourceDataTransferItemGetFolderAsEntry)
+{
+    NSArray<NSString *> *expectedOutput = @[
+        @"Found data transfer item (kind: 'string', type: 'text/plain')",
+        @"Found data transfer item (kind: 'file', type: '')",
+        @"DIR: /somedirectory",
+        @"FILE: /somedirectory/icon.png ('image/png', 42130 bytes)",
+        @"DIR: /somedirectory/subdirectory1",
+        @"FILE: /somedirectory/subdirectory1/text-file-1.txt ('text/plain', 43 bytes)",
+        @"FILE: /somedirectory/archive.zip ('application/zip', 988 bytes)",
+        @"DIR: /somedirectory/subdirectory2",
+        @"FILE: /somedirectory/subdirectory2/text-file-2.txt ('text/plain', 44 bytes)",
+        @""
+    ];
+
+    auto webView = setUpTestWebViewForDataTransferItems();
+    __block bool done = false;
+    [webView performAfterReceivingMessage:@"dropped" action:^() {
+        done = true;
+    }];
+
+    runTestWithTemporaryFolder(^(NSURL *folderURL) {
+        auto itemProvider = adoptNS([[NSItemProvider alloc] init]);
+        [itemProvider setSuggestedName:@"somedirectory"];
+        [itemProvider registerFileRepresentationForTypeIdentifier:(NSString *)kUTTypeFolder fileOptions:0 visibility:NSItemProviderRepresentationVisibilityAll loadHandler:[capturedFolderURL = retainPtr(folderURL)] (FileLoadCompletionBlock completionHandler) -> NSProgress * {
+            completionHandler(capturedFolderURL.get(), NO, nil);
+            return nil;
+        }];
+
+        auto dataInteractionSimulator = adoptNS([[DataInteractionSimulator alloc] initWithWebView:webView.get()]);
+        [dataInteractionSimulator setExternalItemProviders:@[ itemProvider.get() ]];
+        [dataInteractionSimulator runFrom:CGPointMake(50, 50) to:CGPointMake(150, 50)];
+    });
+
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ([expectedOutput componentsJoinedByString:@"\n"], [webView stringByEvaluatingJavaScript:@"output.value"]);
+}
+
+TEST(DataInteractionTests, ExternalSourceDataTransferItemGetPlainTextFileAsEntry)
+{
+    NSArray<NSString *> *expectedOutput = @[
+        @"Found data transfer item (kind: 'string', type: 'text/plain')",
+        @"Found data transfer item (kind: 'file', type: 'text/plain')",
+        @"FILE: /foo.txt ('text/plain', 28 bytes)",
+        @""
+    ];
+
+    auto webView = setUpTestWebViewForDataTransferItems();
+    __block bool done = false;
+    [webView performAfterReceivingMessage:@"dropped" action:^() {
+        done = true;
+    }];
+
+    runTestWithTemporaryTextFile(^(NSURL *fileURL) {
+        auto itemProvider = adoptNS([[NSItemProvider alloc] init]);
+        [itemProvider setSuggestedName:@"foo"];
+        [itemProvider registerFileRepresentationForTypeIdentifier:(NSString *)kUTTypeUTF8PlainText fileOptions:0 visibility:NSItemProviderRepresentationVisibilityAll loadHandler:[capturedFileURL = retainPtr(fileURL)](FileLoadCompletionBlock completionHandler) -> NSProgress * {
+            completionHandler(capturedFileURL.get(), NO, nil);
+            return nil;
+        }];
+
+        auto dataInteractionSimulator = adoptNS([[DataInteractionSimulator alloc] initWithWebView:webView.get()]);
+        [dataInteractionSimulator setExternalItemProviders:@[ itemProvider.get() ]];
+        [dataInteractionSimulator runFrom:CGPointMake(50, 50) to:CGPointMake(150, 50)];
+    });
+
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ([expectedOutput componentsJoinedByString:@"\n"], [webView stringByEvaluatingJavaScript:@"output.value"]);
+}
+
 TEST(DataInteractionTests, ExternalSourceOverrideDropInsertURL)
 {
     auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);