summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/fs')
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-IndexedDB.https.any.js6
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-buckets.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-getUniqueId.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-isSameEntry.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-BroadcastChannel.https.window.js7
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-Error.https.window.js9
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-frames.https.window.js7
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-windows.https.window.js7
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-workers.https.window.js8
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-frames.https.window.js7
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-windows.https.window.js7
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-workers.https.window.js8
-rw-r--r--testing/web-platform/tests/fs/FileSystemBaseHandle-remove.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemDirectoryHandle-getDirectoryHandle.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemDirectoryHandle-getFileHandle.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemDirectoryHandle-iteration.https.any.js4
-rw-r--r--testing/web-platform/tests/fs/FileSystemDirectoryHandle-removeEntry.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemDirectoryHandle-resolve.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.tentative.window.js8
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-getFile.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-move.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js89
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.tentative.worker.js40
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.tentative.worker.js29
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.tentative.worker.js21
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.tentative.worker.js245
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.tentative.worker.js40
-rw-r--r--testing/web-platform/tests/fs/FileSystemWritableFileStream-piped.https.any.js4
-rw-r--r--testing/web-platform/tests/fs/FileSystemWritableFileStream-write.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemWritableFileStream.https.any.js3
-rw-r--r--testing/web-platform/tests/fs/META.yml3
-rw-r--r--testing/web-platform/tests/fs/README.md2
-rw-r--r--testing/web-platform/tests/fs/idlharness.https.any.js17
-rw-r--r--testing/web-platform/tests/fs/opaque-origin.https.window.js75
-rw-r--r--testing/web-platform/tests/fs/resources/message-target-dedicated-worker.js9
-rw-r--r--testing/web-platform/tests/fs/resources/message-target-service-worker.js9
-rw-r--r--testing/web-platform/tests/fs/resources/message-target-shared-worker.js14
-rw-r--r--testing/web-platform/tests/fs/resources/message-target.html22
-rw-r--r--testing/web-platform/tests/fs/resources/message-target.js158
-rw-r--r--testing/web-platform/tests/fs/resources/messaging-blob-helpers.js51
-rw-r--r--testing/web-platform/tests/fs/resources/messaging-helpers.js187
-rw-r--r--testing/web-platform/tests/fs/resources/messaging-serialize-helpers.js210
-rw-r--r--testing/web-platform/tests/fs/resources/opaque-origin-sandbox.html27
-rw-r--r--testing/web-platform/tests/fs/resources/sandboxed-fs-test-helpers.js24
-rw-r--r--testing/web-platform/tests/fs/resources/sync-access-handle-test.js17
-rw-r--r--testing/web-platform/tests/fs/resources/test-helpers.js116
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js121
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js19
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-getUniqueId.js89
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-isSameEntry.js107
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-BroadcastChannel.js82
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-Error.js244
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-frames.js44
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-windows.js35
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-workers.js40
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-frames.js40
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-windows.js31
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-workers.js35
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-remove.js105
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getDirectoryHandle.js117
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getFileHandle.js145
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-iteration.js100
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-removeEntry.js222
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-resolve.js27
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-create-sync-access-handle.js26
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js42
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js369
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemSyncAccessHandle-flush.js8
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-piped.js137
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-write.js334
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream.js105
71 files changed, 4147 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-IndexedDB.https.any.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-IndexedDB.https.any.js
new file mode 100644
index 0000000000..9e67fe8802
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-IndexedDB.https.any.js
@@ -0,0 +1,6 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=/IndexedDB/resources/support-promises.js
+// META: script=script-tests/FileSystemBaseHandle-IndexedDB.js
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-buckets.https.any.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-buckets.https.any.js
new file mode 100644
index 0000000000..6bb7d77119
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-buckets.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-buckets.js
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-getUniqueId.https.any.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-getUniqueId.https.any.js
new file mode 100644
index 0000000000..24ea20244c
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-getUniqueId.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-getUniqueId.js
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-isSameEntry.https.any.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-isSameEntry.https.any.js
new file mode 100644
index 0000000000..67d36dfae8
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-isSameEntry.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-isSameEntry.js
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-BroadcastChannel.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-BroadcastChannel.https.window.js
new file mode 100644
index 0000000000..ca25b548cb
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-BroadcastChannel.https.window.js
@@ -0,0 +1,7 @@
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-BroadcastChannel.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-Error.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-Error.https.window.js
new file mode 100644
index 0000000000..16a7002a2a
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-Error.https.window.js
@@ -0,0 +1,9 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-Error.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-frames.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-frames.https.window.js
new file mode 100644
index 0000000000..612c823295
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-frames.https.window.js
@@ -0,0 +1,7 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-MessagePort-frames.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-windows.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-windows.https.window.js
new file mode 100644
index 0000000000..28cec810ee
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-windows.https.window.js
@@ -0,0 +1,7 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-MessagePort-windows.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-workers.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-workers.https.window.js
new file mode 100644
index 0000000000..1599ba969d
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-MessagePort-workers.https.window.js
@@ -0,0 +1,8 @@
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-MessagePort-workers.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-frames.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-frames.https.window.js
new file mode 100644
index 0000000000..a0e41c51b1
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-frames.https.window.js
@@ -0,0 +1,7 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-frames.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-windows.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-windows.https.window.js
new file mode 100644
index 0000000000..1e3de1ea39
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-windows.https.window.js
@@ -0,0 +1,7 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-windows.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-workers.https.window.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-workers.https.window.js
new file mode 100644
index 0000000000..e690682b6f
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-postMessage-workers.https.window.js
@@ -0,0 +1,8 @@
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=resources/messaging-blob-helpers.js
+// META: script=resources/messaging-serialize-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-postMessage-workers.js
+// META: timeout=long
diff --git a/testing/web-platform/tests/fs/FileSystemBaseHandle-remove.https.any.js b/testing/web-platform/tests/fs/FileSystemBaseHandle-remove.https.any.js
new file mode 100644
index 0000000000..c3c1776784
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-remove.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemBaseHandle-remove.js
diff --git a/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getDirectoryHandle.https.any.js b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getDirectoryHandle.https.any.js
new file mode 100644
index 0000000000..69ca2bf367
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getDirectoryHandle.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemDirectoryHandle-getDirectoryHandle.js
diff --git a/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getFileHandle.https.any.js b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getFileHandle.https.any.js
new file mode 100644
index 0000000000..afe362e757
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-getFileHandle.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemDirectoryHandle-getFileHandle.js
diff --git a/testing/web-platform/tests/fs/FileSystemDirectoryHandle-iteration.https.any.js b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-iteration.https.any.js
new file mode 100644
index 0000000000..b337d61d76
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-iteration.https.any.js
@@ -0,0 +1,4 @@
+// META: script=/common/gc.js
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemDirectoryHandle-iteration.js
diff --git a/testing/web-platform/tests/fs/FileSystemDirectoryHandle-removeEntry.https.any.js b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-removeEntry.https.any.js
new file mode 100644
index 0000000000..a4be8bd267
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-removeEntry.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemDirectoryHandle-removeEntry.js
diff --git a/testing/web-platform/tests/fs/FileSystemDirectoryHandle-resolve.https.any.js b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-resolve.https.any.js
new file mode 100644
index 0000000000..6ee3270930
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemDirectoryHandle-resolve.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemDirectoryHandle-resolve.js
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.tentative.window.js b/testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.tentative.window.js
new file mode 100644
index 0000000000..aa4d91d15d
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.tentative.window.js
@@ -0,0 +1,8 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=script-tests/FileSystemFileHandle-create-sync-access-handle.js
+
+// This variable allows the test to differentiate between local and sandboxed
+// file systems, since createSyncAccessHandle() behavior is different each one.
+const file_system_type = 'sandboxed';
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-getFile.https.any.js b/testing/web-platform/tests/fs/FileSystemFileHandle-getFile.https.any.js
new file mode 100644
index 0000000000..fb93858fe7
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-getFile.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemFileHandle-getFile.js
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-move.https.any.js b/testing/web-platform/tests/fs/FileSystemFileHandle-move.https.any.js
new file mode 100644
index 0000000000..1f9678a041
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-move.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemFileHandle-move.js
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js
new file mode 100644
index 0000000000..c900e2f0ac
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js
@@ -0,0 +1,89 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/test-helpers.js');
+importScripts('resources/sandboxed-fs-test-helpers.js');
+
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ const fileHandle = await root_dir.getFileHandle('OPFS.test', {create: true});
+
+ const syncHandle1 = await fileHandle.createSyncAccessHandle();
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', fileHandle.createSyncAccessHandle());
+
+ syncHandle1.close();
+ const syncHandle2 = await fileHandle.createSyncAccessHandle();
+ syncHandle2.close();
+}, 'There can only be one open access handle at any given time');
+
+directory_test(async (t, root_dir) => {
+ const fooFileHandle = await root_dir.getFileHandle('foo.test', {create: true});
+ const barFileHandle = await root_dir.getFileHandle('bar.test', {create: true});
+
+ const fooSyncHandle = await fooFileHandle.createSyncAccessHandle();
+ t.add_cleanup(() => fooSyncHandle.close());
+
+ const barSyncHandle1 = await barFileHandle.createSyncAccessHandle();
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', barFileHandle.createSyncAccessHandle());
+
+ barSyncHandle1.close();
+ const barSyncHandle2 = await barFileHandle.createSyncAccessHandle();
+ barSyncHandle2.close();
+}, 'An access handle from one file does not interfere with the creation of an' +
+ ' access handle on another file');
+
+directory_test(async (t, root_dir) => {
+ const fooFileHandle = await root_dir.getFileHandle('foo.test', {create: true});
+ const barFileHandle = await root_dir.getFileHandle('bar.test', {create: true});
+
+ const fooWritable = await cleanup_writable(t, await fooFileHandle.createWritable());
+ t.add_cleanup(() => fooWritable.close());
+
+ const barSyncHandle = await barFileHandle.createSyncAccessHandle();
+ t.add_cleanup(() => barSyncHandle.close());
+}, 'A writable stream from one file does not interfere with the creation of an' +
+ ' access handle on another file');
+
+directory_test(async (t, root_dir) => {
+ const fooFileHandle = await root_dir.getFileHandle('foo.test', {create: true});
+ const barFileHandle = await root_dir.getFileHandle('bar.test', {create: true});
+
+ const fooSyncHandle = await fooFileHandle.createSyncAccessHandle();
+ t.add_cleanup(() => fooSyncHandle.close());
+
+ const barWritable = await cleanup_writable(t, await barFileHandle.createWritable());
+ t.add_cleanup(() => barWritable.close());
+}, 'An access handle from one file does not interfere with the creation of a' +
+ ' writable stream on another file');
+
+directory_test(async (t, root_dir) => {
+ const fileHandle = await root_dir.getFileHandle('OPFS.test', {create: true});
+
+ const syncHandle = await fileHandle.createSyncAccessHandle();
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', cleanup_writable(t, await fileHandle.createWritable()));
+
+ syncHandle.close();
+ const writable = await cleanup_writable(t, await fileHandle.createWritable());
+ await writable.close();
+}, 'Writable streams cannot be created if there is an open access handle');
+
+directory_test(async (t, root_dir) => {
+ const fileHandle = await root_dir.getFileHandle('OPFS.test', {create: true});
+
+ const writable1 = await cleanup_writable(t, await fileHandle.createWritable());
+ const writable2 = await cleanup_writable(t, await fileHandle.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', fileHandle.createSyncAccessHandle());
+
+ await writable1.close();
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', fileHandle.createSyncAccessHandle());
+
+ await writable2.close();
+ const syncHandle = await fileHandle.createSyncAccessHandle();
+ syncHandle.close();
+}, 'Access handles cannot be created if there are open Writable streams');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.tentative.worker.js
new file mode 100644
index 0000000000..1c6aaf38a3
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.tentative.worker.js
@@ -0,0 +1,40 @@
+importScripts("/resources/testharness.js");
+importScripts('resources/sync-access-handle-test.js');
+
+'use strict';
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ assert_equals(handle.close(), undefined);
+}, 'SyncAccessHandle.close is idempotent');
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ const readBuffer = new Uint8Array(4);
+ assert_throws_dom(
+ 'InvalidStateError', () => handle.read(readBuffer, {at: 0}));
+}, 'SyncAccessHandle.read fails after SyncAccessHandle.close');
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ const writeBuffer = new Uint8Array(4);
+ writeBuffer.set([96, 97, 98, 99]);
+ assert_throws_dom(
+ 'InvalidStateError', () => handle.write(writeBuffer, {at: 0}));
+}, 'SyncAccessHandle.write fails after SyncAccessHandle.close');
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ assert_throws_dom('InvalidStateError', () => handle.flush());
+}, 'SyncAccessHandle.flush fails after SyncAccessHandle.close');
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ assert_throws_dom('InvalidStateError', () => handle.getSize());
+}, 'SyncAccessHandle.getSize fails after SyncAccessHandle.close');
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.close(), undefined);
+ assert_throws_dom('InvalidStateError', () => handle.truncate(4));
+}, 'SyncAccessHandle.truncate fails after SyncAccessHandle.handle.close');
+
+done(); \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.tentative.worker.js
new file mode 100644
index 0000000000..96953a88f2
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.tentative.worker.js
@@ -0,0 +1,29 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/sync-access-handle-test.js');
+
+'use strict';
+
+sync_access_handle_test((t, handle) => {
+ handle.flush();
+}, 'Test flush on an empty file.');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const text = 'Hello Storage Foundation';
+ const writeBuffer = new TextEncoder().encode(text);
+ handle.write(writeBuffer, {at: 0});
+ handle.flush();
+ let readBuffer = new Uint8Array(text.length);
+ handle.read(readBuffer, {at: 0});
+ assert_equals(
+ text, new TextDecoder().decode(readBuffer),
+ 'Check that the written bytes and the read bytes match');
+}, 'SyncAccessHandle.read returns bytes written by SyncAccessHandle.write' +
+ ' after SyncAccessHandle.flush');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.tentative.worker.js
new file mode 100644
index 0000000000..4b62b280b9
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.tentative.worker.js
@@ -0,0 +1,21 @@
+importScripts("/resources/testharness.js");
+importScripts('resources/sync-access-handle-test.js');
+
+'use strict';
+
+sync_access_handle_test((t, handle) => {
+ assert_equals(handle.getSize(), 0);
+ const bufferSize = 4;
+ const writeBuffer = new Uint8Array(bufferSize);
+ writeBuffer.set([96, 97, 98, 99]);
+ handle.write(writeBuffer, {at: 0});
+ assert_equals(handle.getSize(), bufferSize);
+ let offset = 3;
+ handle.write(writeBuffer, {at: offset});
+ assert_equals(handle.getSize(), bufferSize + offset);
+ offset = 10;
+ handle.write(writeBuffer, {at: offset});
+ assert_equals(handle.getSize(), bufferSize + offset);
+}, 'test SyncAccessHandle.getSize after SyncAccessHandle.write');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.tentative.worker.js
new file mode 100644
index 0000000000..cdefc78b6c
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.tentative.worker.js
@@ -0,0 +1,245 @@
+importScripts("/resources/testharness.js");
+importScripts('resources/sync-access-handle-test.js');
+
+'use strict';
+
+sync_access_handle_test((t, handle) => {
+ const readBuffer = new Uint8Array(24);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(0, readBytes, 'Check that no bytes were read');
+}, 'Test reading an empty file through a sync access handle.');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const decoder = new TextDecoder();
+
+ const text = 'Hello Storage Foundation';
+ const writeBuffer = new TextEncoder().encode(text);
+ const writtenBytes = handle.write(writeBuffer, {at: 0});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ let readBuffer = new Uint8Array(writtenBytes);
+ let readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(writtenBytes, readBytes, 'Check that all bytes were read');
+ assert_equals(
+ text, decoder.decode(readBuffer),
+ 'Check that the written bytes and the read bytes match');
+
+ // Test a read of less bytes than available.
+ const expected = 'Storage';
+ readBuffer = new Uint8Array(expected.length);
+ readBytes = handle.read(readBuffer, {at: text.indexOf(expected)});
+ assert_equals(readBuffer.length, readBytes, 'Check that all bytes were read');
+ const actual = decoder.decode(readBuffer);
+ assert_equals(
+ expected, actual,
+ 'Partial read returned unexpected contents');
+}, 'Test writing and reading through a sync access handle.');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ for (text of ['Hello', 'Longer Text']) {
+ const writeBuffer = encoder.encode(text);
+ const writtenBytes = handle.write(writeBuffer, {at: 0});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ const readBuffer = new Uint8Array(writtenBytes);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(writtenBytes, readBytes, 'Check that all bytes were read');
+ assert_equals(
+ text, decoder.decode(readBuffer),
+ 'Check that the written bytes and the read bytes match');
+ }
+}, 'Test second write that is bigger than the first write');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ for (tuple
+ of [{input: 'Hello World', expected: 'Hello World'},
+ {input: 'foobar', expected: 'foobarWorld'}]) {
+ const text = tuple.input;
+ const expected = tuple.expected;
+ const writeBuffer = encoder.encode(text);
+ const writtenBytes = handle.write(writeBuffer, {at: 0});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ const readBuffer = new Uint8Array(expected.length);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(expected.length, readBytes, 'Check that all bytes were read');
+ assert_equals(
+ expected, decoder.decode(readBuffer),
+ 'Check that the written bytes and the read bytes match');
+ }
+}, 'Test second write that is smaller than the first write');
+
+sync_access_handle_test((t, handle) => {
+ const expected = 17;
+ const writeBuffer = new Uint8Array(1);
+ writeBuffer[0] = expected;
+ const offset = 5;
+ const writtenBytes = handle.write(writeBuffer, {at: offset});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ const fileLength = writeBuffer.byteLength + offset;
+ const readBuffer = new Uint8Array(fileLength);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(fileLength, readBytes, 'Check that all bytes were read');
+ for (let i = 0; i < offset; ++i) {
+ assert_equals(
+ readBuffer[i], 0,
+ `Gaps in the file should be filled with 0, but got ${readBuffer[i]}.`);
+ }
+
+ assert_equals(
+ readBuffer[offset], expected,
+ 'Gaps in the file should be filled with 0.');
+}, 'Test initial write with an offset');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ for (tuple
+ of [{input: 'Hello World', expected: 'Hello World', offset: 0},
+ {input: 'foobar', expected: 'Hello foobar', offset: 6}]) {
+ const text = tuple.input;
+ const expected = tuple.expected;
+ const offset = tuple.offset;
+ const writeBuffer = encoder.encode(text);
+ const writtenBytes = handle.write(writeBuffer, {at: offset});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ const readBuffer = new Uint8Array(expected.length);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(expected.length, readBytes, 'Check that all bytes were read');
+ const actual = decoder.decode(readBuffer);
+ assert_equals(
+ expected, actual,
+ 'Check content read from the handle');
+ }
+}, 'Test overwriting the file at an offset');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const decoder = new TextDecoder();
+
+ const text = 'Hello Storage Foundation';
+ const writeBuffer = new TextEncoder().encode(text);
+ const writtenBytes = handle.write(writeBuffer, {at: 0});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+ const bufferLength = text.length;
+ for (tuple
+ of [{offset: 0, expected: text},
+ {offset: 6, expected: text.substring(6)}]) {
+ const offset = tuple.offset;
+ const expected = tuple.expected;
+
+ const readBuffer = new Uint8Array(bufferLength);
+ const readBytes = handle.read(readBuffer, {at: offset});
+ assert_equals(expected.length, readBytes, 'Check that all bytes were read');
+ const actual = decoder.decode(readBuffer);
+ assert_true(
+ actual.startsWith(expected),
+ `Expected to read ${expected} but the actual value was ${actual}.`);
+ }
+
+ const readBuffer = new Uint8Array(bufferLength);
+ // Offset is greater than the file length.
+ const readBytes = handle.read(readBuffer, {at: bufferLength + 1});
+ assert_equals(0, readBytes, 'Check that no bytes were read');
+ for (let i = 0; i < readBuffer.byteLength; ++i) {
+ assert_equals(0, readBuffer[i], 'Check that the read buffer is unchanged.');
+ }
+}, 'Test read at an offset');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const expected = 'Hello Storage Foundation';
+ const writeBuffer = new TextEncoder().encode(expected);
+ const writtenBytes = handle.write(writeBuffer, {at: 0});
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+
+ const bufferLength = expected.length;
+ const readBuffer = new Uint8Array(expected.length);
+ // No options parameter provided, should read at offset 0.
+ const readBytes = handle.read(readBuffer);
+ assert_equals(expected.length, readBytes, 'Check that all bytes were read');
+ const actual = new TextDecoder().decode(readBuffer);
+ assert_equals(
+ expected, actual,
+ `Expected to read ${expected} but the actual value was ${actual}.`);
+}, 'Test read with default options');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const expected = 'Hello Storage Foundation';
+ const writeBuffer = new TextEncoder().encode(expected);
+ // No options parameter provided, should write at offset 0.
+ const writtenBytes = handle.write(writeBuffer);
+ assert_equals(
+ writeBuffer.byteLength, writtenBytes,
+ 'Check that all bytes were written.');
+
+ const bufferLength = expected.length;
+ const readBuffer = new Uint8Array(expected.length);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(expected.length, readBytes, 'Check that all bytes were read');
+ const actual = new TextDecoder().decode(readBuffer);
+ assert_equals(
+ expected, actual,
+ `Expected to read ${expected} but the actual value was ${actual}.`);
+}, 'Test write with default options');
+
+sync_access_handle_test((t, handle) => {
+ const readBuffer = new Uint8Array(24);
+ assert_throws_js(TypeError, () => handle.read(readBuffer, {at: -1}));
+}, 'Test reading at a negative offset fails.');
+
+sync_access_handle_test((t, handle) => {
+ const text = 'foobar';
+ const writeBuffer = new TextEncoder().encode(text);
+ assert_throws_js(TypeError, () => handle.write(writeBuffer, {at: -1}));
+
+ const readBuffer = new Uint8Array(24);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(0, readBytes, 'Check that no bytes were written');
+}, 'Test writing at a negative offset fails.');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.tentative.worker.js
new file mode 100644
index 0000000000..921e0066c9
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.tentative.worker.js
@@ -0,0 +1,40 @@
+importScripts("/resources/testharness.js");
+importScripts('resources/sync-access-handle-test.js');
+
+'use strict';
+
+sync_access_handle_test((t, handle) => {
+ // Without this assertion, the test passes even if truncate is not defined.
+ assert_implements(handle.truncate,
+ "SyncAccessHandle.truncate is not implemented.");
+
+ handle.truncate(4);
+ assert_equals(handle.getSize(), 4);
+ handle.truncate(2);
+ assert_equals(handle.getSize(), 2);
+ handle.truncate(7);
+ assert_equals(handle.getSize(), 7);
+ handle.truncate(0);
+ assert_equals(handle.getSize(), 0);
+ assert_throws_js(TypeError, () => handle.truncate(-4));
+}, 'test SyncAccessHandle.truncate with different sizes');
+
+sync_access_handle_test((t, handle) => {
+ const writeBuffer = new Uint8Array(4);
+ writeBuffer.set([96, 97, 98, 99]);
+ handle.write(writeBuffer, {at: 0});
+
+ handle.truncate(2);
+ let readBuffer = new Uint8Array(6);
+ assert_equals(2, handle.read(readBuffer, {at: 0}));
+ let expected = new Uint8Array(6);
+ expected.set([96, 97, 0, 0, 0, 0]);
+ assert_array_equals(expected, readBuffer);
+
+ // Resize the file to 6, expect that everything beyond the old size is '0'.
+ handle.truncate(6);
+ assert_equals(6, handle.read(readBuffer, {at: 0}));
+ assert_array_equals(expected, readBuffer);
+}, 'test SyncAccessHandle.truncate after SyncAccessHandle.write');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemWritableFileStream-piped.https.any.js b/testing/web-platform/tests/fs/FileSystemWritableFileStream-piped.https.any.js
new file mode 100644
index 0000000000..eed6a561dc
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemWritableFileStream-piped.https.any.js
@@ -0,0 +1,4 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=../streams/resources/recording-streams.js
+// META: script=script-tests/FileSystemWritableFileStream-piped.js
diff --git a/testing/web-platform/tests/fs/FileSystemWritableFileStream-write.https.any.js b/testing/web-platform/tests/fs/FileSystemWritableFileStream-write.https.any.js
new file mode 100644
index 0000000000..7ef0ea0ef8
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemWritableFileStream-write.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemWritableFileStream-write.js
diff --git a/testing/web-platform/tests/fs/FileSystemWritableFileStream.https.any.js b/testing/web-platform/tests/fs/FileSystemWritableFileStream.https.any.js
new file mode 100644
index 0000000000..16dbbe6a80
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemWritableFileStream.https.any.js
@@ -0,0 +1,3 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=script-tests/FileSystemWritableFileStream.js
diff --git a/testing/web-platform/tests/fs/META.yml b/testing/web-platform/tests/fs/META.yml
new file mode 100644
index 0000000000..23d7765cdf
--- /dev/null
+++ b/testing/web-platform/tests/fs/META.yml
@@ -0,0 +1,3 @@
+spec: https://fs.spec.whatwg.org/
+suggested_reviewers:
+ - mkruisselbrink \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/README.md b/testing/web-platform/tests/fs/README.md
new file mode 100644
index 0000000000..8b99a0140d
--- /dev/null
+++ b/testing/web-platform/tests/fs/README.md
@@ -0,0 +1,2 @@
+This directory contains tests for the
+[File System](https://fs.spec.whatwg.org/) specification.
diff --git a/testing/web-platform/tests/fs/idlharness.https.any.js b/testing/web-platform/tests/fs/idlharness.https.any.js
new file mode 100644
index 0000000000..508beccc36
--- /dev/null
+++ b/testing/web-platform/tests/fs/idlharness.https.any.js
@@ -0,0 +1,17 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+idl_test(
+ ['fs'],
+ ['storage', 'streams'],
+ idl_array => {
+ idl_array.add_objects({
+ // TODO: Add instances of FileSystemHandle, FileSystemFileHandle,
+ // FileSystemDirectoryHandle, FileSystemWritableFileStream, and
+ // StorageManager.
+ });
+ }
+);
diff --git a/testing/web-platform/tests/fs/opaque-origin.https.window.js b/testing/web-platform/tests/fs/opaque-origin.https.window.js
new file mode 100644
index 0000000000..94b4cd7978
--- /dev/null
+++ b/testing/web-platform/tests/fs/opaque-origin.https.window.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const kSandboxWindowUrl = 'resources/opaque-origin-sandbox.html';
+
+function add_iframe(test, src, sandbox) {
+ const iframe = document.createElement('iframe');
+ iframe.src = src;
+ if (sandbox !== undefined) {
+ iframe.sandbox = sandbox;
+ }
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => {
+ iframe.remove();
+ });
+}
+
+// Creates a data URI iframe that uses postMessage() to provide its parent
+// with the test result. The iframe checks for the existence of
+// |property_name| on the window.
+async function verify_does_exist_in_data_uri_iframe(
+ test, property_name) {
+ const iframe_content =
+ '<script>' +
+ ' const is_property_name_defined = ' +
+ ` (self.${property_name} !== undefined);` +
+ ' parent.postMessage({is_property_name_defined}, "*")' +
+ '</script>';
+
+ const data_uri = `data:text/html,${encodeURIComponent(iframe_content)}`;
+ add_iframe(test, data_uri);
+
+ const event_watcher = new EventWatcher(test, self, 'message');
+ const message_event = await event_watcher.wait_for('message')
+
+ assert_true(message_event.data.is_property_name_defined,
+ `Data URI iframes must define '${property_name}'.`);
+}
+
+// |kSandboxWindowUrl| sends the result of navigator.storage.getDirectory() to
+// this window. For windows using sandbox='allow-scripts', this must produce a
+// rejected promise.
+async function verify_results_from_sandboxed_child_window(test) {
+ const event_watcher = new EventWatcher(test, self, 'message');
+
+ const message_event = await event_watcher.wait_for('message');
+ assert_equals(message_event.data,
+ 'navigator.storage.getDirectory(): REJECTED: SecurityError');
+}
+
+promise_test(async test => {
+ await verify_does_exist_in_data_uri_iframe(
+ test, 'FileSystemDirectoryHandle');
+}, 'FileSystemDirectoryHandle must be defined for data URI iframes.');
+
+promise_test(
+ async test => {
+ add_iframe(test, kSandboxWindowUrl, /*sandbox=*/ 'allow-scripts');
+ await verify_results_from_sandboxed_child_window(test);
+ },
+ 'navigator.storage.getDirectory() must reject in a sandboxed iframe.');
+
+promise_test(
+ async test => {
+ const child_window_url = kSandboxWindowUrl +
+ '?pipe=header(Content-Security-Policy, sandbox allow-scripts)';
+
+ const child_window = window.open(child_window_url);
+ test.add_cleanup(() => {
+ child_window.close();
+ });
+
+ await verify_results_from_sandboxed_child_window(test);
+ },
+ 'navigator.storage.getDirectory() ' +
+ 'must reject in a sandboxed opened window.');
diff --git a/testing/web-platform/tests/fs/resources/message-target-dedicated-worker.js b/testing/web-platform/tests/fs/resources/message-target-dedicated-worker.js
new file mode 100644
index 0000000000..26ff23ef8a
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target-dedicated-worker.js
@@ -0,0 +1,9 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+add_message_event_handlers(/*receiver=*/self, /*target=*/self);
diff --git a/testing/web-platform/tests/fs/resources/message-target-service-worker.js b/testing/web-platform/tests/fs/resources/message-target-service-worker.js
new file mode 100644
index 0000000000..4a6174ae3b
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target-service-worker.js
@@ -0,0 +1,9 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+add_message_event_handlers(/*receiver=*/self); \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/resources/message-target-shared-worker.js b/testing/web-platform/tests/fs/resources/message-target-shared-worker.js
new file mode 100644
index 0000000000..6829c61d4c
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target-shared-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+self.addEventListener('connect', connect_event => {
+ const message_port = connect_event.ports[0];
+ add_message_event_handlers(
+ /*receiver=*/message_port, /*target=*/message_port);
+ message_port.start();
+}); \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/resources/message-target.html b/testing/web-platform/tests/fs/resources/message-target.html
new file mode 100644
index 0000000000..32c7f0c56c
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src='test-helpers.js'></script>
+<script src='messaging-serialize-helpers.js'></script>
+<script src='message-target.js'></script>
+<script id="inline_script">
+ 'use strict'
+
+ if (window.parent !== null) {
+ window.parent.postMessage('LOADED', { targetOrigin: '*' });
+ }
+
+ if (window.opener !== null) {
+ window.opener.postMessage('LOADED', { targetOrigin: '*' });
+ }
+
+ // Use an undefined message target to send responses to
+ // MessageEvent::source instead.
+ const target = undefined;
+
+ add_message_event_handlers(
+ /*receiver=*/self, target, /*target_origin=*/'*');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/resources/message-target.js b/testing/web-platform/tests/fs/resources/message-target.js
new file mode 100644
index 0000000000..7c29afb799
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target.js
@@ -0,0 +1,158 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/test-helpers.js
+
+// add_message_event_handlers() is the helper function used to setup all
+// message targets, including iframes and workers.
+//
+// Adds a message event handler and a message error handler to |receiver|.
+// The 'data' property from received MessageEvents must include a 'type'
+// property. The 'type' selects the test logic to run. Most message type
+// handlers use postMessage() to respond to the sender with test results.
+// The sender then validates the test results after receiving the response.
+//
+// Both |target| and |target_origin| are optional. |target| is used
+// to send message responses back to the sender. When omitted, the
+// 'source' from received MessageEvents is used instead.
+//
+// For window messaging, |target_origin| specifies the origin to receive
+// responses. Most window tests use '*' for the |target_origin|. Worker
+// and message port tests must use undefined for |target_origin| to avoid
+// exceptions.
+function add_message_event_handlers(receiver, target, target_origin) {
+ receiver.addEventListener('message', async function (message_event) {
+ const message_data = message_event.data;
+
+ // Reply to the sender using the 'source' from the received MessageEvent.
+ let message_source = message_event.source;
+ if (message_source === null) {
+ // However, some message senders, like DedicatedWorkers, don't include
+ // a source. Fallback to the target when the source is null.
+ message_source = target;
+ }
+
+ try {
+ switch (message_data.type) {
+ case 'receive-message-port':
+ // Receive a MessagePort to use as a message target for testing.
+ add_message_event_handlers(
+ /*receiver=*/message_data.message_port,
+ /*target=*/message_data.message_port);
+ message_data.message_port.start();
+ break;
+
+ case 'create-broadcast-channel':
+ // Create a BroadcastChannel to use as a message target for testing.
+ const broadcast_channel =
+ new BroadcastChannel(message_data.broadcast_channel_name);
+ add_message_event_handlers(
+ /*receiver=*/broadcast_channel,
+ /*target=*/broadcast_channel);
+ message_source.postMessage(
+ { type: 'broadcast-channel-created' },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'receive-file-system-handles':
+ // Receive a list of cloned FileSystemFileHandles. Access the
+ // properties of each FileSystemFileHandle by serializing the
+ // handle to a JavaScript object. Then respond with the serialized
+ // results, enabling the sender to verify that the cloned handle
+ // produced the expected property values from this execution context.
+ const serialized_handles = [];
+ const cloned_handles = message_data.cloned_handles;
+ for (let i = 0; i < cloned_handles.length; ++i) {
+ const serialized = await serialize_handle(cloned_handles[i]);
+ serialized_handles.push(serialized);
+ }
+ message_source.postMessage({
+ type: 'receive-serialized-file-system-handles',
+ serialized_handles,
+ // Respond with the cloned handles to create new clones for
+ // the sender to verify.
+ cloned_handles,
+ }, { targetOrigin: target_origin });
+ break;
+
+ case 'receive-serialized-file-system-handles':
+ // Do nothing. This message is meant for test runner validation.
+ // Other message targets may receive this message while testing
+ // broadcast channels.
+ break;
+
+ case 'create-file':
+ // Create a new file and then respond to the sender with it.
+ const directory = await navigator.storage.getDirectory();
+ const file_handle =
+ await directory.getFileHandle('temp-file', { create: true });
+ message_source.postMessage(
+ { type: 'receive-file', file_handle },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'create-directory':
+ // Create a new directory and then respond to the sender with it.
+ const parent_directory = await navigator.storage.getDirectory();
+ const directory_handle =
+ await parent_directory.getDirectoryHandle('temp-directory',
+ { create: true });
+ message_source.postMessage(
+ { type: 'receive-directory', directory_handle },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'create-sync-access-handle':
+ // Receive a file and create a sync access handle out of it. Report
+ // success to the sender.
+ let success = true;
+ try {
+ const access_handle = await message_data.file_handle
+ .createSyncAccessHandle({mode: "in-place"});
+ access_handle.close();
+ } catch (error) {
+ success = false;
+ }
+
+ message_source.postMessage(
+ { type: 'receive-sync-access-handle-result', success },
+ { targetOrigin: target_origin });
+ break;
+
+ default:
+ throw `Unknown message type: '${message_data.type}'`;
+ }
+ } catch (error) {
+ // Respond with an error to trigger a failure in the sender's
+ // test runner.
+ message_source.postMessage(`ERROR: ${error}`,
+ { targetOrigin: target_origin });
+ }
+ });
+
+ receiver.addEventListener('messageerror', async function (message_event) {
+ // Select the target for message responses (see comment in 'message' event
+ // listener above).
+ let message_source = message_event.source;
+ if (message_source === null) {
+ message_source = target;
+ }
+
+ try {
+ // Respond with the MessageEvent's property values, enabling the sender
+ // to verify results.
+ const serialized_message_error_event =
+ serialize_message_error_event(message_event);
+ message_source.postMessage({
+ type: 'serialized-message-error',
+ serialized_message_error_event
+ }, { targetOrigin: target_origin });
+ } catch (error) {
+ // Respond with an error to trigger a failure in the sender's
+ // test runner.
+ message_source.postMessage(`ERROR: ${error}`,
+ { targetOrigin: target_origin });
+ }
+ });
+}
diff --git a/testing/web-platform/tests/fs/resources/messaging-blob-helpers.js b/testing/web-platform/tests/fs/resources/messaging-blob-helpers.js
new file mode 100644
index 0000000000..852f2e2d32
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/messaging-blob-helpers.js
@@ -0,0 +1,51 @@
+'use strict';
+
+// Creates a blob URL with the contents of 'message-target.html'. Use the
+// blob as an iframe src or a window.open() URL, which creates a same origin
+// message target.
+async function create_message_target_blob_url(test) {
+ const html = await create_message_target_html_without_subresources(test);
+ const blob = new Blob([html], { type: 'text/html' });
+ return URL.createObjectURL(blob);
+}
+
+// Creates a data URI with the contents of 'message-target.html'. Use the
+// data URI as an iframe src, which creates a cross origin message target.
+async function create_message_target_data_uri(test) {
+ const iframe_html =
+ await create_message_target_html_without_subresources(test);
+ return `data:text/html,${encodeURIComponent(iframe_html)}`;
+}
+
+// Constructs a version of 'message-target.html' without any subresources.
+// Enables the creation of blob URLs, data URIs and iframe srcdocs re-using
+// the contents of 'message-target.html'.
+async function create_message_target_html_without_subresources(test) {
+ const test_helpers_script = await fetch_text('resources/test-helpers.js');
+
+ const messaging_helpers_script =
+ await fetch_text('resources/messaging-helpers.js');
+
+ const messaging_serialize_helpers_script =
+ await fetch_text('resources/messaging-serialize-helpers.js');
+
+ const message_target_script =
+ await fetch_text('resources/message-target.js');
+
+ // Get the inline script code from 'message-target.html'.
+ const iframe = await add_iframe(test, { src: 'resources/message-target.html' });
+ const iframe_script =
+ iframe.contentWindow.document.getElementById('inline_script').outerHTML;
+ iframe.remove();
+
+ return '<!DOCTYPE html>' +
+ `<script>${test_helpers_script}</script>` +
+ `<script>${messaging_serialize_helpers_script}</script>` +
+ `<script>${message_target_script}</script>` +
+ `${iframe_script}`;
+}
+
+async function fetch_text(url) {
+ const response = await fetch(url);
+ return await response.text();
+}
diff --git a/testing/web-platform/tests/fs/resources/messaging-helpers.js b/testing/web-platform/tests/fs/resources/messaging-helpers.js
new file mode 100644
index 0000000000..776c0c50d5
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/messaging-helpers.js
@@ -0,0 +1,187 @@
+'use strict';
+
+// This script depends on the following script:
+// /fs/resources/test-helpers.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+// Define the URL constants used for each type of message target, including
+// iframes and workers.
+const kDocumentMessageTarget = 'resources/message-target.html';
+const kSharedWorkerMessageTarget = 'resources/message-target-shared-worker.js';
+const kServiceWorkerMessageTarget =
+ 'resources/message-target-service-worker.js';
+const kDedicatedWorkerMessageTarget =
+ 'resources/message-target-dedicated-worker.js';
+
+function create_dedicated_worker(test, url) {
+ const dedicated_worker = new Worker(url);
+ test.add_cleanup(() => {
+ dedicated_worker.terminate();
+ });
+ return dedicated_worker;
+}
+
+async function create_service_worker(test, script_url, scope) {
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope);
+ test.add_cleanup(() => {
+ return registration.unregister();
+ });
+ return registration;
+}
+
+// Creates an iframe and waits to receive a message from the iframe.
+// Valid |options| include src, srcdoc and sandbox, which mirror the
+// corresponding iframe element properties.
+async function add_iframe(test, options) {
+ const iframe = document.createElement('iframe');
+
+ if (options.sandbox !== undefined) {
+ iframe.sandbox = options.sandbox;
+ }
+
+ if (options.src !== undefined) {
+ iframe.src = options.src;
+ }
+
+ if (options.srcdoc !== undefined) {
+ iframe.srcdoc = options.srcdoc;
+ }
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => {
+ iframe.remove();
+ });
+
+ await wait_for_loaded_message(self);
+ return iframe;
+}
+
+// Creates a child window using window.open() and waits to receive a message
+// from the child window.
+async function open_window(test, url) {
+ const child_window = window.open(url);
+ test.add_cleanup(() => {
+ child_window.close();
+ });
+ await wait_for_loaded_message(self);
+ return child_window;
+}
+
+// Wait until |receiver| gets a message event with the data set to 'LOADED'.
+// The postMessage() tests use messaging instead of the loaded event because
+// cross-origin child windows from window.open() do not dispatch the loaded
+// event to the parent window.
+async function wait_for_loaded_message(receiver) {
+ const message_promise = new Promise((resolve, reject) => {
+ receiver.addEventListener('message', message_event => {
+ if (message_event.data === 'LOADED') {
+ resolve();
+ } else {
+ reject('The message target must receive a "LOADED" message response.');
+ }
+ });
+ });
+ await message_promise;
+}
+
+// Sets up a new message channel. Sends one port to |target| and then returns
+// the other port.
+function create_message_channel(target, target_origin) {
+ const message_channel = new MessageChannel();
+
+ const message_data =
+ { type: 'receive-message-port', message_port: message_channel.port2 };
+ target.postMessage(
+ message_data,
+ {
+ transfer: [message_channel.port2],
+ targetOrigin: target_origin
+ });
+ message_channel.port1.start();
+ return message_channel.port1;
+}
+
+// Creates a variety of different FileSystemFileHandles for testing.
+async function create_file_system_handles(test, root) {
+ // Create some files to use with postMessage().
+ const empty_file = await createEmptyFile(test, 'empty-file', root);
+ const first_file = await createFileWithContents(
+ test, 'first-file-with-contents', 'first-text-content', root);
+ const second_file = await createFileWithContents(
+ test, 'second-file-with-contents', 'second-text-content', root);
+
+ // Create an empty directory to use with postMessage().
+ const empty_directory = await createDirectory(test, 'empty-directory', root);
+
+ // Create a directory containing both files and subdirectories to use
+ // with postMessage().
+ const directory_with_files =
+ await createDirectory(test, 'directory-with-files', root);
+ await createFileWithContents(test, 'first-file-in-directory',
+ 'first-directory-text-content', directory_with_files);
+ await createFileWithContents(test, 'second-file-in-directory',
+ 'second-directory-text-content', directory_with_files);
+ const subdirectory =
+ await createDirectory(test, 'subdirectory', directory_with_files);
+ await createFileWithContents(test, 'first-file-in-subdirectory',
+ 'first-subdirectory-text-content', subdirectory);
+
+ return [
+ empty_file,
+ first_file,
+ second_file,
+ // Include the same FileSystemFileHandle twice.
+ second_file,
+ empty_directory,
+ // Include the Same FileSystemDirectoryHandle object twice.
+ empty_directory,
+ directory_with_files
+ ];
+}
+
+// Tests sending an array of FileSystemHandles to |target| with postMessage().
+// The array includes both FileSystemFileHandles and FileSystemDirectoryHandles.
+// After receiving the message, |target| accesses all cloned handles by
+// serializing the properties of each handle to a JavaScript object.
+//
+// |target| then responds with the resulting array of serialized handles. The
+// response also includes the array of cloned handles, which creates more
+// clones. After receiving the response, this test runner verifies that both
+// the serialized handles and the cloned handles contain the expected properties.
+async function do_post_message_test(
+ test, root_dir, receiver, target, target_origin) {
+ // Create and send the handles to |target|.
+ const handles =
+ await create_file_system_handles(test, root_dir, target, target_origin);
+ target.postMessage(
+ { type: 'receive-file-system-handles', cloned_handles: handles },
+ { targetOrigin: target_origin });
+
+ // Wait for |target| to respond with results.
+ const event_watcher = new EventWatcher(test, receiver, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_equals(response.type, 'receive-serialized-file-system-handles',
+ 'The test runner must receive a "serialized-file-system-handles" ' +
+ `message response. Actual response: ${response}`);
+
+ // Verify the results.
+ const expected_serialized_handles = await serialize_handles(handles);
+
+ assert_equals_serialized_handles(
+ response.serialized_handles, expected_serialized_handles);
+
+ await assert_equals_cloned_handles(response.cloned_handles, handles);
+}
+
+// Runs the same test as do_post_message_test(), but uses a MessagePort.
+// This test starts by establishing a message channel between the test runner
+// and |target|. Afterwards, the test sends FileSystemHandles through the
+// message port channel.
+async function do_message_port_test(test, root_dir, target, target_origin) {
+ const message_port = create_message_channel(target, target_origin);
+ await do_post_message_test(
+ test, root_dir, /*receiver=*/ message_port, /*target=*/ message_port);
+}
diff --git a/testing/web-platform/tests/fs/resources/messaging-serialize-helpers.js b/testing/web-platform/tests/fs/resources/messaging-serialize-helpers.js
new file mode 100644
index 0000000000..c7dfc0436e
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/messaging-serialize-helpers.js
@@ -0,0 +1,210 @@
+'use strict';
+
+// This script depends on the following script:
+// /fs/resources/test-helpers.js
+
+// Serializes an array of FileSystemHandles where each element can be either a
+// FileSystemFileHandle or FileSystemDirectoryHandle.
+async function serialize_handles(handle_array) {
+ const serialized_handle_array = [];
+ for (let i = 0; i < handle_array.length; ++i) {
+ serialized_handle_array.push(await serialize_handle(handle_array[i]));
+ }
+ return serialized_handle_array;
+}
+
+// Serializes either a FileSystemFileHandle or FileSystemDirectoryHandle.
+async function serialize_handle(handle) {
+ switch (handle.kind) {
+ case 'directory':
+ return await serialize_file_system_directory_handle(handle);
+ case 'file':
+ return await serialize_file_system_file_handle(handle);
+ default:
+ throw 'Object is not a FileSystemFileHandle or ' +
+ `FileSystemDirectoryHandle ${handle}`;
+ }
+}
+
+// Creates a dictionary for a FileSystemHandle base, which contains
+// serialized properties shared by both FileSystemFileHandle and
+// FileSystemDirectoryHandle.
+async function serialize_file_system_handle(handle) {
+ return {
+ kind: handle.kind,
+ name: handle.name
+ };
+}
+
+// Create a dictionary with each property value in FileSystemFileHandle.
+// Also, reads the contents of the file to include with the returned
+// dictionary. Example output:
+// {
+// kind: "file",
+// name: "example-file-name",
+// contents: "example-file-contents"
+// }
+async function serialize_file_system_file_handle(file_handle) {
+ const contents = await getFileContents(file_handle);
+
+ const serialized_file_system_handle =
+ await serialize_file_system_handle(file_handle);
+
+ return Object.assign(serialized_file_system_handle, { contents });
+}
+
+// Create a dictionary with each property value in FileSystemDirectoryHandle.
+// Example output:
+// {
+// kind: "directory",
+// name: "example-directory-name",
+// files: [<first serialized file>, ...]
+// directories: [<first serialized subdirectory>, ...]
+// }
+async function serialize_file_system_directory_handle(directory_handle) {
+ // Serialize the contents of the directory.
+ const serialized_files = [];
+ const serialized_directories = [];
+ for await (const child_handle of directory_handle.values()) {
+ const serialized_child_handle = await serialize_handle(child_handle);
+ if (child_handle.kind === "directory") {
+ serialized_directories.push(serialized_child_handle);
+ } else {
+ serialized_files.push(serialized_child_handle);
+ }
+ }
+
+ // Order the serialized contents of the directory by name.
+ serialized_files.sort((left, right) => {
+ return left.name.localeCompare(right.name);
+ });
+ serialized_directories.sort((left, right) => {
+ return left.name.localeCompare(right.name);
+ });
+
+ // Serialize the directory's common properties shared by all
+ // FileSystemHandles.
+ const serialized_file_system_handle =
+ await serialize_file_system_handle(directory_handle);
+
+ return Object.assign(
+ serialized_file_system_handle,
+ { files: serialized_files, directories: serialized_directories });
+}
+
+// Verifies |left_array| is a clone of |right_array| where each element
+// is a cloned FileSystemHandle with the same properties and contents.
+async function assert_equals_cloned_handles(left_array, right_array) {
+ assert_equals(left_array.length, right_array.length,
+ 'Each array of FileSystemHandles must have the same length');
+
+ for (let i = 0; i < left_array.length; ++i) {
+ assert_not_equals(left_array[i], right_array[i],
+ 'Clones must create new FileSystemHandle instances.');
+
+ const left_serialized = await serialize_handle(left_array[i]);
+ const right_serialized = await serialize_handle(right_array[i]);
+ assert_equals_serialized_handle(left_serialized, right_serialized);
+ }
+}
+
+// Verifies |left_array| is the same as |right_array| where each element
+// is a serialized FileSystemHandle with the same properties.
+function assert_equals_serialized_handles(left_array, right_array) {
+ assert_equals(left_array.length, right_array.length,
+ 'Each array of serialized handles must have the same length');
+
+ for (let i = 0; i < left_array.length; ++i) {
+ assert_equals_serialized_handle(left_array[i], right_array[i]);
+ }
+}
+
+// Verifies each property of a serialized FileSystemFileHandle or
+// FileSystemDirectoryHandle.
+function assert_equals_serialized_handle(left, right) {
+ switch (left.kind) {
+ case 'directory':
+ assert_equals_serialized_file_system_directory_handle(left, right);
+ break;
+ case 'file':
+ assert_equals_serialized_file_system_file_handle(left, right);
+ break;
+ default:
+ throw 'Object is not a FileSystemFileHandle or ' +
+ `FileSystemDirectoryHandle ${left}`;
+ }
+}
+
+// Compares the output of serialize_file_system_handle() for
+// two FileSystemHandles.
+function assert_equals_serialized_file_system_handle(left, right) {
+ assert_equals(left.kind, right.kind,
+ 'Each FileSystemHandle instance must use the expected "kind".');
+
+ assert_equals(left.name, right.name,
+ 'Each FileSystemHandle instance must use the expected "name" ' +
+ ' property.');
+}
+
+// Compares the output of serialize_file_system_file_handle()
+// for two FileSystemFileHandle.
+function assert_equals_serialized_file_system_file_handle(left, right) {
+ assert_equals_serialized_file_system_handle(left, right);
+ assert_equals(left.contents, right.contents,
+ 'Each FileSystemFileHandle instance must have the same contents.');
+}
+
+// Compares the output of serialize_file_system_directory_handle()
+// for two FileSystemDirectoryHandles.
+function assert_equals_serialized_file_system_directory_handle(left, right) {
+ assert_equals_serialized_file_system_handle(left, right);
+
+ assert_equals(left.files.length, right.files.length,
+ 'Each FileSystemDirectoryHandle must contain the same number of ' +
+ 'file children');
+
+ for (let i = 0; i < left.files.length; ++i) {
+ assert_equals_serialized_file_system_file_handle(
+ left.files[i], right.files[i]);
+ }
+
+ assert_equals(left.directories.length, right.directories.length,
+ 'Each FileSystemDirectoryHandle must contain the same number of ' +
+ 'directory children');
+
+ for (let i = 0; i < left.directories.length; ++i) {
+ assert_equals_serialized_file_system_directory_handle(
+ left.directories[i], right.directories[i]);
+ }
+}
+
+// Creates a dictionary with interesting property values from MessageEvent.
+function serialize_message_error_event(message_error_event) {
+ return {
+ data: message_error_event.data,
+ origin: message_error_event.origin,
+ last_event_id: message_error_event.lastEventId,
+ has_source: (message_error_event.source !== null),
+ ports_length: message_error_event.ports.length
+ };
+}
+
+// Compares the output of serialize_message_error_event() with an
+// expected result.
+function assert_equals_serialized_message_error_event(
+ serialized_event, expected_origin, expected_has_source) {
+ assert_equals(serialized_event.data, null,
+ 'The message error event must set the "data" property to null.');
+
+ assert_equals(serialized_event.origin, expected_origin,
+ 'The message error event must have the expected "origin" property.');
+
+ assert_equals(serialized_event.last_event_id, "",
+ 'The message error event must set the "lastEventId" property to the empty string.');
+
+ assert_equals(serialized_event.has_source, expected_has_source,
+ 'The message error event must have the expected "source" property.');
+
+ assert_equals(serialized_event.ports_length, 0,
+ 'The message error event must not contain any message ports.');
+}
diff --git a/testing/web-platform/tests/fs/resources/opaque-origin-sandbox.html b/testing/web-platform/tests/fs/resources/opaque-origin-sandbox.html
new file mode 100644
index 0000000000..b2582ca4c2
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/opaque-origin-sandbox.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<script>
+ 'use strict'
+
+ // Sends a message containing the result of navigator.storage.getDirectory()
+ // to its creator.
+
+ function post_message(data) {
+ if (window.parent !== null) {
+ window.parent.postMessage(data, { targetOrigin: '*' });
+ }
+ if (window.opener !== null) {
+ window.opener.postMessage(data, { targetOrigin: '*' });
+ }
+ }
+
+ try {
+ navigator.storage.getDirectory()
+ .then(() => {
+ post_message('navigator.storage.getDirectory(): FULFILLED');
+ }).catch(error => {
+ post_message(`navigator.storage.getDirectory(): REJECTED: ${error.name}`);
+ });
+ } catch (error) {
+ post_message(`navigator.storage.getDirectory(): EXCEPTION: ${error.name}`);
+ }
+</script>
diff --git a/testing/web-platform/tests/fs/resources/sandboxed-fs-test-helpers.js b/testing/web-platform/tests/fs/resources/sandboxed-fs-test-helpers.js
new file mode 100644
index 0000000000..9c93212f49
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/sandboxed-fs-test-helpers.js
@@ -0,0 +1,24 @@
+// This file defines a directory_test() function that can be used to define
+// tests that require a FileSystemDirectoryHandle. The implementation of that
+// function in this file will return an empty directory in the sandboxed file
+// system.
+//
+// Another implementation of this function exists in
+// file-system-access/local-fs-test-helpers.js, where that version uses the
+// local file system instead.
+
+async function cleanupSandboxedFileSystem() {
+ const dir = await navigator.storage.getDirectory();
+ for await (let entry of dir.values())
+ await dir.removeEntry(entry.name, {recursive: entry.kind === 'directory'});
+}
+
+function directory_test(func, description) {
+ promise_test(async t => {
+ // To be extra resilient against bad tests, cleanup before every test.
+ await cleanupSandboxedFileSystem();
+
+ const dir = await navigator.storage.getDirectory();
+ await func(t, dir);
+ }, description);
+}
diff --git a/testing/web-platform/tests/fs/resources/sync-access-handle-test.js b/testing/web-platform/tests/fs/resources/sync-access-handle-test.js
new file mode 100644
index 0000000000..46c5d3072c
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/sync-access-handle-test.js
@@ -0,0 +1,17 @@
+async function cleanupSandboxedFileSystem() {
+ const dir = await navigator.storage.getDirectory();
+ for await (let entry of dir.values())
+ await dir.removeEntry(entry.name, {recursive: entry.kind === 'directory'});
+}
+
+function sync_access_handle_test(test, description) {
+ promise_test(async t => {
+ // To be extra resilient against bad tests, cleanup before every test.
+ await cleanupSandboxedFileSystem();
+ const dir = await navigator.storage.getDirectory();
+ const fileHandle = await dir.getFileHandle('OPFS.test', {create: true});
+ const syncHandle = await fileHandle.createSyncAccessHandle();
+ test(t, syncHandle);
+ syncHandle.close();
+ }, description);
+}
diff --git a/testing/web-platform/tests/fs/resources/test-helpers.js b/testing/web-platform/tests/fs/resources/test-helpers.js
new file mode 100644
index 0000000000..01593165a0
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/test-helpers.js
@@ -0,0 +1,116 @@
+// A special path component meaning "this directory."
+const kCurrentDirectory = '.';
+
+// A special path component meaning "the parent directory."
+const kParentDirectory = '..';
+
+// Array of separators used to separate components in hierarchical paths.
+let kPathSeparators;
+if (navigator.userAgent.includes('Windows NT')) {
+ // Windows uses both '/' and '\' as path separators.
+ kPathSeparators = ['/', '\\'];
+} else {
+ kPathSeparators = ['/'];
+}
+
+async function getFileSize(handle) {
+ const file = await handle.getFile();
+ return file.size;
+}
+
+async function getFileContents(handle) {
+ const file = await handle.getFile();
+ return new Response(file).text();
+}
+
+async function getDirectoryEntryCount(handle) {
+ let result = 0;
+ for await (let entry of handle) {
+ result++;
+ }
+ return result;
+}
+
+async function getSortedDirectoryEntries(handle) {
+ let result = [];
+ for await (let entry of handle.values()) {
+ if (entry.kind === 'directory') {
+ result.push(entry.name + '/');
+ } else {
+ result.push(entry.name);
+ }
+ }
+ result.sort();
+ return result;
+}
+
+async function createDirectory(test, name, parent) {
+ const new_dir_handle = await parent.getDirectoryHandle(name, {create: true});
+ cleanup(test, new_dir_handle, async () => {
+ try {
+ await parent.removeEntry(name, {recursive: true});
+ } catch (e) {
+ // Ignore any errors when removing directories, as tests might
+ // have already removed the directory.
+ }
+ });
+ return new_dir_handle;
+}
+
+async function createEmptyFile(test, name, parent) {
+ const handle = await parent.getFileHandle(name, {create: true});
+ cleanup(test, handle, async () => {
+ try {
+ await parent.removeEntry(name);
+ } catch (e) {
+ // Ignore any errors when removing files, as tests might already remove
+ // the file.
+ }
+ });
+ // Make sure the file is empty.
+ assert_equals(await getFileSize(handle), 0);
+ return handle;
+}
+
+async function createFileWithContents(test, name, contents, parent) {
+ const handle = await createEmptyFile(test, name, parent);
+ const writer = await handle.createWritable();
+ await writer.write(new Blob([contents]));
+ await writer.close();
+ return handle;
+}
+
+var fs_cleanups = [];
+
+async function cleanup(test, value, cleanup_func) {
+ if (fs_cleanups.length === 0) {
+ // register to get called back once from cleanup
+ test.add_cleanup(async () => {
+ // Cleanup in LIFO order to ensure locks are released correctly relative
+ // to thinks like removeEntry(). Do so in a serialized form, not in parallel!
+ fs_cleanups.reverse();
+ for (let cleanup of fs_cleanups) {
+ try {
+ await cleanup();
+ } catch (e) {
+ // Ignore any errors when removing files, as tests might already remove
+ // the file.
+ }
+ }
+ fs_cleanups.length = 0;
+ });
+ }
+ fs_cleanups.push(cleanup_func);
+ return value;
+}
+
+async function cleanup_writable(test, value) {
+ return cleanup(test, value, async () => {
+ try {
+ return (await value).close();
+ } catch (e) {
+ // Ignore any errors when closing writables, since attempting to close
+ // aborted or closed writables will error.
+ }
+ });
+}
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js
new file mode 100644
index 0000000000..15a0c2a3b5
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js
@@ -0,0 +1,121 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ const handles = await create_file_system_handles(t, root_dir);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store');
+ });
+
+ const value = handles;
+
+ const tx = db.transaction('store', 'readwrite');
+ const store = tx.objectStore('store');
+ await promiseForRequest(t, store.put(value, 'key'));
+ const result = await promiseForRequest(t, store.get('key'));
+
+ await promiseForTransaction(t, tx);
+
+ assert_true(Array.isArray(result), 'Result should be an array');
+ assert_equals(result.length, value.length);
+ await assert_equals_cloned_handles(result, value);
+}, 'Store handle in IndexedDB and read from pending transaction.');
+
+directory_test(async (t, root_dir) => {
+ const handles = await create_file_system_handles(t, root_dir);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store');
+ });
+
+ const value = handles;
+
+ let tx = db.transaction('store', 'readwrite');
+ let store = tx.objectStore('store');
+ await promiseForRequest(t, store.put(value, 'key'));
+ await promiseForTransaction(t, tx);
+
+ tx = db.transaction('store', 'readonly');
+ store = tx.objectStore('store');
+ const result = await promiseForRequest(t, store.get('key'));
+ await promiseForTransaction(t, tx);
+
+ assert_true(Array.isArray(result), 'Result should be an array');
+ assert_equals(result.length, value.length);
+ await assert_equals_cloned_handles(result, value);
+}, 'Store handle in IndexedDB and read from new transaction.');
+
+directory_test(async (t, root_dir) => {
+ const handles = await create_file_system_handles(t, root_dir);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store');
+ });
+
+ const value = {handles, blob: new Blob(["foobar"])};
+
+ let tx = db.transaction('store', 'readwrite');
+ let store = tx.objectStore('store');
+ await promiseForRequest(t, store.put(value, 'key'));
+ await promiseForTransaction(t, tx);
+
+ tx = db.transaction('store', 'readonly');
+ store = tx.objectStore('store');
+ const result = await promiseForRequest(t, store.get('key'));
+ await promiseForTransaction(t, tx);
+
+ assert_true(Array.isArray(result.handles), 'Result should be an array');
+ assert_equals(result.handles.length, value.handles.length);
+ await assert_equals_cloned_handles(result.handles, value.handles);
+
+ assert_equals(await result.blob.text(), await value.blob.text());
+}, 'Store handles and blobs in IndexedDB.');
+
+directory_test(async (t, root_dir) => {
+ const handles = await create_file_system_handles(t, root_dir);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store');
+ });
+
+ const value = handles;
+
+ let tx = db.transaction('store', 'readwrite');
+ let store = tx.objectStore('store');
+ await promiseForRequest(t, store.put(value, 'key'));
+ await promiseForTransaction(t, tx);
+
+ tx = db.transaction('store', 'readonly');
+ store = tx.objectStore('store');
+ let cursor_request = store.openCursor();
+ await requestWatcher(t, cursor_request).wait_for('success');
+ const result = cursor_request.result.value;
+ await promiseForTransaction(t, tx);
+
+ assert_true(Array.isArray(result), 'Result should be an array');
+ assert_equals(result.length, value.length);
+ await assert_equals_cloned_handles(result, value);
+}, 'Store handle in IndexedDB and read using a cursor.');
+
+directory_test(async (t, root_dir) => {
+ const handles = await create_file_system_handles(t, root_dir);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store', {keyPath: 'key'});
+ });
+
+ const value = handles;
+ let tx = db.transaction('store', 'readwrite');
+ let store = tx.objectStore('store');
+ await promiseForRequest(t, store.put({key: 'key', value}));
+ await promiseForTransaction(t, tx);
+
+ tx = db.transaction('store', 'readonly');
+ store = tx.objectStore('store');
+ const result = await promiseForRequest(t, store.get('key'));
+ await promiseForTransaction(t, tx);
+
+ assert_true(Array.isArray(result.value), 'Result should be an array');
+ assert_equals(result.value.length, value.length);
+ await assert_equals_cloned_handles(result.value, value);
+}, 'Store handle in IndexedDB using inline keys.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js
new file mode 100644
index 0000000000..98261995f9
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js
@@ -0,0 +1,19 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ const inboxBucket = await navigator.storageBuckets.open('inbox');
+ const inboxRootDir = await inboxBucket.getDirectory();
+
+ assert_false(await inboxRootDir.isSameEntry(root_dir));
+
+ const handle1 = await createEmptyFile(t, 'mtime.txt', inboxRootDir);
+ const handle2 = await inboxRootDir.getFileHandle('mtime.txt');
+ assert_true(await handle1.isSameEntry(handle2));
+}, 'isSameEntry works as expected with buckets');
+
+directory_test(async (t, root_dir) => {
+ const inboxBucket = await navigator.storageBuckets.open('inbox');
+ await navigator.storageBuckets.delete('inbox');
+ const directoryPromise = inboxBucket.getDirectory();
+ await promise_rejects_dom(t, 'InvalidStateError', directoryPromise);
+}, 'getDirectory promise rejects if bucket has been deleted');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-getUniqueId.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-getUniqueId.js
new file mode 100644
index 0000000000..ee0cd5e349
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-getUniqueId.js
@@ -0,0 +1,89 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ assert_equals(await root_dir.getUniqueId(), await root_dir.getUniqueId());
+
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ assert_equals(await subdir.getUniqueId(), await subdir.getUniqueId());
+}, 'identical directory handles return the same ID');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+
+ assert_not_equals(await root_dir.getUniqueId(), await subdir.getUniqueId());
+}, 'different directories return different IDs');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const subdir2 = await root_dir.getDirectoryHandle('subdir-name');
+
+ assert_equals(await subdir.getUniqueId(), await subdir2.getUniqueId());
+}, 'different handles for the same directory return the same ID');
+
+directory_test(async (t, root_dir) => {
+ const handle = await createEmptyFile(t, 'foo.txt', root_dir);
+
+ assert_equals(await handle.getUniqueId(), await handle.getUniqueId());
+}, 'identical file handles return the same unique ID');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'foo.txt', root_dir);
+ const handle2 = await createEmptyFile(t, 'bar.txt', root_dir);
+
+ assert_not_equals(await handle1.getUniqueId(), await handle2.getUniqueId());
+}, 'different files return different IDs');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'foo.txt', root_dir);
+ const handle2 = await root_dir.getFileHandle('foo.txt');
+
+ assert_equals(await handle1.getUniqueId(), await handle2.getUniqueId());
+}, 'different handles for the same file return the same ID');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'foo.txt', root_dir);
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const handle2 = await createEmptyFile(t, 'foo.txt', subdir);
+
+ assert_not_equals(await handle1.getUniqueId(), await handle2.getUniqueId());
+}, 'two files of the same name in different directories return different IDs');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'foo.txt', root_dir);
+ const handle2 = await createDirectory(t, 'subdir-name', root_dir);
+
+ assert_not_equals(await handle1.getUniqueId(), await handle2.getUniqueId());
+}, 'a file and a directory return different IDs');
+
+directory_test(async (t, root_dir) => {
+ const file_handle = await createEmptyFile(t, 'foo', root_dir);
+ const file_id = await file_handle.getUniqueId();
+
+ // Remove the file.
+ await root_dir.removeEntry('foo');
+
+ // Create a directory of the same name and path.
+ const dir_handle = await createDirectory(t, 'foo', root_dir);
+ assert_not_equals(await dir_handle.getUniqueId(), file_id);
+}, 'a file and a directory of the same path return different IDs');
+
+directory_test(async (t, root_dir) => {
+ const handle = await createEmptyFile(t, 'foo.txt', root_dir);
+ const id_before = await handle.getUniqueId();
+
+ // Write to the file. The unique ID should not change.
+ const writable = await cleanup_writable(t, await handle.createWritable());
+ await writable.write("blah");
+ await writable.close();
+
+ assert_equals(await handle.getUniqueId(), id_before);
+}, 'unique ID of a file handle does not change after writes');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+
+ const UUIDRegex =
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
+ assert_true(UUIDRegex.test(await root_dir.getUniqueId()));
+ assert_true(UUIDRegex.test(await subdir.getUniqueId()));
+}, 'unique ID is in GUID version 4 format');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-isSameEntry.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-isSameEntry.js
new file mode 100644
index 0000000000..e3b6d1891e
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-isSameEntry.js
@@ -0,0 +1,107 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ assert_true(await root_dir.isSameEntry(root_dir));
+
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ assert_true(await subdir.isSameEntry(subdir));
+}, 'isSameEntry for identical directory handles returns true');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+
+ assert_false(await root_dir.isSameEntry(subdir));
+ assert_false(await subdir.isSameEntry(root_dir));
+}, 'isSameEntry for different directories returns false');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const subdir2 = await root_dir.getDirectoryHandle('subdir-name');
+
+ assert_true(await subdir.isSameEntry(subdir2));
+ assert_true(await subdir2.isSameEntry(subdir));
+}, 'isSameEntry for different handles for the same directory');
+
+directory_test(async (t, root_dir) => {
+ const handle = await createEmptyFile(t, 'mtime.txt', root_dir);
+
+ assert_true(await handle.isSameEntry(handle));
+}, 'isSameEntry for identical file handles returns true');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'mtime.txt', root_dir);
+ const handle2 = await createEmptyFile(t, 'foo.txt', root_dir);
+
+ assert_false(await handle1.isSameEntry(handle2));
+ assert_false(await handle2.isSameEntry(handle1));
+}, 'isSameEntry for different files returns false');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'mtime.txt', root_dir);
+ const handle2 = await root_dir.getFileHandle('mtime.txt');
+
+ assert_true(await handle1.isSameEntry(handle2));
+ assert_true(await handle2.isSameEntry(handle1));
+}, 'isSameEntry for different handles for the same file');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'mtime.txt', root_dir);
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const handle2 = await createEmptyFile(t, 'mtime.txt', subdir);
+
+ assert_false(await handle1.isSameEntry(handle2));
+ assert_false(await handle2.isSameEntry(handle1));
+}, 'isSameEntry comparing a file to a file in a different directory returns false');
+
+directory_test(async (t, root_dir) => {
+ const handle1 = await createEmptyFile(t, 'mtime.txt', root_dir);
+ const handle2 = await createDirectory(t, 'subdir-name', root_dir);
+
+ assert_false(await handle1.isSameEntry(handle2));
+ assert_false(await handle2.isSameEntry(handle1));
+}, 'isSameEntry comparing a file to a directory returns false');
+
+directory_test(async (t, root_dir) => {
+ const filename = 'foo';
+ const handle1 = await createEmptyFile(t, filename, root_dir);
+ // Remove the file and create a new file of the same path.
+ await root_dir.removeEntry(filename);
+ const handle2 = await createEmptyFile(t, filename, root_dir);
+
+ assert_true(
+ await handle1.isSameEntry(handle2),
+ 'two file handles pointing at the same path should be considered the same entry');
+ assert_true(
+ await handle2.isSameEntry(handle1),
+ 'two file handles pointing at the same path should be considered the same entry');
+}, 'isSameEntry comparing two files pointing to the same path returns true');
+
+directory_test(async (t, root_dir) => {
+ const filename = 'foo';
+ const handle1 = await createDirectory(t, filename, root_dir);
+ // Remove the directory and create a new directory of the same path.
+ await root_dir.removeEntry(filename);
+ const handle2 = await createDirectory(t, filename, root_dir);
+
+ assert_true(
+ await handle1.isSameEntry(handle2),
+ 'two directory handles pointing at the same path should be considered the same entry');
+ assert_true(
+ await handle2.isSameEntry(handle1),
+ 'two directory handles pointing at the same path should be considered the same entry');
+}, 'isSameEntry comparing two directories pointing to the same path returns true');
+
+directory_test(async (t, root_dir) => {
+ const filename = 'foo';
+ const dir_handle = await createDirectory(t, filename, root_dir);
+ // Remove the directory and create a file of the same path.
+ await root_dir.removeEntry(filename);
+ const file_handle = await createEmptyFile(t, filename, root_dir);
+
+ assert_false(
+ await dir_handle.isSameEntry(file_handle),
+ 'a file and directory handle pointing at the same path should not be considered the same entry');
+ assert_false(
+ await file_handle.isSameEntry(dir_handle),
+ 'a file and directory handle pointing at the same path should not be considered the same entry');
+}, 'isSameEntry comparing a file to a directory of the same path returns false');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-BroadcastChannel.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-BroadcastChannel.js
new file mode 100644
index 0000000000..681037db2f
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-BroadcastChannel.js
@@ -0,0 +1,82 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+// Sets up a new broadcast channel in |target|. Posts a message instructing
+// |target| to open the broadcast channel using |broadcast_channel_name|.
+async function create_broadcast_channel(
+ test, broadcast_channel_name, receiver, target, target_origin) {
+ target.postMessage(
+ { type: 'create-broadcast-channel', broadcast_channel_name },
+ { targetOrigin: target_origin });
+ const event_watcher = new EventWatcher(test, receiver, 'message');
+
+ // Wait until |target| is listening to the broad cast channel.
+ const message_event = await event_watcher.wait_for('message');
+ assert_equals(message_event.data.type, 'broadcast-channel-created',
+ 'The message target must receive a "broadcast-channel-created" message ' +
+ 'response.');
+}
+
+// This test is very similar to 'FileSystemBaseHandle-postMessage.js'. It
+// starts by creating three message targets for the broadcast channel:
+// an iframe, dedicated worker and a service worker. After setup, an array
+// of FileSystemHandles is sent across the broadcast channel. The test
+// expects three responses -- one from each message target.
+directory_test(async (t, root) => {
+ const broadcast_channel_name = 'file-system-file-handle-channel';
+ const broadcast_channel = new BroadcastChannel(broadcast_channel_name);
+ const broadcast_channel_event_watcher =
+ new EventWatcher(t, broadcast_channel, 'message');
+
+ const iframe = await add_iframe(t, { src: kDocumentMessageTarget });
+ await create_broadcast_channel(
+ t, broadcast_channel_name, self, iframe.contentWindow, '*');
+
+ const scope = `${kServiceWorkerMessageTarget}` +
+ '?post-message-to-broadcast-channel-with-file-handle';
+
+ const registration = await create_service_worker(
+ t, kServiceWorkerMessageTarget, scope);
+
+ await create_broadcast_channel(
+ t, broadcast_channel_name,
+ navigator.serviceWorker, registration.installing);
+
+ const dedicated_worker =
+ create_dedicated_worker(t, kDedicatedWorkerMessageTarget);
+
+ await create_broadcast_channel(
+ t, broadcast_channel_name, dedicated_worker, dedicated_worker);
+
+ const handles = await create_file_system_handles(t, root);
+
+ broadcast_channel.postMessage(
+ { type: 'receive-file-system-handles', cloned_handles: handles });
+
+ const expected_response_count = 3;
+ const responses = [];
+ for (let i = 0; i < expected_response_count; ++i) {
+ const message_event =
+ await broadcast_channel_event_watcher.wait_for('message');
+ responses.push(message_event.data);
+ }
+
+ const expected_serialized_handles = await serialize_handles(handles);
+
+ for (let i = 0; i < responses.length; ++i) {
+ assert_equals(responses[i].type, 'receive-serialized-file-system-handles',
+ 'The test runner must receive a "serialized-file-system-handles" ' +
+ `message response. Actual response: ${responses[i]}`);
+
+ assert_equals_serialized_handles(
+ responses[i].serialized_handles, expected_serialized_handles);
+
+ await assert_equals_cloned_handles(responses[i].cloned_handles, handles);
+ }
+}, 'Send and receive messages using a broadcast channel in an iframe, ' +
+'dedicated worker and service worker.'); \ No newline at end of file
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-Error.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-Error.js
new file mode 100644
index 0000000000..7c97a7da48
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-Error.js
@@ -0,0 +1,244 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+// /common/get-host-info.sub.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+// Define URL constants for cross origin windows.
+const kRemoteOrigin = get_host_info().HTTPS_REMOTE_ORIGIN;
+const kRemoteOriginDocumentMessageTarget = `${kRemoteOrigin}${base_path()}` +
+ kDocumentMessageTarget;
+
+// Sending a FileSystemHandle to a cross origin |target| through postMessage()
+// must dispatch the 'messageerror' event.
+//
+// This test sends a FileSystemHandle to |target|. |target| responds with a
+// serialized MessageEvent from the 'messageerror' event, allowing the test
+// runner to verify MessageEvent properties.
+async function do_send_message_error_test(
+ test,
+ root_dir,
+ receiver,
+ target,
+ target_origin,
+ // False when the MessageEvent's source is null.
+ expected_has_source,
+ // The origin of MessageEvents received by |target|.
+ expected_origin) {
+ const message_watcher = new EventWatcher(test, receiver, 'message');
+
+ // Send a file to |target|.
+ const file = await createFileWithContents(
+ test, 'test-error-file', 'test-error-file-contents', root_dir);
+ target.postMessage(
+ { type: 'receive-file-system-handles', cloned_file_system_handles: [file] },
+ { targetOrigin: target_origin });
+
+ // Wait for |target| to respond with results.
+ let message_event = await message_watcher.wait_for('message');
+ const first_response = message_event.data;
+ assert_equals(first_response.type, 'serialized-message-error',
+ 'The test runner must receive a "serialized-message-error" message ' +
+ 'in response to a FileSystemFileHandle message.');
+
+ // Verify the results.
+ assert_equals_serialized_message_error_event(
+ first_response.serialized_message_error_event,
+ expected_origin, expected_has_source);
+
+ // Send a directory to |target|.
+ const directory = await createDirectory(
+ test, 'test-error-directory', root_dir);
+
+ target.postMessage(
+ {
+ type: 'receive-file-system-handles',
+ cloned_file_system_handles: [directory]
+ }, { targetOrigin: target_origin });
+
+ // Wait for |target| to respond with results.
+ message_event = await message_watcher.wait_for('message');
+ const second_response = message_event.data;
+ assert_equals(second_response.type, 'serialized-message-error',
+ 'The test runner must receive a "serialized-message-error" message ' +
+ 'response to a FileSystemDirectoryHandle message.');
+
+ // Verify the results.
+ assert_equals_serialized_message_error_event(
+ second_response.serialized_message_error_event,
+ expected_origin, expected_has_source);
+}
+
+// This test receives a FileSystemHandle from |target|. This test runner
+// must dispatch the 'messageerror' event after receiving a handle from target.
+async function do_receive_message_error_test(
+ test,
+ receiver,
+ target,
+ target_origin,
+ // False when the MessageEvent's source is null.
+ expected_has_source,
+ // The origin of MessageEvents received by this test runner.
+ expected_origin) {
+ const error_watcher = new EventWatcher(test, receiver, 'messageerror');
+
+ // Receive a file from |target|.
+ target.postMessage(
+ { type: 'create-file' }, { targetOrigin: target_origin });
+ const first_error = await error_watcher.wait_for('messageerror');
+ const serialized_first_error = serialize_message_error_event(first_error);
+ assert_equals_serialized_message_error_event(
+ serialized_first_error, expected_origin, expected_has_source);
+
+ // Receive a directory from |target|.
+ target.postMessage(
+ { type: 'create-directory' }, { targetOrigin: target_origin });
+ const second_error = await error_watcher.wait_for('messageerror');
+ const serialized_second_error = serialize_message_error_event(second_error);
+ assert_equals_serialized_message_error_event(
+ serialized_second_error, expected_origin, expected_has_source);
+}
+
+// Performs the send message error test followed by the receive message error
+// test.
+async function do_send_and_receive_message_error_test(
+ test,
+ root_dir,
+ receiver,
+ target,
+ target_origin,
+ // False when the MessageEvent's source is null.
+ expected_has_source,
+ // The origin of MessageEvents received by |target|.
+ expected_origin,
+ // The origin of MessageEvents received by this test runner.
+ expected_remote_origin) {
+ await do_send_message_error_test(
+ test, root_dir, receiver, target, target_origin, expected_has_source,
+ expected_origin);
+ await do_receive_message_error_test(
+ test, receiver, target, target_origin, expected_has_source,
+ expected_remote_origin);
+}
+
+// Runs the same test as do_send_message_error_test(), but uses a MessagePort.
+// This test starts by establishing a message channel between the test runner
+// and |target|.
+async function do_send_message_port_error_test(
+ test, root_dir, target, target_origin) {
+ const message_port = create_message_channel(target, target_origin);
+ await do_send_message_error_test(
+ test, root_dir, /*receiver=*/message_port, /*target=*/message_port,
+ /*target_origin=*/undefined, /*expected_has_source=*/false,
+ /*expected_origin=*/'', /*expected_remote_origin=*/'');
+}
+
+// Runs the same test as do_receive_message_error_test(), but uses a MessagePort.
+async function do_receive_message_port_error_test(
+ test, target, target_origin) {
+ const message_port = create_message_channel(target, target_origin);
+ await do_receive_message_error_test(
+ test, /*receiver=*/message_port, /*target=*/message_port,
+ /*target_origin=*/undefined, /*expected_has_source=*/false,
+ /*expected_origin=*/'');
+}
+
+// Runs the same test as do_send_and_receive_message_error_test(), but uses a
+// MessagePort.
+async function do_send_and_receive_message_port_error_test(
+ test, root_dir, target, target_origin) {
+ await do_send_message_port_error_test(
+ test, root_dir, target, target_origin);
+ await do_receive_message_port_error_test(
+ test, target, target_origin);
+}
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(
+ t, { src: kRemoteOriginDocumentMessageTarget });
+ await do_send_and_receive_message_error_test(
+ t, root_dir, /*receiver=*/self, /*target=*/iframe.contentWindow,
+ /*target_origin=*/'*', /*expected_has_source=*/true,
+ /*expected_origin=*/location.origin,
+ /*expected_remote_origin=*/kRemoteOrigin);
+}, 'Fail to send and receive messages using a cross origin iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(t, { src: kRemoteOriginDocumentMessageTarget });
+ await do_send_and_receive_message_port_error_test(
+ t, root_dir, /*target=*/iframe.contentWindow, /*target_origin=*/'*');
+}, 'Fail to send and receive messages using a cross origin message port in ' +
+'an iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(
+ t, { src: kDocumentMessageTarget, sandbox: 'allow-scripts' });
+
+ await do_send_message_error_test(
+ t, root_dir, /*receiver=*/self, /*target=*/iframe.contentWindow,
+ /*target_origin=*/'*', /*expected_has_source*/true,
+ /*expected_origin=*/location.origin);
+}, 'Fail to send to a sandboxed iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(
+ t, { src: kDocumentMessageTarget, sandbox: 'allow-scripts' });
+ await do_send_message_port_error_test(
+ t, root_dir, /*target=*/iframe.contentWindow, /*target_origin=*/'*');
+}, 'Fail to send messages using a message port to a sandboxed ' +
+'iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe_data_uri = await create_message_target_data_uri(t);
+ const iframe = await add_iframe(t, { src: iframe_data_uri });
+ await do_send_message_error_test(t, root_dir, /*receiver=*/self,
+ /*target=*/iframe.contentWindow, /*target_origin=*/'*',
+ /*expected_has_source*/true, /*expected_origin=*/location.origin);
+ // Do not test receiving FileSystemHandles from the data URI iframe. Data URI
+ // iframes are insecure and do not expose the File System APIs.
+}, 'Fail to send messages to a data URI iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe_data_uri = await create_message_target_data_uri(t);
+ const iframe = await add_iframe(t, { src: iframe_data_uri });
+ await do_send_message_port_error_test(
+ t, root_dir, /*target=*/iframe.contentWindow, /*target_origin=*/'*');
+}, 'Fail to send messages using a message port in a data URI iframe.');
+
+directory_test(async (t, root_dir) => {
+ const child_window = await open_window(t, kRemoteOriginDocumentMessageTarget);
+ await do_send_and_receive_message_error_test(
+ t, root_dir, /*receiver=*/self, /*target=*/child_window, /*target_origin=*/'*',
+ /*expected_has_source=*/true, /*expected_origin=*/location.origin,
+ /*expected_remote_origin=*/kRemoteOrigin);
+}, 'Fail to send and receive messages using a cross origin window.');
+
+directory_test(async (t, root_dir) => {
+ const child_window = await open_window(t, kRemoteOriginDocumentMessageTarget);
+ await do_send_message_port_error_test(
+ t, root_dir, /*target=*/child_window, /*target_origin=*/'*');
+}, 'Fail to send and receive messages using a cross origin message port in ' +
+'a window.');
+
+directory_test(async (t, root_dir) => {
+ const url = `${kDocumentMessageTarget}?pipe=header(Content-Security-Policy` +
+ ', sandbox allow-scripts)';
+ const child_window = await open_window(t, url);
+ await do_send_message_error_test(
+ t, root_dir, /*receiver=*/self, /*target=*/child_window,
+ /*target_origin=*/'*', /*expected_has_source*/true,
+ /*expected_origin=*/location.origin);
+}, 'Fail to send messages to a sandboxed window.');
+
+directory_test(async (t, root_dir) => {
+ const url = `${kDocumentMessageTarget}?pipe=header(Content-Security-Policy` +
+ ', sandbox allow-scripts)';
+ const child_window = await open_window(t, url);
+ await do_send_message_port_error_test(
+ t, root_dir, /*target=*/child_window, /*target_origin=*/'*');
+}, 'Fail to send messages using a message port to a sandboxed ' +
+'window.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-frames.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-frames.js
new file mode 100644
index 0000000000..b70b2992c6
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-frames.js
@@ -0,0 +1,44 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+
+directory_test(
+ async (t, root_dir) => {
+ const iframe = await add_iframe(t, {src: kDocumentMessageTarget});
+ await do_message_port_test(
+ t, root_dir, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+ },
+ 'Send and receive messages using a message port in a same origin ' +
+ 'iframe.');
+
+directory_test(
+ async (t, root_dir) => {
+ const iframe = await add_iframe(t, {
+ src: kDocumentMessageTarget,
+ sandbox: 'allow-scripts allow-same-origin'
+ });
+ await do_message_port_test(
+ t, root_dir, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+ },
+ 'Send and receive messages using a message port in a sandboxed same ' +
+ 'origin iframe.');
+
+directory_test(async (t, root_dir) => {
+ const blob_url = await create_message_target_blob_url(t);
+ const iframe = await add_iframe(t, {src: blob_url});
+ await do_message_port_test(
+ t, root_dir, /*target=*/ iframe.contentWindow, /*target_origin=*/ '*');
+}, 'Send and receive messages using a message port in a blob iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe_html = await create_message_target_html_without_subresources(t);
+ const iframe = await add_iframe(t, {srcdoc: iframe_html});
+ await do_message_port_test(
+ t, root_dir, /*target=*/ iframe.contentWindow, /*target_origin=*/ '*');
+}, 'Send and receive messages using a message port in an iframe srcdoc.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-windows.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-windows.js
new file mode 100644
index 0000000000..dceb250ebb
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-windows.js
@@ -0,0 +1,35 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+
+directory_test(
+ async (t, root_dir) => {
+ const child_window = await open_window(t, kDocumentMessageTarget);
+ await do_message_port_test(
+ t, root_dir, /*target=*/ child_window, /*target_origin=*/ '*');
+ },
+ 'Send and receive messages using a message port in a same origin ' +
+ 'window.');
+
+directory_test(async (t, root_dir) => {
+ const blob_url = await create_message_target_blob_url(t);
+ const child_window = await open_window(t, blob_url);
+ await do_message_port_test(
+ t, root_dir, /*target=*/ child_window, /*target_origin=*/ '*');
+}, 'Send and receive messages using a message port in a blob window.');
+
+directory_test(
+ async (t, root_dir) => {
+ const url =
+ `${kDocumentMessageTarget}?pipe=header(Content-Security-Policy` +
+ ', sandbox allow-scripts allow-same-origin)';
+ const child_window = await open_window(t, url);
+ await do_message_port_test(
+ t, root_dir, /*target=*/ child_window, /*target_origin=*/ '*');
+ },
+ 'Send and receive messages using a message port in a sandboxed same ' +
+ 'origin window.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-workers.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-workers.js
new file mode 100644
index 0000000000..b386527dbd
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-MessagePort-workers.js
@@ -0,0 +1,40 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+directory_test(
+ async (t, root_dir) => {
+ const dedicated_worker =
+ create_dedicated_worker(t, kDedicatedWorkerMessageTarget);
+ await do_message_port_test(t, root_dir, /*target=*/ dedicated_worker);
+ },
+ 'Send and receive messages using a message port in a dedicated ' +
+ 'worker.');
+
+directory_test(
+ async (t, root_dir) => {
+ const scope = `${kServiceWorkerMessageTarget}` +
+ '?post-message-to-message-port-with-file-handle';
+ const registration =
+ await create_service_worker(t, kServiceWorkerMessageTarget, scope);
+ await do_message_port_test(
+ t, root_dir, /*target=*/ registration.installing);
+ },
+ 'Send and receive messages using a message port in a service ' +
+ 'worker.');
+
+if (self.SharedWorker !== undefined) {
+ directory_test(
+ async (t, root_dir) => {
+ const shared_worker = new SharedWorker(kSharedWorkerMessageTarget);
+ shared_worker.port.start();
+ await do_message_port_test(t, root_dir, /*target=*/ shared_worker.port);
+ },
+ 'Send and receive messages using a message port in a shared ' +
+ ' worker.');
+}
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-frames.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-frames.js
new file mode 100644
index 0000000000..1e77b89d77
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-frames.js
@@ -0,0 +1,40 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(t, {src: kDocumentMessageTarget});
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a same origin iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe = await add_iframe(t, {
+ src: kDocumentMessageTarget,
+ sandbox: 'allow-scripts allow-same-origin'
+ });
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a sandboxed same origin iframe.');
+
+directory_test(async (t, root_dir) => {
+ const blob_url = await create_message_target_blob_url(t);
+ const iframe = await add_iframe(t, {src: blob_url});
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a blob iframe.');
+
+directory_test(async (t, root_dir) => {
+ const iframe_html = await create_message_target_html_without_subresources(t);
+ const iframe = await add_iframe(t, {srcdoc: iframe_html});
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ iframe.contentWindow,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using an iframe srcdoc.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-windows.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-windows.js
new file mode 100644
index 0000000000..798d458534
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-windows.js
@@ -0,0 +1,31 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+
+directory_test(async (t, root_dir) => {
+ const child_window = await open_window(t, kDocumentMessageTarget);
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ child_window,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a same origin window.');
+
+directory_test(async (t, root_dir) => {
+ const blob_url = await create_message_target_blob_url(t);
+ const child_window = await open_window(t, blob_url);
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ child_window,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a blob window.');
+
+directory_test(async (t, root_dir) => {
+ const url = `${kDocumentMessageTarget}?pipe=header(Content-Security-Policy` +
+ ', sandbox allow-scripts allow-same-origin)';
+ const child_window = await open_window(t, url);
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ self, /*target=*/ child_window,
+ /*target_origin=*/ '*');
+}, 'Send and receive messages using a sandboxed same origin window.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-workers.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-workers.js
new file mode 100644
index 0000000000..dbd8e5754d
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-postMessage-workers.js
@@ -0,0 +1,35 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/messaging-blob-helpers.js
+// /fs/resources/messaging-serialize-helpers.js
+// /fs/resources/test-helpers.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+directory_test(async (t, root_dir) => {
+ const dedicated_worker =
+ create_dedicated_worker(t, kDedicatedWorkerMessageTarget);
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ dedicated_worker,
+ /*target=*/ dedicated_worker);
+}, 'Send and receive messages using a dedicated worker.');
+
+directory_test(async (t, root_dir) => {
+ const scope = `${kServiceWorkerMessageTarget}?post-message-with-file-handle`;
+ const registration =
+ await create_service_worker(t, kServiceWorkerMessageTarget, scope);
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ navigator.serviceWorker,
+ /*target=*/ registration.installing);
+}, 'Send and receive messages using a service worker.');
+
+if (self.SharedWorker !== undefined) {
+ directory_test(async (t, root_dir) => {
+ const shared_worker = new SharedWorker(kSharedWorkerMessageTarget);
+ shared_worker.port.start();
+ await do_post_message_test(
+ t, root_dir, /*receiver=*/ shared_worker.port,
+ /*target=*/ shared_worker.port);
+ }, 'Send and receive messages using a shared worker.');
+}
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-remove.js b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-remove.js
new file mode 100644
index 0000000000..021576310b
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-remove.js
@@ -0,0 +1,105 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await handle.remove();
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getFileContents(handle));
+}, 'remove() to remove a file');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await handle.remove();
+
+ await promise_rejects_dom(t, 'NotFoundError', handle.remove());
+}, 'remove() on an already removed file should fail');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await dir.remove();
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getSortedDirectoryEntries(dir));
+}, 'remove() to remove an empty directory');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await dir.remove();
+
+ await promise_rejects_dom(t, 'NotFoundError', dir.remove());
+}, 'remove() on an already removed directory should fail');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ t.add_cleanup(() => root.removeEntry('dir-to-remove', {recursive: true}));
+ await createEmptyFile(t, 'file-in-dir', dir);
+
+ await promise_rejects_dom(t, 'InvalidModificationError', dir.remove());
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-to-remove/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir), ['file-in-dir']);
+}, 'remove() on a non-empty directory should fail');
+
+directory_test(async (t, root) => {
+ // root
+ // ├──file-to-keep
+ // ├──dir-to-remove
+ // ├── file0
+ // ├── dir1-in-dir
+ // │   └── file1
+ // └── dir2
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await createEmptyFile(t, 'file0', dir);
+ const dir1_in_dir = await createDirectory(t, 'dir1-in-dir', dir);
+ await createEmptyFile(t, 'file1', dir1_in_dir);
+ await createDirectory(t, 'dir2-in-dir', dir);
+
+ await dir.remove({recursive: true});
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'remove() on a directory recursively should delete all sub-items');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await handle.remove({recursive: true});
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getFileContents(handle));
+}, 'remove() on a file should ignore the recursive option');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+
+ const writable = await cleanup_writable(t, await handle.createWritable());
+ await promise_rejects_dom(t, 'NoModificationAllowedError', handle.remove());
+
+ await writable.close();
+ assert_array_equals(
+ await getSortedDirectoryEntries(root),
+ ['file-to-keep', 'file-to-remove']);
+
+ await handle.remove();
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getFileContents(handle));
+}, 'remove() while the file has an open writable fails');
+
+promise_test(async (t) => {
+ const root = await navigator.storage.getDirectory();
+ await root.getFileHandle('file.txt', {create: true});
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file.txt']);
+
+ await root.remove();
+
+ // Creates a fresh sandboxed file system.
+ const newRoot = await navigator.storage.getDirectory();
+ assert_array_equals(await getSortedDirectoryEntries(newRoot), []);
+}, 'can remove the root of a sandbox file system');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getDirectoryHandle.js b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getDirectoryHandle.js
new file mode 100644
index 0000000000..6a63edecc5
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getDirectoryHandle.js
@@ -0,0 +1,117 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ await promise_rejects_dom(
+ t, 'NotFoundError', root.getDirectoryHandle('non-existing-dir'));
+}, 'getDirectoryHandle(create=false) rejects for non-existing directories');
+
+directory_test(async (t, root) => {
+ const handle =
+ await root.getDirectoryHandle('non-existing-dir', {create: true});
+ t.add_cleanup(() => root.removeEntry('non-existing-dir', {recursive: true}));
+
+ assert_equals(handle.kind, 'directory');
+ assert_equals(handle.name, 'non-existing-dir');
+ assert_equals(await getDirectoryEntryCount(handle), 0);
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['non-existing-dir/']);
+}, 'getDirectoryHandle(create=true) creates an empty directory');
+
+directory_test(async (t, root) => {
+ const existing_handle =
+ await root.getDirectoryHandle('dir-with-contents', {create: true});
+ t.add_cleanup(() => root.removeEntry('dir-with-contents', {recursive: true}));
+ const file_handle = await createEmptyFile(t, 'test-file', existing_handle);
+
+ const handle =
+ await root.getDirectoryHandle('dir-with-contents', {create: false});
+
+ assert_equals(handle.kind, 'directory');
+ assert_equals(handle.name, 'dir-with-contents');
+ assert_array_equals(await getSortedDirectoryEntries(handle), ['test-file']);
+}, 'getDirectoryHandle(create=false) returns existing directories');
+
+directory_test(async (t, root) => {
+ const existing_handle =
+ await root.getDirectoryHandle('dir-with-contents', {create: true});
+ t.add_cleanup(() => root.removeEntry('dir-with-contents', {recursive: true}));
+ const file_handle =
+ await existing_handle.getFileHandle('test-file', {create: true});
+
+ const handle =
+ await root.getDirectoryHandle('dir-with-contents', {create: true});
+
+ assert_equals(handle.kind, 'directory');
+ assert_equals(handle.name, 'dir-with-contents');
+ assert_array_equals(await getSortedDirectoryEntries(handle), ['test-file']);
+}, 'getDirectoryHandle(create=true) returns existing directories without erasing');
+
+directory_test(async (t, root) => {
+ await createEmptyFile(t, 'file-name', root);
+
+ await promise_rejects_dom(
+ t, 'TypeMismatchError', root.getDirectoryHandle('file-name'));
+ await promise_rejects_dom(
+ t, 'TypeMismatchError',
+ root.getDirectoryHandle('file-name', {create: false}));
+ await promise_rejects_dom(
+ t, 'TypeMismatchError',
+ root.getDirectoryHandle('file-name', {create: true}));
+}, 'getDirectoryHandle() when a file already exists with the same name');
+
+directory_test(async (t, dir) => {
+ await promise_rejects_js(
+ t, TypeError, dir.getDirectoryHandle('', {create: true}));
+ await promise_rejects_js(
+ t, TypeError, dir.getDirectoryHandle('', {create: false}));
+}, 'getDirectoryHandle() with empty name');
+
+directory_test(async (t, dir) => {
+ await promise_rejects_js(
+ t, TypeError, dir.getDirectoryHandle(kCurrentDirectory));
+ await promise_rejects_js(
+ t, TypeError, dir.getDirectoryHandle(kCurrentDirectory, {create: true}));
+}, `getDirectoryHandle() with "${kCurrentDirectory}" name`);
+
+directory_test(async (t, dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', /*parent=*/ dir);
+
+ await promise_rejects_js(
+ t, TypeError, subdir.getDirectoryHandle(kParentDirectory));
+ await promise_rejects_js(
+ t, TypeError,
+ subdir.getDirectoryHandle(kParentDirectory, {create: true}));
+}, `getDirectoryHandle() with "${kParentDirectory}" name`);
+
+directory_test(async (t, dir) => {
+ const first_subdir_name = 'first-subdir-name';
+ const first_subdir =
+ await createDirectory(t, first_subdir_name, /*parent=*/ dir);
+
+ const second_subdir_name = 'second-subdir-name';
+ const second_subdir =
+ await createDirectory(t, second_subdir_name, /*parent=*/ first_subdir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator =
+ `${first_subdir_name}${kPathSeparators[i]}${second_subdir_name}`;
+ await promise_rejects_js(
+ t, TypeError, dir.getDirectoryHandle(path_with_separator),
+ `getDirectoryHandle() must reject names containing "${
+ kPathSeparators[i]}"`);
+ }
+}, 'getDirectoryHandle(create=false) with a path separator when the directory exists');
+
+directory_test(async (t, dir) => {
+ const subdir_name = 'subdir-name';
+ const subdir = await createDirectory(t, subdir_name, /*parent=*/ dir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator = `${subdir_name}${kPathSeparators[i]}file_name`;
+ await promise_rejects_js(
+ t, TypeError,
+ dir.getDirectoryHandle(path_with_separator, {create: true}),
+ `getDirectoryHandle(true) must reject names containing "${
+ kPathSeparators[i]}"`);
+ }
+}, 'getDirectoryHandle(create=true) with a path separator');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getFileHandle.js b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getFileHandle.js
new file mode 100644
index 0000000000..840e85b436
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-getFileHandle.js
@@ -0,0 +1,145 @@
+'use strict';
+
+directory_test(async (t, dir) => {
+ await promise_rejects_dom(
+ t, 'NotFoundError', dir.getFileHandle('non-existing-file'));
+}, 'getFileHandle(create=false) rejects for non-existing files');
+
+directory_test(async (t, dir) => {
+ const handle = await dir.getFileHandle('non-existing-file', {create: true});
+ t.add_cleanup(() => dir.removeEntry('non-existing-file'));
+
+ assert_equals(handle.kind, 'file');
+ assert_equals(handle.name, 'non-existing-file');
+ assert_equals(await getFileSize(handle), 0);
+ assert_equals(await getFileContents(handle), '');
+}, 'getFileHandle(create=true) creates an empty file for non-existing files');
+
+directory_test(async (t, dir) => {
+ var name = '';
+ // test the ascii characters -- start after the non-character ASCII values, exclude DEL
+ for (let i = 32; i < 127; i++) {
+ // Path separators are disallowed
+ let disallow = false;
+ for (let j = 0; j < kPathSeparators.length; ++j) {
+ if (String.fromCharCode(i) == kPathSeparators[j]) {
+ disallow = true;
+ }
+ }
+ if (!disallow) {
+ name += String.fromCharCode(i);
+ }
+ }
+ // Add in CR, LF, FF, Tab, Vertical Tab
+ for (let i = 9; i < 14; i++) {
+ name += String.fromCharCode(i);
+ }
+ const handle = await dir.getFileHandle(name, {create: true});
+ t.add_cleanup(() => dir.removeEntry(name));
+
+ assert_equals(handle.kind, 'file');
+ assert_equals(handle.name, name);
+ assert_equals(await getFileSize(handle), 0);
+ assert_equals(await getFileContents(handle), '');
+}, 'getFileHandle(create=true) creates an empty file with all valid ASCII characters in the name');
+
+directory_test(async (t, dir) => {
+ var name;
+ // A non-ASCII name
+ name = 'Funny cat \u{1F639}'
+ const handle = await dir.getFileHandle(name, {create: true});
+ t.add_cleanup(() => dir.removeEntry(name));
+
+ assert_equals(handle.kind, 'file');
+ assert_equals(handle.name, name);
+ assert_equals(await getFileSize(handle), 0);
+ assert_equals(await getFileContents(handle), '');
+}, 'getFileHandle(create=true) creates an empty file with non-ASCII characters in the name');
+
+directory_test(async (t, dir) => {
+ const existing_handle = await createFileWithContents(
+ t, 'existing-file', '1234567890', /*parent=*/ dir);
+ const handle = await dir.getFileHandle('existing-file');
+
+ assert_equals(handle.kind, 'file');
+ assert_equals(handle.name, 'existing-file');
+ assert_equals(await getFileSize(handle), 10);
+ assert_equals(await getFileContents(handle), '1234567890');
+}, 'getFileHandle(create=false) returns existing files');
+
+directory_test(async (t, dir) => {
+ const existing_handle = await createFileWithContents(
+ t, 'file-with-contents', '1234567890', /*parent=*/ dir);
+ const handle = await dir.getFileHandle('file-with-contents', {create: true});
+
+ assert_equals(handle.kind, 'file');
+ assert_equals(handle.name, 'file-with-contents');
+ assert_equals(await getFileSize(handle), 10);
+ assert_equals(await getFileContents(handle), '1234567890');
+}, 'getFileHandle(create=true) returns existing files without erasing');
+
+directory_test(async (t, dir) => {
+ const dir_handle = await dir.getDirectoryHandle('dir-name', {create: true});
+ t.add_cleanup(() => dir.removeEntry('dir-name', {recursive: true}));
+
+ await promise_rejects_dom(
+ t, 'TypeMismatchError', dir.getFileHandle('dir-name'));
+}, 'getFileHandle(create=false) when a directory already exists with the same name');
+
+directory_test(async (t, dir) => {
+ const dir_handle = await dir.getDirectoryHandle('dir-name', {create: true});
+ t.add_cleanup(() => dir.removeEntry('dir-name', {recursive: true}));
+
+ await promise_rejects_dom(
+ t, 'TypeMismatchError', dir.getFileHandle('dir-name', {create: true}));
+}, 'getFileHandle(create=true) when a directory already exists with the same name');
+
+directory_test(async (t, dir) => {
+ await promise_rejects_js(t, TypeError, dir.getFileHandle('', {create: true}));
+ await promise_rejects_js(
+ t, TypeError, dir.getFileHandle('', {create: false}));
+}, 'getFileHandle() with empty name');
+
+directory_test(async (t, dir) => {
+ await promise_rejects_js(t, TypeError, dir.getFileHandle(kCurrentDirectory));
+ await promise_rejects_js(
+ t, TypeError, dir.getFileHandle(kCurrentDirectory, {create: true}));
+}, `getFileHandle() with "${kCurrentDirectory}" name`);
+
+directory_test(async (t, dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', /*parent=*/ dir);
+
+ await promise_rejects_js(
+ t, TypeError, subdir.getFileHandle(kParentDirectory));
+ await promise_rejects_js(
+ t, TypeError, subdir.getFileHandle(kParentDirectory, {create: true}));
+}, `getFileHandle() with "${kParentDirectory}" name`);
+
+directory_test(async (t, dir) => {
+ const subdir_name = 'subdir-name';
+ const subdir = await createDirectory(t, subdir_name, /*parent=*/ dir);
+
+ const file_name = 'file-name';
+ await createEmptyFile(t, file_name, /*parent=*/ subdir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator =
+ `${subdir_name}${kPathSeparators[i]}${file_name}`;
+ await promise_rejects_js(
+ t, TypeError, dir.getFileHandle(path_with_separator),
+ `getFileHandle() must reject names containing "${kPathSeparators[i]}"`);
+ }
+}, 'getFileHandle(create=false) with a path separator when the file exists.');
+
+directory_test(async (t, dir) => {
+ const subdir_name = 'subdir-name';
+ const subdir = await createDirectory(t, subdir_name, /*parent=*/ dir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator = `${subdir_name}${kPathSeparators[i]}file_name`;
+ await promise_rejects_js(
+ t, TypeError, dir.getFileHandle(path_with_separator, {create: true}),
+ `getFileHandle(create=true) must reject names containing "${
+ kPathSeparators[i]}"`);
+ }
+}, 'getFileHandle(create=true) with a path separator');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-iteration.js b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-iteration.js
new file mode 100644
index 0000000000..815ae21936
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-iteration.js
@@ -0,0 +1,100 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ const file_name2 = 'foo2.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+ await createFileWithContents(t, file_name2, 'contents', /*parent=*/ root);
+
+ for await (let entry of root) {
+ break;
+ }
+
+}, 'returning early from an iteration doesn\'t crash');
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ const file_name2 = 'foo2.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+ await createFileWithContents(t, file_name2, 'contents', /*parent=*/ root);
+
+ let names = [];
+ for await (let entry of root) {
+ assert_true(Array.isArray(entry));
+ assert_equals(entry.length, 2);
+ assert_equals(typeof entry[0], 'string');
+ assert_true(entry[1] instanceof FileSystemFileHandle);
+ assert_equals(entry[0], entry[1].name);
+ names.push(entry[0]);
+ }
+ names.sort();
+ assert_array_equals(names, [file_name1, file_name2]);
+
+}, '@@asyncIterator: full iteration works');
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ const file_name2 = 'foo2.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+ await createFileWithContents(t, file_name2, 'contents', /*parent=*/ root);
+
+ let names = [];
+ for await (let entry of root.entries()) {
+ assert_true(Array.isArray(entry));
+ assert_equals(entry.length, 2);
+ assert_equals(typeof entry[0], 'string');
+ assert_true(entry[1] instanceof FileSystemFileHandle);
+ assert_equals(entry[0], entry[1].name);
+ names.push(entry[0]);
+ }
+ names.sort();
+ assert_array_equals(names, [file_name1, file_name2]);
+}, 'entries: full iteration works');
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ const file_name2 = 'foo2.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+ await createFileWithContents(t, file_name2, 'contents', /*parent=*/ root);
+
+ let names = [];
+ for await (let entry of root.values()) {
+ assert_true(entry instanceof FileSystemFileHandle);
+ names.push(entry.name);
+ }
+ names.sort();
+ assert_array_equals(names, [file_name1, file_name2]);
+}, 'values: full iteration works');
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ const file_name2 = 'foo2.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+ await createFileWithContents(t, file_name2, 'contents', /*parent=*/ root);
+
+ let names = [];
+ for await (let entry of root.keys()) {
+ assert_equals(typeof entry, 'string');
+ names.push(entry);
+ }
+ names.sort();
+ assert_array_equals(names, [file_name1, file_name2]);
+}, 'keys: full iteration works');
+
+directory_test(async (t, root) => {
+ const file_name1 = 'foo1.txt';
+ await createFileWithContents(t, file_name1, 'contents', /*parent=*/ root);
+
+ const next = (() => {
+ const iterator = root.entries();
+ return iterator.next();
+ })();
+ garbageCollect();
+ let entry = await next;
+ assert_false(entry.done);
+ assert_true(Array.isArray(entry.value));
+ assert_equals(entry.value.length, 2);
+ assert_equals(entry.value[0], file_name1);
+ assert_true(entry.value[1] instanceof FileSystemFileHandle);
+ assert_equals(entry.value[1].name, file_name1);
+}, 'iteration while iterator gets garbage collected');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-removeEntry.js b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-removeEntry.js
new file mode 100644
index 0000000000..07d26e9011
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-removeEntry.js
@@ -0,0 +1,222 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await root.removeEntry('file-to-remove');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getFileContents(handle));
+}, 'removeEntry() to remove a file');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await root.removeEntry('file-to-remove');
+
+ await promise_rejects_dom(
+ t, 'NotFoundError', root.removeEntry('file-to-remove'));
+}, 'removeEntry() on an already removed file should fail');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await root.removeEntry('dir-to-remove');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'removeEntry() to remove an empty directory');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir-to-remove', root);
+ await createFileWithContents(t, 'file-in-dir', 'abc', dir);
+
+ await promise_rejects_dom(
+ t, 'InvalidModificationError', root.removeEntry('dir-to-remove'));
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-to-remove/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir), ['file-in-dir']);
+}, 'removeEntry() on a non-empty directory should fail');
+
+directory_test(async (t, root) => {
+ // root
+ // ├──file-to-keep
+ // ├──dir-to-remove
+ // ├── file0
+ // ├── dir1-in-dir
+ // │   └── file1
+ // └── dir2
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await createEmptyFile(t, 'file0', dir);
+ const dir1_in_dir = await createDirectory(t, 'dir1-in-dir', dir);
+ await createEmptyFile(t, 'file1', dir1_in_dir);
+ await createDirectory(t, 'dir2-in-dir', dir);
+
+ await root.removeEntry('dir-to-remove', {recursive: true});
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'removeEntry() on a directory recursively should delete all sub-items');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(''));
+}, 'removeEntry() with empty name should fail');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(kCurrentDirectory));
+}, `removeEntry() with "${kCurrentDirectory}" name should fail`);
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(kParentDirectory));
+}, `removeEntry() with "${kParentDirectory}" name should fail`);
+
+directory_test(async (t, root) => {
+ const dir_name = 'dir-name';
+ const dir = await createDirectory(t, dir_name, root);
+
+ const file_name = 'file-name';
+ await createEmptyFile(t, file_name, dir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator = `${dir_name}${kPathSeparators[i]}${file_name}`;
+ await promise_rejects_js(
+ t, TypeError, root.removeEntry(path_with_separator),
+ `removeEntry() must reject names containing "${kPathSeparators[i]}"`);
+ }
+}, 'removeEntry() with a path separator should fail.');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await root.removeEntry('file-to-remove');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+ await promise_rejects_dom(t, 'NotFoundError', getFileContents(handle));
+}, 'removeEntry() to remove a file');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await root.removeEntry('file-to-remove');
+
+ await promise_rejects_dom(t, 'NotFoundError', root.removeEntry('file-to-remove'));
+}, 'removeEntry() on an already removed file should fail');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await root.removeEntry('dir-to-remove');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'removeEntry() to remove an empty directory');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ t.add_cleanup(() => root.removeEntry('dir-to-remove', {recursive: true}));
+ await createEmptyFile(t, 'file-in-dir', dir);
+
+ await promise_rejects_dom(
+ t, 'InvalidModificationError', root.removeEntry('dir-to-remove'));
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-to-remove/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir), ['file-in-dir']);
+}, 'removeEntry() on a non-empty directory should fail');
+
+directory_test(async (t, root) => {
+ // root
+ // ├──file-to-keep
+ // ├──dir-to-remove
+ // ├── file0
+ // ├── dir1-in-dir
+ // │   └── file1
+ // └── dir2
+ const dir = await root.getDirectoryHandle('dir-to-remove', {create: true});
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+ await createEmptyFile(t, 'file0', dir);
+ const dir1_in_dir = await createDirectory(t, 'dir1-in-dir', dir);
+ await createEmptyFile(t, 'file1', dir1_in_dir);
+ await createDirectory(t, 'dir2-in-dir', dir);
+
+ await root.removeEntry('dir-to-remove', {recursive: true});
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'removeEntry() on a directory recursively should delete all sub-items');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(''));
+}, 'removeEntry() with empty name should fail');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(kCurrentDirectory));
+}, `removeEntry() with "${kCurrentDirectory}" name should fail`);
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'dir', root);
+ await promise_rejects_js(t, TypeError, dir.removeEntry(kParentDirectory));
+}, `removeEntry() with "${kParentDirectory}" name should fail`);
+
+directory_test(async (t, root) => {
+ const dir_name = 'dir-name';
+ const dir = await createDirectory(t, dir_name, root);
+
+ const file_name = 'file-name';
+ await createEmptyFile(t, file_name, dir);
+
+ for (let i = 0; i < kPathSeparators.length; ++i) {
+ const path_with_separator = `${dir_name}${kPathSeparators[i]}${file_name}`;
+ await promise_rejects_js(
+ t, TypeError, root.removeEntry(path_with_separator),
+ `removeEntry() must reject names containing "${kPathSeparators[i]}"`);
+ }
+}, 'removeEntry() with a path separator should fail.');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await createFileWithContents(t, 'file-to-keep', 'abc', root);
+
+ const writable = await cleanup_writable(t, await handle.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', root.removeEntry('file-to-remove'));
+
+ await writable.close();
+ await root.removeEntry('file-to-remove');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-to-keep']);
+}, 'removeEntry() while the file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const dir_name = 'dir-name';
+ const dir = await createDirectory(t, dir_name, root);
+
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', dir);
+ await createFileWithContents(t, 'file-to-keep', 'abc', dir);
+
+ const writable = await cleanup_writable(t, await handle.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', root.removeEntry(dir_name));
+
+ await writable.close();
+ assert_array_equals(
+ await getSortedDirectoryEntries(dir), ['file-to-keep', 'file-to-remove']);
+
+ await dir.removeEntry('file-to-remove');
+ assert_array_equals(await getSortedDirectoryEntries(dir), ['file-to-keep']);
+}, 'removeEntry() of a directory while a containing file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-remove', '12345', root);
+ await root.removeEntry('file-to-remove');
+
+ await promise_rejects_dom(t, 'NotFoundError', cleanup_writable(t, handle.createWritable({keepExistingData: true})));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root),
+ []);
+}, 'createWritable after removeEntry succeeds but doesnt recreate the file');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-resolve.js b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-resolve.js
new file mode 100644
index 0000000000..a8900f97e5
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemDirectoryHandle-resolve.js
@@ -0,0 +1,27 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ assert_array_equals(await root_dir.resolve(root_dir), []);
+}, 'Resolve returns empty array for same directory');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const file = await createEmptyFile(t, 'file-name', subdir);
+
+ assert_array_equals(await root_dir.resolve(file), ['subdir-name', 'file-name']);
+}, 'Resolve returns correct path');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir😊', root_dir);
+ const file = await createEmptyFile(t, 'file-name', subdir);
+
+ assert_array_equals(await root_dir.resolve(file), ['subdir😊', 'file-name']);
+ assert_array_equals(await root_dir.resolve(subdir), ['subdir😊']);
+}, 'Resolve returns correct path with non-ascii characters');
+
+directory_test(async (t, root_dir) => {
+ const subdir = await createDirectory(t, 'subdir-name', root_dir);
+ const file = await createEmptyFile(t, 'file-name', root_dir);
+
+ assert_equals(await subdir.resolve(file), null);
+}, 'Resolve returns null when entry is not a child');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-create-sync-access-handle.js b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-create-sync-access-handle.js
new file mode 100644
index 0000000000..302d4a5a7a
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-create-sync-access-handle.js
@@ -0,0 +1,26 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /fs/resources/test-helpers.js
+
+directory_test(async (t, root_dir) => {
+ assert_true(
+ file_system_type == 'sandboxed' || file_system_type == 'local',
+ 'File system type should be sandboxed or local.');
+ const expect_success = file_system_type == 'sandboxed';
+
+ const dedicated_worker =
+ create_dedicated_worker(t, kDedicatedWorkerMessageTarget);
+ const file_handle =
+ await root_dir.getFileHandle('sync-access-handle-file', {create: true});
+
+ dedicated_worker.postMessage(
+ {type: 'create-sync-access-handle', file_handle});
+
+ const event_watcher = new EventWatcher(t, dedicated_worker, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_equals(response.success, expect_success);
+}, 'Attempt to create a sync access handle.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js
new file mode 100644
index 0000000000..b7ceb5ee7a
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js
@@ -0,0 +1,42 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const fileContents = 'awesome content';
+ let handle = await createFileWithContents(t, 'foo.txt', fileContents, /*parent=*/ root);
+ let file = await handle.getFile();
+ let slice = file.slice(1, file.size);
+ let actualContents = await slice.text();
+ assert_equals(actualContents, fileContents.slice(1, fileContents.length));
+}, 'getFile() provides a file that can be sliced');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'mtime.txt', root);
+ let file = await handle.getFile();
+ const first_mtime = file.lastModified;
+
+ // We wait for 2s here to ensure that the files do not have the
+ // same modification time. Some filesystems have low resolutions
+ // for modification timestamps.
+ let timeout = new Promise(resolve => {
+ t.step_timeout(resolve, 2000);
+ });
+ await timeout;
+
+ const writer = await cleanup_writable(t, await handle.createWritable({keepExistingData: false}));
+ await writer.write(new Blob(['foo']));
+ await writer.close();
+
+ file = await handle.getFile();
+ const second_mtime = file.lastModified;
+
+ // We wait for 5 ms here to ensure that `lastModified`
+ // from the File objects is stable between getFile invocations.
+ timeout = new Promise(resolve => {
+ t.step_timeout(resolve, 5);
+ });
+ await timeout;
+ let fileReplica = await handle.getFile();
+ assert_equals(second_mtime, fileReplica.lastModified);
+
+ assert_less_than(first_mtime, second_mtime);
+}, 'getFile() returns last modified time');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js
new file mode 100644
index 0000000000..dd848626e4
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js
@@ -0,0 +1,369 @@
+// META: script=resources/test-helpers.js
+
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await handle.move('file-after');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-after']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(name) to rename a file');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await handle.move('file-after');
+ const newhandle = await root.getFileHandle('file-after');
+ assert_equals(await getFileContents(newhandle), 'foo');
+ assert_equals(await getFileSize(newhandle), 3);
+}, 'get a handle to a moved file');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await handle.move('file-before');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(name) to rename a file the same name');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await promise_rejects_js(t, TypeError, handle.move(''));
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move("") to rename a file fails');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-1', 'foo', root);
+
+ await handle.move('file-2');
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-2']);
+
+ await handle.move('file-3');
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-3']);
+
+ await handle.move('file-1');
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-1']);
+}, 'move(name) can be called multiple times');
+
+directory_test(async (t, root) => {
+ const dir = await root.getDirectoryHandle('dir', {create: true});
+ const handle = await createFileWithContents(t, 'file-before', 'foo', dir);
+ await promise_rejects_js(t, TypeError, handle.move('Lorem.'));
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['dir/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(name) with a name with a trailing period should fail');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await promise_rejects_js(t, TypeError, handle.move('test/test'));
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(name) with a name with invalid characters should fail');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'abc', root);
+
+ // Cannot rename handle with an active writable.
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', handle.move('file-after'));
+
+ // Can move handle once the writable is closed.
+ await stream.close();
+ await handle.move('file-after');
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-after']);
+}, 'move(name) while the file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'abc', root);
+ const handle_dest =
+ await createFileWithContents(t, 'file-after', '123', root);
+
+ // Cannot overwrite a handle with an active writable.
+ const stream = await cleanup_writable(t, await handle_dest.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', handle.move('file-after'));
+
+ await stream.close();
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['file-after', 'file-before']);
+}, 'move(name) while the destination file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'abc', root);
+ const handle_dest =
+ await createFileWithContents(t, 'file-after', '123', root);
+
+ await handle.move('file-after');
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-after']);
+ assert_equals(await getFileContents(handle), 'abc');
+ assert_equals(await getFileContents(handle_dest), 'abc');
+}, 'move(name) can overwrite an existing file');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await handle.move(root, 'file-after');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-after']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(dir, name) to rename a file');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await handle.move(root, 'file-before');
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(dir, name) to rename a file the same name');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file', 'abc', dir_src);
+ await file.move(dir_dest);
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']);
+ assert_equals(await getFileContents(file), 'abc');
+ assert_equals(await getFileSize(file), 3);
+}, 'move(dir) to move a file to a new directory');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file', 'abc', dir_src);
+ await promise_rejects_js(t, TypeError, file.move(dir_dest, ''));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), ['file']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), []);
+ assert_equals(await getFileContents(file), 'abc');
+ assert_equals(await getFileSize(file), 3);
+}, 'move(dir, "") to move a file to a new directory fails');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file =
+ await createFileWithContents(t, 'file-in-dir-src', 'abc', dir_src);
+ await file.move(dir_dest, 'file-in-dir-dest');
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(
+ await getSortedDirectoryEntries(dir_dest), ['file-in-dir-dest']);
+ assert_equals(await getFileContents(file), 'abc');
+ assert_equals(await getFileSize(file), 3);
+}, 'move(dir, name) to move a file to a new directory');
+
+directory_test(async (t, root) => {
+ const dir1 = await root.getDirectoryHandle('dir1', {create: true});
+ const dir2 = await root.getDirectoryHandle('dir2', {create: true});
+ const handle = await createFileWithContents(t, 'file', 'foo', root);
+
+ await handle.move(dir1);
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), ['file']);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), []);
+ assert_equals(await getFileContents(handle), 'foo');
+
+ await handle.move(dir2);
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), ['file']);
+ assert_equals(await getFileContents(handle), 'foo');
+
+ await handle.move(root);
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/', 'file']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), []);
+ assert_equals(await getFileContents(handle), 'foo');
+}, 'move(dir) can be called multiple times');
+
+directory_test(async (t, root) => {
+ const dir1 = await root.getDirectoryHandle('dir1', {create: true});
+ const dir2 = await root.getDirectoryHandle('dir2', {create: true});
+ const handle = await createFileWithContents(t, 'file', 'foo', root);
+
+ await handle.move(dir1, 'file-1');
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), ['file-1']);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), []);
+ assert_equals(await getFileContents(handle), 'foo');
+
+ await handle.move(dir2, 'file-2');
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), ['file-2']);
+ assert_equals(await getFileContents(handle), 'foo');
+
+ await handle.move(root, 'file-3');
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir1/', 'dir2/', 'file-3']);
+ assert_array_equals(await getSortedDirectoryEntries(dir1), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir2), []);
+ assert_equals(await getFileContents(handle), 'foo');
+}, 'move(dir, name) can be called multiple times');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await promise_rejects_js(t, TypeError, handle.move(root, '..'));
+
+ assert_array_equals(await getSortedDirectoryEntries(root), ['file-before']);
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'move(dir, name) with a name with invalid characters should fail');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file', 'abc', dir_src);
+
+ // Cannot move handle with an active writable.
+ const stream = await cleanup_writable(t, await file.createWritable());
+ await promise_rejects_dom(t, 'NoModificationAllowedError', file.move(dir_dest));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ // Assert the file hasn't been moved to the destination directory.
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), []);
+
+ // Can move handle once the writable is closed.
+ await stream.close();
+ await file.move(dir_dest);
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']);
+}, 'move(dir) while the file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file-before', 'abc', dir_src);
+
+ // Cannot move handle with an active writable.
+ const stream = await cleanup_writable(t, await file.createWritable());
+ await promise_rejects_dom(t, 'NoModificationAllowedError', file.move(dir_dest));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ // Assert the file hasn't been moved to the destination directory.
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), []);
+
+ // Can move handle once the writable is closed.
+ await stream.close();
+ await file.move(dir_dest, 'file-after');
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(
+ await getSortedDirectoryEntries(dir_dest), ['file-after']);
+}, 'move(dir, name) while the file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file', 'abc', dir_src);
+ const file_dest = await createFileWithContents(t, 'file', '123', dir_dest);
+
+ // Cannot overwrite handle with an active writable.
+ const stream = await cleanup_writable(t, await file_dest.createWritable());
+ await promise_rejects_dom(t, 'NoModificationAllowedError', file.move(dir_dest));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ // Assert the file is still in the source directory.
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), ['file']);
+
+ await stream.close();
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']);
+}, 'move(dir) while the destination file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file', 'abc', dir_src);
+ const file_dest = await createFileWithContents(t, 'file', '123', dir_dest);
+
+ await file.move(dir_dest);
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file']);
+ assert_equals(await getFileContents(file), 'abc');
+ assert_equals(await getFileContents(file_dest), 'abc');
+}, 'move(dir) can overwrite an existing file');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file-src', 'abc', dir_src);
+ const file_dest =
+ await createFileWithContents(t, 'file-dest', '123', dir_dest);
+
+ // Cannot overwrite handle with an active writable.
+ const stream = await cleanup_writable(t, await file_dest.createWritable());
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', file.move(dir_dest, 'file-dest'));
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root), ['dir-dest/', 'dir-src/']);
+ // Assert the file is still in the source directory.
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), ['file-src']);
+
+ await stream.close();
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file-dest']);
+}, 'move(dir, name) while the destination file has an open writable fails');
+
+directory_test(async (t, root) => {
+ const dir_src = await root.getDirectoryHandle('dir-src', {create: true});
+ const dir_dest = await root.getDirectoryHandle('dir-dest', {create: true});
+ const file = await createFileWithContents(t, 'file-src', 'abc', dir_src);
+ const file_dest =
+ await createFileWithContents(t, 'file-dest', '123', dir_dest);
+
+ await file.move(dir_dest, 'file-dest');
+
+ // Assert the file has been moved to the destination directory and renamed.
+ assert_array_equals(await getSortedDirectoryEntries(dir_src), []);
+ assert_array_equals(await getSortedDirectoryEntries(dir_dest), ['file-dest']);
+ assert_equals(await getFileContents(file), 'abc');
+ assert_equals(await getFileContents(file_dest), 'abc');
+}, 'move(dir, name) can overwrite an existing file');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'file-to-move', '12345', root);
+ const handle2 = handle;
+
+ await handle.move('file-was-moved');
+
+ assert_equals(await getFileContents(handle), '12345');
+ assert_equals(await getFileSize(handle), 5);
+ assert_equals(await getFileContents(handle2), '12345');
+ assert_equals(await getFileSize(handle2), 5);
+
+ assert_array_equals(
+ await getSortedDirectoryEntries(root),
+ ['file-was-moved']);
+}, 'FileSystemFileHandles are references, not paths');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemSyncAccessHandle-flush.js b/testing/web-platform/tests/fs/script-tests/FileSystemSyncAccessHandle-flush.js
new file mode 100644
index 0000000000..580da69cc9
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemSyncAccessHandle-flush.js
@@ -0,0 +1,8 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+
+sync_access_handle_test(async handle => {
+ await handle.flush();
+}, 'Test flush on an empty file.');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-piped.js b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-piped.js
new file mode 100644
index 0000000000..79e4cfb4f2
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-piped.js
@@ -0,0 +1,137 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_string.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue('foo_string');
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'foo_string');
+ assert_equals(await getFileSize(handle), 10);
+}, 'can be piped to with a string');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_arraybuf.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+ const buf = new ArrayBuffer(3);
+ const intView = new Uint8Array(buf);
+ intView[0] = 0x66;
+ intView[1] = 0x6f;
+ intView[2] = 0x6f;
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue(buf);
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'can be piped to with an ArrayBuffer');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_blob.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue(new Blob(['foo']));
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'can be piped to with a Blob');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_write_param.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue({type: 'write', data: 'foobar'});
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'foobar');
+ assert_equals(await getFileSize(handle), 6);
+}, 'can be piped to with a param object with write command');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_write_param.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue({type: 'write', data: 'foobar'});
+ controller.enqueue({type: 'truncate', size: 10});
+ controller.enqueue({type: 'write', position: 0, data: 'baz'});
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'bazbar\0\0\0\0');
+ assert_equals(await getFileSize(handle), 10);
+}, 'can be piped to with a param object with multiple commands');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'foo_write_queued.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const rs = recordingReadableStream({
+ start(controller) {
+ controller.enqueue('foo');
+ controller.enqueue('bar');
+ controller.enqueue('baz');
+ controller.close();
+ }
+ });
+
+ await rs.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'foobarbaz');
+ assert_equals(await getFileSize(handle), 9);
+}, 'multiple operations can be queued');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'fetched.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const response = await fetch('data:text/plain,fetched from far');
+ const body = await response.body;
+ await body.pipeTo(wfs, { preventCancel: true });
+ assert_equals(await getFileContents(handle), 'fetched from far');
+ assert_equals(await getFileSize(handle), 16);
+}, 'plays well with fetch');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'aborted should_be_empty.txt', root);
+ const wfs = await cleanup_writable(t, await handle.createWritable());
+
+ const response = await fetch('data:text/plain,fetched from far');
+ const body = await response.body;
+
+ const abortController = new AbortController();
+ const signal = abortController.signal;
+
+ const promise = body.pipeTo(wfs, { signal });
+ await abortController.abort();
+
+ await promise_rejects_dom(t, 'AbortError', promise, 'stream is aborted');
+ await promise_rejects_js(t, TypeError, wfs.close(), 'stream cannot be closed to flush writes');
+
+ assert_equals(await getFileContents(handle), '');
+ assert_equals(await getFileSize(handle), 0);
+}, 'abort() aborts write');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-write.js b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-write.js
new file mode 100644
index 0000000000..66d6a944a5
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-write.js
@@ -0,0 +1,334 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'empty_blob', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write(new Blob([]));
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '');
+ assert_equals(await getFileSize(handle), 0);
+}, 'write() with an empty blob to an empty file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'valid_blob', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write(new Blob(['1234567890']));
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() a blob to an empty file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'write_param_empty', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write({type: 'write', data: '1234567890'});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() with WriteParams without position to an empty file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'string_zero_offset', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write({type: 'write', position: 0, data: '1234567890'});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() a string to an empty file with zero offset');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'blob_zero_offset', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write({type: 'write', position: 0, data: new Blob(['1234567890'])});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() a blob to an empty file with zero offset');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'write_appends', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('12345');
+ await stream.write('67890');
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() called consecutively appends');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'write_appends_object_string', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('12345');
+ await stream.write({type: 'write', data: '67890'});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() WriteParams without position and string appends');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'write_appends_object_blob', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('12345');
+ await stream.write({type: 'write', data: new Blob(['67890'])});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234567890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() WriteParams without position and blob appends');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'string_with_offset', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('1234567890');
+ await stream.write({type: 'write', position: 4, data: 'abc'});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '1234abc890');
+ assert_equals(await getFileSize(handle), 10);
+}, 'write() called with a string and a valid offset');
+
+directory_test(async (t, root) => {
+const handle = await createEmptyFile(t, 'blob_with_offset', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+await stream.write('1234567890');
+await stream.write({type: 'write', position: 4, data: new Blob(['abc'])});
+await stream.close();
+
+assert_equals(await getFileContents(handle), '1234abc890');
+assert_equals(await getFileSize(handle), 10);
+}, 'write() called with a blob and a valid offset');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'bad_offset', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write({type: 'write', position: 4, data: new Blob(['abc'])});
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '\0\0\0\0abc');
+ assert_equals(await getFileSize(handle), 7);
+}, 'write() called with an offset beyond the end of the file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'empty_string', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('');
+ await stream.close();
+ assert_equals(await getFileContents(handle), '');
+ assert_equals(await getFileSize(handle), 0);
+}, 'write() with an empty string to an empty file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'valid_utf8_string', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('foo🤘');
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo🤘');
+ assert_equals(await getFileSize(handle), 7);
+}, 'write() with a valid utf-8 string');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'string_with_unix_line_ending', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('foo\n');
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo\n');
+ assert_equals(await getFileSize(handle), 4);
+}, 'write() with a string with unix line ending preserved');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createEmptyFile(t, 'string_with_windows_line_ending', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('foo\r\n');
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo\r\n');
+ assert_equals(await getFileSize(handle), 5);
+}, 'write() with a string with windows line ending preserved');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'empty_array_buffer', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ const buf = new ArrayBuffer(0);
+ await stream.write(buf);
+ await stream.close();
+ assert_equals(await getFileContents(handle), '');
+ assert_equals(await getFileSize(handle), 0);
+}, 'write() with an empty array buffer to an empty file');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createEmptyFile(t, 'valid_string_typed_byte_array', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ const buf = new ArrayBuffer(3);
+ const intView = new Uint8Array(buf);
+ intView[0] = 0x66;
+ intView[1] = 0x6f;
+ intView[2] = 0x6f;
+ await stream.write(buf);
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+}, 'write() with a valid typed array buffer');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'atomic_writes.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await stream.write('foox');
+
+ const stream2 = await cleanup_writable(t, await handle.createWritable());
+ await stream2.write('bar');
+
+ assert_equals(await getFileSize(handle), 0);
+
+ await stream2.close();
+ assert_equals(await getFileContents(handle), 'bar');
+ assert_equals(await getFileSize(handle), 3);
+
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foox');
+ assert_equals(await getFileSize(handle), 4);
+}, 'atomic writes: writable file streams make atomic changes on close');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'atomic_write_after_close.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await stream.write('foo');
+
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+
+ await promise_rejects_js(
+ t, TypeError, stream.write('abc'));
+}, 'atomic writes: write() after close() fails');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createEmptyFile(t, 'atomic_truncate_after_close.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await stream.write('foo');
+
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+
+ await promise_rejects_js(t, TypeError, stream.truncate(0));
+}, 'atomic writes: truncate() after close() fails');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'atomic_close_after_close.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await stream.write('foo');
+
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'foo');
+ assert_equals(await getFileSize(handle), 3);
+
+ await promise_rejects_js(t, TypeError, stream.close());
+}, 'atomic writes: close() after close() fails');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'there_can_be_only_one.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await stream.write('foo');
+
+ // This test might be flaky if there is a race condition allowing
+ // close() to be called multiple times.
+ const success_promises =
+ [...Array(100)].map(() => stream.close().then(() => 1).catch(() => 0));
+ const close_attempts = await Promise.all(success_promises);
+ const success_count = close_attempts.reduce((x, y) => x + y);
+ assert_equals(success_count, 1);
+}, 'atomic writes: only one close() operation may succeed');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'writer_written', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ assert_false(stream.locked);
+ const writer = stream.getWriter();
+ assert_true(stream.locked);
+
+ await writer.write('foo');
+ await writer.write(new Blob(['bar']));
+ await writer.write({type: 'seek', position: 0});
+ await writer.write({type: 'write', data: 'baz'});
+ await writer.close();
+
+ assert_equals(await getFileContents(handle), 'bazbar');
+ assert_equals(await getFileSize(handle), 6);
+}, 'getWriter() can be used');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(
+ t, 'content.txt', 'very long string', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await promise_rejects_dom(
+ t, 'SyntaxError', stream.write({type: 'truncate'}),
+ 'truncate without size');
+}, 'WriteParams: truncate missing size param');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'content.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await promise_rejects_dom(
+ t, 'SyntaxError', stream.write({type: 'write'}), 'write without data');
+}, 'WriteParams: write missing data param');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'content.txt', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await promise_rejects_js(
+ t, TypeError, stream.write({type: 'write', data: null}),
+ 'write with null data');
+}, 'WriteParams: write null data param');
+
+directory_test(async (t, root) => {
+ const handle =
+ await createFileWithContents(t, 'content.txt', 'seekable', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await promise_rejects_dom(
+ t, 'SyntaxError', stream.write({type: 'seek'}), 'seek without position');
+}, 'WriteParams: seek missing position param');
+
+directory_test(async (t, root) => {
+ const source_file =
+ await createFileWithContents(t, 'source_file', 'source data', root);
+ const source_blob = await source_file.getFile();
+ await root.removeEntry(source_file.name);
+
+ const handle = await createEmptyFile(t, 'invalid_blob_test', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ await promise_rejects_dom(t, "NotFoundError", stream.write(source_blob));
+ await promise_rejects_js(t, TypeError, stream.close());
+
+ assert_equals(await getFileContents(handle), '');
+ assert_equals(await getFileSize(handle), 0);
+}, 'write() with an invalid blob to an empty file should reject');
diff --git a/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream.js b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream.js
new file mode 100644
index 0000000000..19dc6371cf
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream.js
@@ -0,0 +1,105 @@
+'use strict';
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'trunc_shrink', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('1234567890');
+ await stream.truncate(5);
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '12345');
+ assert_equals(await getFileSize(handle), 5);
+}, 'truncate() to shrink a file');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'trunc_grow', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+
+ await stream.write('abc');
+ await stream.truncate(5);
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), 'abc\0\0');
+ assert_equals(await getFileSize(handle), 5);
+}, 'truncate() to grow a file');
+
+directory_test(async (t, root) => {
+ const dir = await createDirectory(t, 'parent_dir', root);
+ const file_name = 'create_writable_fails_when_dir_removed.txt';
+ const handle = await createEmptyFile(t, file_name, dir);
+
+ await root.removeEntry('parent_dir', {recursive: true});
+ await promise_rejects_dom(t, 'NotFoundError', cleanup_writable(t, handle.createWritable()));
+}, 'createWritable() fails when parent directory is removed');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(
+ t, 'atomic_file_is_copied.txt', 'fooks', root);
+ const stream = await cleanup_writable(t, await handle.createWritable({keepExistingData: true}));
+
+ await stream.write('bar');
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'barks');
+ assert_equals(await getFileSize(handle), 5);
+}, 'createWritable({keepExistingData: true}): atomic writable file stream initialized with source contents');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(
+ t, 'atomic_file_is_not_copied.txt', 'very long string', root);
+ const stream = await cleanup_writable(t, await handle.createWritable({keepExistingData: false}));
+
+ await stream.write('bar');
+ assert_equals(await getFileContents(handle), 'very long string');
+ await stream.close();
+ assert_equals(await getFileContents(handle), 'bar');
+ assert_equals(await getFileSize(handle), 3);
+}, 'createWritable({keepExistingData: false}): atomic writable file stream initialized with empty file');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(
+ t, 'trunc_smaller_offset.txt', '1234567890', root);
+ const stream = await cleanup_writable(t, await handle.createWritable({keepExistingData: true}));
+
+ await stream.truncate(5);
+ await stream.write('abc');
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), 'abc45');
+ assert_equals(await getFileSize(handle), 5);
+}, 'cursor position: truncate size > offset');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(
+ t, 'trunc_bigger_offset.txt', '1234567890', root);
+ const stream = await cleanup_writable(t, await handle.createWritable({keepExistingData: true}));
+
+ await stream.seek(6);
+ await stream.truncate(5);
+ await stream.write('abc');
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), '12345abc');
+ assert_equals(await getFileSize(handle), 8);
+}, 'cursor position: truncate size < offset');
+
+directory_test(async (t, root) => {
+ const handle = await createEmptyFile(t, 'contents', root);
+ const stream = await cleanup_writable(t, await handle.createWritable());
+ assert_false(stream.locked);
+
+ stream.write('abc');
+ assert_false(stream.locked);
+ stream.write('def');
+ assert_false(stream.locked);
+ stream.truncate(9);
+ assert_false(stream.locked);
+ stream.seek(0);
+ assert_false(stream.locked);
+ stream.write('xyz');
+ assert_false(stream.locked);
+ await stream.close();
+
+ assert_equals(await getFileContents(handle), 'xyzdef\0\0\0');
+ assert_equals(await getFileSize(handle), 9);
+}, 'commands are queued, stream is unlocked after each operation');