summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/fs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.js4
-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.window.js4
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-cross-primitive-locking.https.tentative.worker.js135
-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-back-forward-cache.https.tentative.window.js64
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-lock-modes.https.tentative.worker.js114
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js93
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-back-forward-cache.https.tentative.window.js64
-rw-r--r--testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-lock-modes.https.tentative.worker.js69
-rw-r--r--testing/web-platform/tests/fs/FileSystemObserver.https.tentative.window.js3
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.worker.js40
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.worker.js29
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.worker.js21
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.worker.js314
-rw-r--r--testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.worker.js70
-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/bfcache-test-helpers.js47
-rw-r--r--testing/web-platform/tests/fs/resources/bfcache-test-page.js34
-rw-r--r--testing/web-platform/tests/fs/resources/bfcache-test-worker.js42
-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.js178
-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.js28
-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.js382
-rw-r--r--testing/web-platform/tests/fs/root-name.https.any.js6
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js145
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js42
-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.js27
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js52
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js359
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemObserver.js57
-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.js364
-rw-r--r--testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream.js105
82 files changed, 5250 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..cd78c5a950
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemBaseHandle-buckets.https.any.js
@@ -0,0 +1,4 @@
+// META: script=resources/test-helpers.js
+// META: script=resources/sandboxed-fs-test-helpers.js
+// META: script=/storage/buckets/resources/util.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.window.js b/testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.window.js
new file mode 100644
index 0000000000..6e20b57cb7
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-create-sync-access-handle.https.window.js
@@ -0,0 +1,4 @@
+// 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
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-cross-primitive-locking.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemFileHandle-cross-primitive-locking.https.tentative.worker.js
new file mode 100644
index 0000000000..df71082b11
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-cross-primitive-locking.https.tentative.worker.js
@@ -0,0 +1,135 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/sandboxed-fs-test-helpers.js');
+importScripts('resources/test-helpers.js');
+
+'use strict';
+
+// Adds tests to test the interaction between a lock created by the move
+// operation and a lock created by `createLock`.
+function generateCrossLockMoveTests(lockName, createLock) {
+ generateCrossLockTests(createMoveWithCleanup, createLock, {
+ diffFile: `A file with an ongoing move operation does not interfere with` +
+ ` ${lockName} on another file`,
+ acquireAfterRelease: `After a file has finished moving, that file can` +
+ ` have ${lockName}`,
+ // TODO(https://github.com/whatwg/fs/pull/10): Add tests for directory moves
+ // once supported.
+ });
+
+ directory_test(async (t, rootDir) => {
+ const [fooFileHandle, barFileHandle] =
+ await createFileHandles(rootDir, 'foo.test', 'bar.test');
+
+ createLock(t, fooFileHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError',
+ createMoveWithCleanup(t, barFileHandle, 'foo.test'));
+ }, `A file cannot be moved to a location with ${lockName}`);
+}
+
+// Adds tests to test the interaction between a lock created by the remove
+// operation and a lock created by `createLock`.
+function generateCrossLockRemoveTests(lockName, createLock) {
+ generateCrossLockTests(createRemoveWithCleanup, createLock, {
+ diffFile: `A file with an ongoing remove operation does not interfere` +
+ ` with the creation of ${lockName} on another file`,
+ acquireAfterRelease: `After a file has finished being removed, that file` +
+ ` can have ${lockName}`,
+ });
+ generateCrossLockTests(createLock, createRemoveWithCleanup, {
+ takeFileThenDir: `A directory cannot be removed if it contains a file` +
+ ` that has ${lockName}.`,
+ });
+}
+
+// Gets the name of a writable file stream opened in `wfsMode` to be used in
+// tests.
+function getWFSLockName(wfsMode) {
+ return `an open writable stream in ${wfsMode} mode`
+}
+
+// Adds tests to test the interaction between a lock created by an open writable
+// and a lock created by `createLock`.
+function generateCrossLockWFSTests(lockName, createLock, wfsMode) {
+ const WFSLockName = getWFSLockName(wfsMode);
+ const tests = {
+ sameFile: `When there's ${WFSLockName} on a file, cannot have` +
+ ` ${lockName} on that same file`,
+ diffFile: `A file with ${WFSLockName} does not interfere with` +
+ ` ${lockName} on another file`,
+ };
+ if (wfsMode === 'siloed') {
+ tests.multiAcquireAfterRelease = `After all writable streams in siloed` +
+ ` mode have been closed for a file, that file can have ${lockName}`;
+ } else {
+ tests.acquireAfterRelease = `After a writable stream in exclusive mode` +
+ ` has been closed for a file, that file can have ${lockName}`;
+ }
+ generateCrossLockTests(
+ createWFSWithCleanupFactory({mode: wfsMode}), createLock, tests);
+}
+
+// Adds tests to test the interaction between a lock created by an open access
+// handle in `sahMode and locks created by other file primitives and operations.
+function generateCrossLockSAHTests(sahMode) {
+ const createSAHLock = createSAHWithCleanupFactory({mode: sahMode});
+ const SAHLockName = `an open access handle in ${sahMode} mode`;
+
+ // Test interaction between move locks and SAH locks.
+ generateCrossLockMoveTests(SAHLockName, createSAHLock);
+ generateCrossLockTests(createSAHLock, createMoveWithCleanup, {
+ sameFile: `A file with ${SAHLockName} cannot be moved`,
+ diffFile: `A file with ${SAHLockName} does not interfere with moving` +
+ ` another file`,
+ acquireAfterRelease: `After ${SAHLockName} on a file has been closed,` +
+ ` that file can be moved`,
+ });
+
+ // Test interaction between remove locks and SAH locks.
+ generateCrossLockRemoveTests(SAHLockName, createSAHLock);
+ generateCrossLockTests(createSAHLock, createRemoveWithCleanup, {
+ sameFile: `A file with ${SAHLockName} cannot be removed`,
+ diffFile: `A file with ${SAHLockName} does not interfere with removing` +
+ ` another file`,
+ acquireAfterRelease: `After ${SAHLockName} on a file has been closed,` +
+ ` that file can be removed`,
+ });
+
+ // Test interaction between WFS locks and SAH locks.
+ for (const wfsMode of WFS_MODES) {
+ const WFSLockName = getWFSLockName(wfsMode);
+ const wfsOptions = {mode: wfsMode};
+ generateCrossLockWFSTests(SAHLockName, createSAHLock, wfsMode);
+ generateCrossLockTests(
+ createSAHLock, createWFSWithCleanupFactory(wfsOptions), {
+ sameFile: `When there's ${SAHLockName} on a file, cannot open` +
+ ` ${WFSLockName} on that same file`,
+ diffFile: `A file with ${SAHLockName} does not interfere with the` +
+ ` creation of ${WFSLockName} on another file`,
+ });
+ }
+}
+
+// Test interaction for each SAH lock mode.
+for (const sahMode of SAH_MODES) {
+ generateCrossLockSAHTests(sahMode);
+}
+
+// Test interaction for each WFS lock mode.
+for (const wfsMode of WFS_MODES) {
+ const WFSLockName = getWFSLockName(wfsMode);
+ const wfsOptions = {mode: wfsMode};
+ // Test interaction between move locks and WFS locks.
+ generateCrossLockMoveTests(
+ WFSLockName, createWFSWithCleanupFactory(wfsOptions));
+ generateCrossLockWFSTests(
+ 'an ongoing move operation', createMoveWithCleanup, wfsMode);
+
+ // Test interaction between remove locks and WFS locks.
+ generateCrossLockRemoveTests(
+ WFSLockName, createWFSWithCleanupFactory(wfsOptions));
+ generateCrossLockWFSTests(
+ 'an ongoing remove operation', createRemoveWithCleanup, wfsMode);
+}
+
+done();
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-back-forward-cache.https.tentative.window.js b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-back-forward-cache.https.tentative.window.js
new file mode 100644
index 0000000000..46a6c726ab
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-back-forward-cache.https.tentative.window.js
@@ -0,0 +1,64 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: timeout=long
+
+'use strict';
+
+
+createBFCacheTest(async (t, testControls) => {
+ const {getRemoteFuncs, assertBFCacheEligibility} = testControls;
+ const [createAndReleaseSAH] = getRemoteFuncs('createAndReleaseSAH');
+
+ for (const mode of SAH_MODES) {
+ await createAndReleaseSAH(mode, 'hello.txt');
+ await assertBFCacheEligibility(/*shouldRestoreFromBFCache=*/ true);
+ }
+}, 'Creating an SAH should not make it ineligible for the BFCache.');
+
+createBFCacheTest(async (t, testControls) => {
+ const origFile = 'hello.txt';
+ const diffFile = 'world.txt';
+
+ const {getRemoteFuncs, forward, back} = testControls;
+ const [createSAH, releaseSAH, createAndReleaseSAH] =
+ getRemoteFuncs('createSAH', 'releaseSAH', 'createAndReleaseSAH');
+
+ async function testTakeLockOnForward(
+ mode, fileName, shouldRestoreFromBFCache) {
+ await forward();
+
+ assert_true(await createAndReleaseSAH(mode, fileName));
+
+ await back(shouldRestoreFromBFCache);
+ }
+
+ for (const backMode of SAH_MODES) {
+ for (const forwMode of SAH_MODES) {
+ const contentiousLocks = sahModesAreContentious(backMode, forwMode);
+
+ // Create a lock on the page that will be BFCached.
+ const lockId = await createSAH(backMode, origFile);
+ assert_true(lockId !== undefined);
+
+ // Navigating to a new page and taking a lock on a different file should
+ // not evict the page from BFCache.
+ await testTakeLockOnForward(
+ forwMode, diffFile, /*shouldRestoreFromBFCache=*/ true);
+
+ // Navigating to a new page and taking a lock on the same file should only
+ // evict if the locks are contentious.
+ await testTakeLockOnForward(
+ forwMode, origFile, /*shouldRestoreFromBFCache=*/ !contentiousLocks);
+
+ // Release the lock when there isn't contention since it won't have been
+ // evicted.
+ if (!contentiousLocks) {
+ await releaseSAH(lockId);
+ }
+ }
+ }
+}, `Creating a SAH on an active page evicts an inactive page on contention.`)
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-lock-modes.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-lock-modes.https.tentative.worker.js
new file mode 100644
index 0000000000..bb82632e18
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-lock-modes.https.tentative.worker.js
@@ -0,0 +1,114 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/sandboxed-fs-test-helpers.js');
+importScripts('resources/test-helpers.js');
+
+'use strict';
+
+const LOCK_WRITE_PERMISSION = {
+ NOT_WRITABLE: 'not writable',
+ WRITABLE: 'writable',
+};
+
+async function testLockWritePermission(t, fileHandle, createSAHLock) {
+ const syncHandle = await createSAHLock(t, fileHandle);
+
+ let permission;
+ const writeBuffer = new TextEncoder().encode('Hello Storage Foundation');
+ try {
+ syncHandle.write(writeBuffer, {at: 0});
+ permission = LOCK_WRITE_PERMISSION.WRITABLE;
+ } catch (e) {
+ permission = LOCK_WRITE_PERMISSION.NOT_WRITABLE;
+ assert_throws_dom('NoModificationAllowedError', () => {
+ throw e;
+ });
+ }
+ // truncate and flush should throw a NoModificationAllowedError if an only if
+ // write threw a NoModificationAllowedError.
+ if (permission == LOCK_WRITE_PERMISSION.WRITABLE) {
+ syncHandle.truncate(0);
+ syncHandle.flush();
+ } else {
+ assert_throws_dom(
+ 'NoModificationAllowedError', () => syncHandle.truncate(0));
+ assert_throws_dom('NoModificationAllowedError', () => syncHandle.flush());
+ }
+
+ return permission;
+}
+
+// Adds tests for expected behaviors of an access handle created in `sahMode`
+// mode.
+function lockPropertyTests(
+ sahMode, expectedLockAccess, expectedLockWritePermission) {
+ const createSAHLock = createSAHWithCleanupFactory({mode: sahMode});
+
+ directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const {mode} = await createSAHLock(t, fileHandle);
+ assert_equals(mode, sahMode);
+ }, `An access handle in ${sahMode} mode has a mode property equal to` +
+ ` ${sahMode}`);
+
+ directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+ assert_equals(
+ await testLockAccess(t, fileHandle, createSAHLock), expectedLockAccess);
+ }, `An access handle in ${sahMode} mode takes a lock that is` +
+ ` ${expectedLockAccess}`);
+
+ directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+ assert_equals(
+ await testLockWritePermission(t, fileHandle, createSAHLock),
+ expectedLockWritePermission);
+ }, `An access handle in ${sahMode} mode is ${expectedLockWritePermission}`);
+
+ // Test interaction with other access handle modes.
+ for (const mode of SAH_MODES) {
+ // Add tests depending on which access handle modes are being tested against
+ // each other.
+ const testingAgainstSelf = mode === sahMode;
+ const testingExclusiveLock = expectedLockAccess === 'exclusive';
+ const tests = {
+ diffFile: `When there's an open access handle in ${sahMode} mode on a` +
+ ` file, can open another access handle in ${mode} on a different` +
+ ` file`,
+ };
+ if (!testingAgainstSelf || testingExclusiveLock) {
+ tests.sameFile = `When there's an open access handle in ${sahMode} mode` +
+ ` on a file, cannot open another access handle in ${mode} on that` +
+ ` same file`;
+ }
+ if (testingExclusiveLock) {
+ tests.acquireAfterRelease = `After an access handle in ${sahMode} mode` +
+ ` on a file has been closed, can open another access handle in` +
+ ` ${mode} on the same file`;
+ }
+ if (!testingExclusiveLock && !testingAgainstSelf) {
+ tests.multiAcquireAfterRelease = `After all access handles in` +
+ ` ${sahMode} mode on a file has been closed, can open another` +
+ ` access handle in ${mode} on the same file`;
+ }
+
+ generateCrossLockTests(
+ createSAHLock, createSAHWithCleanupFactory({mode: mode}), tests);
+ }
+}
+
+directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const syncHandle = await createSAHWithCleanup(t, fileHandle);
+ assert_equals(syncHandle.mode, 'readwrite');
+}, 'A sync access handle opens in readwrite mode by default');
+
+lockPropertyTests(
+ 'readwrite', LOCK_ACCESS.EXCLUSIVE, LOCK_WRITE_PERMISSION.WRITABLE);
+lockPropertyTests(
+ 'read-only', LOCK_ACCESS.SHARED, LOCK_WRITE_PERMISSION.NOT_WRITABLE);
+lockPropertyTests(
+ 'readwrite-unsafe', LOCK_ACCESS.SHARED, LOCK_WRITE_PERMISSION.WRITABLE);
+
+done();
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..169364cb44
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-sync-access-handle-writable-lock.https.tentative.worker.js
@@ -0,0 +1,93 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/test-helpers.js');
+importScripts('resources/sandboxed-fs-test-helpers.js');
+importScripts('resources/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();
+ t.add_cleanup(() => syncHandle1.close());
+ 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();
+ t.add_cleanup(() => barSyncHandle1.close());
+ 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();
+ t.add_cleanup(() => { syncHandle.close(); });
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', cleanup_writable(t, 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/FileSystemFileHandle-writable-file-stream-back-forward-cache.https.tentative.window.js b/testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-back-forward-cache.https.tentative.window.js
new file mode 100644
index 0000000000..7195dff654
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-back-forward-cache.https.tentative.window.js
@@ -0,0 +1,64 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/utils.js
+// META: script=resources/test-helpers.js
+// META: script=resources/messaging-helpers.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: timeout=long
+
+'use strict';
+
+
+createBFCacheTest(async (t, testControls) => {
+ const {getRemoteFuncs, assertBFCacheEligibility} = testControls;
+ const [createAndReleaseWFS] = getRemoteFuncs('createAndReleaseWFS');
+
+ for (const mode of WFS_MODES) {
+ await createAndReleaseWFS(mode, 'hello.txt');
+ await assertBFCacheEligibility(/*shouldRestoreFromBFCache=*/ true);
+ }
+}, 'Creating an WFS should not make it ineligible for the BFCache.');
+
+createBFCacheTest(async (t, testControls) => {
+ const origFile = 'hello.txt';
+ const diffFile = 'world.txt';
+
+ const {getRemoteFuncs, forward, back} = testControls;
+ const [createWFS, releaseWFS, createAndReleaseWFS] =
+ getRemoteFuncs('createWFS', 'releaseWFS', 'createAndReleaseWFS');
+
+ async function testTakeLockOnForward(
+ mode, fileName, shouldRestoreFromBFCache) {
+ await forward();
+
+ assert_true(await createAndReleaseWFS(mode, fileName));
+
+ await back(shouldRestoreFromBFCache);
+ }
+
+ for (const backMode of WFS_MODES) {
+ for (const forwMode of WFS_MODES) {
+ const contentiousLocks = wfsModesAreContentious(backMode, forwMode);
+
+ // Create a lock on the page that will be BFCached.
+ const lockId = await createWFS(backMode, origFile);
+ assert_true(lockId !== undefined);
+
+ // Navigating to a new page and taking a lock on a different file should
+ // not evict the page from BFCache.
+ await testTakeLockOnForward(
+ forwMode, diffFile, /*shouldRestoreFromBFCache=*/ true);
+
+ // Navigating to a new page and taking a lock on the same file should only
+ // evict if the locks are contentious.
+ await testTakeLockOnForward(
+ forwMode, origFile, /*shouldRestoreFromBFCache=*/ !contentiousLocks);
+
+ // Release the lock when there isn't contention since it won't have been
+ // evicted.
+ if (!contentiousLocks) {
+ await releaseWFS(lockId);
+ }
+ }
+ }
+}, `Creating a WFS on an active page evicts an inactive page on contention.`)
diff --git a/testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-lock-modes.https.tentative.worker.js b/testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-lock-modes.https.tentative.worker.js
new file mode 100644
index 0000000000..3e9b4334b9
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemFileHandle-writable-file-stream-lock-modes.https.tentative.worker.js
@@ -0,0 +1,69 @@
+importScripts('/resources/testharness.js');
+importScripts('resources/sandboxed-fs-test-helpers.js');
+importScripts('resources/test-helpers.js');
+
+'use strict';
+
+// Adds tests for expected behaviors of a writable stream created in `wfsMode`
+// mode.
+function lockPropertyTests(wfsMode, expectedLockAccess) {
+ const createWFSLock = createWFSWithCleanupFactory({mode: wfsMode});
+
+ directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const {mode} = await createWFSLock(t, fileHandle);
+ assert_equals(mode, wfsMode);
+ }, `A writable stream in ${wfsMode} mode has a mode property equal to` +
+ ` ${wfsMode}`);
+
+ directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+ assert_equals(
+ await testLockAccess(t, fileHandle, createWFSLock), expectedLockAccess);
+ }, `A writable stream in ${wfsMode} mode takes a lock that is` +
+ ` ${expectedLockAccess}`);
+
+ // Test interaction with other writable stream modes.
+ for (const mode of WFS_MODES) {
+ // Add tests depending on which writable stream modes are being tested
+ // against each other.
+ const testingAgainstSelf = mode === wfsMode;
+ const testingExclusiveLock = expectedLockAccess === 'exclusive';
+ const tests = {
+ diffFile: `When there's an open writable stream in ${wfsMode} mode on a` +
+ ` file, can open another writable stream in ${mode} on a different` +
+ ` file`,
+ };
+ if (!testingAgainstSelf || testingExclusiveLock) {
+ tests.sameFile = `When there's an open writable stream in ${wfsMode}` +
+ ` mode on a file, cannot open another writable stream in ${mode} on` +
+ ` that same file`;
+ }
+ if (testingExclusiveLock) {
+ tests.acquireAfterRelease = `After a writable stream in ${wfsMode} mode` +
+ ` on a file has been closed, can open another writable stream in` +
+ ` ${mode} on the same file`;
+ }
+ if (!testingExclusiveLock && !testingAgainstSelf) {
+ tests.multiAcquireAfterRelease = `After all writable streams in` +
+ ` ${wfsMode} mode on a file has been closed, can open another` +
+ ` writable stream in ${mode} on the same file`;
+ }
+
+ generateCrossLockTests(
+ createWFSLock, createWFSWithCleanupFactory({mode: mode}), tests);
+ }
+}
+
+directory_test(async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const syncHandle = await createWFSWithCleanup(t, fileHandle);
+ assert_equals(syncHandle.mode, 'siloed');
+}, 'A writable stream opens in siloed mode by default');
+
+lockPropertyTests('siloed', LOCK_ACCESS.SHARED);
+lockPropertyTests('exclusive', LOCK_ACCESS.EXCLUSIVE);
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemObserver.https.tentative.window.js b/testing/web-platform/tests/fs/FileSystemObserver.https.tentative.window.js
new file mode 100644
index 0000000000..2b9f7ca7c7
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemObserver.https.tentative.window.js
@@ -0,0 +1,3 @@
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+// META: script=resources/messaging-helpers.js
+// META: script=script-tests/FileSystemObserver.js
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.worker.js
new file mode 100644
index 0000000000..1c6aaf38a3
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-close.https.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.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.worker.js
new file mode 100644
index 0000000000..96953a88f2
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-flush.https.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.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.worker.js
new file mode 100644
index 0000000000..4b62b280b9
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-getSize.https.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.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.worker.js
new file mode 100644
index 0000000000..1c8fda94bb
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-read-write.https.worker.js
@@ -0,0 +1,314 @@
+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) => {
+ const readBuffer = new ArrayBuffer(0);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(0, readBytes, 'Check that no bytes were read');
+}, 'Test using an empty ArrayBuffer.');
+
+sync_access_handle_test((t, handle) => {
+ const readBuffer = new ArrayBuffer(24);
+ const readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(0, readBytes, 'Check that no bytes were read');
+}, 'Test using an ArrayBuffer.');
+
+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, {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 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.');
+
+sync_access_handle_test((t, handle) => {
+ if (!('TextEncoder' in self)) {
+ return;
+ }
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ let writeBuffer = encoder.encode("Hello ");
+ let writtenBytes = handle.write(writeBuffer);
+ writeBuffer = encoder.encode("World");
+ writtenBytes += handle.write(writeBuffer);
+ let readBuffer = new Uint8Array(256);
+ let readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(readBytes, "Hello World".length, 'Check that all bytes were read');
+ let actual = decoder.decode(readBuffer).substring(0, readBytes);
+ assert_equals(
+ actual, "Hello World",
+ 'Check content read from the handle');
+
+ readBuffer = new Uint8Array(5);
+ readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(readBytes, 5, 'Check that all bytes were read');
+ actual = decoder.decode(readBuffer).substring(0, readBytes);
+ assert_equals(
+ actual, "Hello",
+ 'Check content read from the handle');
+
+ readBuffer = new Uint8Array(256);
+ readBytes = handle.read(readBuffer);
+ assert_equals(readBytes, "Hello World".length - 5, 'Check that all bytes were read');
+ actual = decoder.decode(readBuffer).substring(0, readBytes);
+ assert_equals(
+ actual, " World",
+ 'Check content read from the handle');
+
+ readBuffer = new Uint8Array(5);
+ readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(readBytes, 5, 'Check that all bytes were read');
+ actual = decoder.decode(readBuffer);
+ assert_equals(
+ actual, "Hello",
+ 'Check content read from the handle');
+ writeBuffer = encoder.encode(" X");
+ writtenBytes = handle.write(writeBuffer);
+ assert_equals(writtenBytes, 2, 'Check overwrite length');
+
+ readBuffer = new Uint8Array(256);
+ readBytes = handle.read(readBuffer, {at: 0});
+ assert_equals(readBytes, "Hello Xorld".length, 'Check that all bytes were read');
+ actual = decoder.decode(readBuffer).substring(0, readBytes);
+ assert_equals(
+ actual, "Hello Xorld",
+ 'Check content read from the handle');
+}, 'Test reading and writing a file using the cursor');
+
+done();
diff --git a/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.worker.js b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.worker.js
new file mode 100644
index 0000000000..e5f557e070
--- /dev/null
+++ b/testing/web-platform/tests/fs/FileSystemSyncAccessHandle-truncate.https.worker.js
@@ -0,0 +1,70 @@
+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');
+
+sync_access_handle_test((t, handle) => {
+ const writeBuffer = new Uint8Array(4);
+ writeBuffer.set([96, 97, 98, 99]);
+ handle.write(writeBuffer, {at: 0});
+
+ // Moves cursor to 2
+ handle.truncate(2);
+ let readBuffer = new Uint8Array(256);
+ assert_equals(handle.read(readBuffer), 0);
+
+ writeBuffer.set([100, 101, 102, 103]);
+ handle.write(writeBuffer);
+
+ assert_equals(handle.read(readBuffer, {at: 0}), 6);
+ let expected = new Uint8Array(256);
+ expected.set([96, 97, 100, 101, 102, 103]);
+ assert_array_equals(readBuffer, expected);
+
+ // Resize the file to 10, expect that everything beyond the old size is '0'.
+ handle.truncate(10); // file cursor should still be at 6
+ // overwrite two bytes
+ const writeBuffer2 = new Uint8Array(2);
+ writeBuffer2.set([110, 111]);
+ handle.write(writeBuffer2);
+ expected = new Uint8Array(256);
+ expected.set([96, 97, 100, 101, 102, 103, 110, 111, 0, 0]);
+ assert_equals(handle.read(readBuffer, {at: 0}), 10);
+ assert_array_equals(readBuffer, expected);
+}, 'Test truncate effect on cursor');
+
+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/bfcache-test-helpers.js b/testing/web-platform/tests/fs/resources/bfcache-test-helpers.js
new file mode 100644
index 0000000000..70dde8c0b6
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/bfcache-test-helpers.js
@@ -0,0 +1,47 @@
+'use strict';
+
+// Calls `createLock` with a file handle for `fileName`. Returns the lock if it
+// succeeds. Returns undefined if it doesn't.
+export async function tryToCreateLock(fileName, createLock) {
+ const dir = await navigator.storage.getDirectory();
+ const fileHandle = await dir.getFileHandle(fileName, {create: true});
+
+ try {
+ return await createLock(fileHandle);
+ } catch {
+ return undefined;
+ }
+}
+
+// Returns a function that forwards `funcName` and the `args` passed to it to
+// the `bfcache-test-worker.js` dedicated worker.
+//
+// Will create the dedicated worker if it doesn't already exist.
+export const forwardToDedicatedWorker = (() => {
+ let dedicatedWorker;
+
+ // Returns a promise that resolves with the next dedicated worker result. Or
+ // rejects if there is an error on the worker.
+ function getNextDedicatedWorkerResult(dedicatedWorker) {
+ return new Promise((resolve, reject) => {
+ dedicatedWorker.addEventListener('message', ({data}) => {
+ resolve(data);
+ }, {once: true});
+ dedicatedWorker.addEventListener('error', () => {
+ reject(new Error('An error occurred on the dedicated worker.'));
+ }, {once: true});
+ });
+ }
+
+ return function(funcName) {
+ return (...args) => {
+ if (!dedicatedWorker) {
+ dedicatedWorker = new Worker(
+ `/fs/resources/bfcache-test-worker.js`, {type: 'module'});
+ }
+
+ dedicatedWorker.postMessage({funcName, args});
+ return getNextDedicatedWorkerResult(dedicatedWorker);
+ }
+ }
+})();
diff --git a/testing/web-platform/tests/fs/resources/bfcache-test-page.js b/testing/web-platform/tests/fs/resources/bfcache-test-page.js
new file mode 100644
index 0000000000..0ae9dbdf13
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/bfcache-test-page.js
@@ -0,0 +1,34 @@
+'use strict';
+
+import {forwardToDedicatedWorker, tryToCreateLock} from './bfcache-test-helpers.js';
+
+export const createSAH = forwardToDedicatedWorker('createSAH');
+export const releaseSAH = forwardToDedicatedWorker('releaseSAH');
+export const createAndReleaseSAH =
+ forwardToDedicatedWorker('createAndReleaseSAH');
+
+let openWFS;
+
+export async function createWFS(mode, fileName) {
+ if (openWFS) {
+ throw new Error('Already have an open writable.');
+ }
+ openWFS = await tryToCreateLock(
+ fileName, fileHandle => fileHandle.createWritable({mode}));
+ return openWFS !== undefined;
+}
+
+export async function releaseWFS() {
+ if (!openWFS) {
+ throw new Error('No open writable.');
+ }
+ await openWFS.close();
+ openWFS = undefined;
+}
+
+export async function createAndReleaseWFS(mode, fileName) {
+ const wfsLock = await tryToCreateLock(
+ fileName, fileHandle => fileHandle.createWritable({mode}));
+ await wfsLock?.close();
+ return wfsLock !== undefined;
+}
diff --git a/testing/web-platform/tests/fs/resources/bfcache-test-worker.js b/testing/web-platform/tests/fs/resources/bfcache-test-worker.js
new file mode 100644
index 0000000000..218858b24c
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/bfcache-test-worker.js
@@ -0,0 +1,42 @@
+'use strict';
+
+import {tryToCreateLock} from './bfcache-test-helpers.js';
+
+let openSAH;
+
+export async function createSAH(mode, fileName) {
+ if (openSAH) {
+ throw new Error('Already have an open access handle.');
+ }
+ openSAH = await tryToCreateLock(
+ fileName, fileHandle => fileHandle.createSyncAccessHandle({mode}));
+ return openSAH !== undefined;
+}
+
+export async function releaseSAH() {
+ if (!openSAH) {
+ throw new Error('No open access handle.');
+ }
+ await openSAH.close();
+ openSAH = undefined;
+}
+
+export async function createAndReleaseSAH(mode, fileName) {
+ const sahLock = await tryToCreateLock(
+ fileName, fileHandle => fileHandle.createSyncAccessHandle({mode}));
+ await sahLock?.close();
+ return sahLock !== undefined;
+}
+
+// Functions exposed to the renderer.
+const funcs = {
+ createSAH,
+ releaseSAH,
+ createAndReleaseSAH,
+};
+
+// Sets up a message handler that calls the `funcName` in `funcs` with `args`
+// and then postMessages the result back to the renderer.
+addEventListener('message', async ({data: {funcName, args}}) => {
+ postMessage(await funcs[funcName](...args));
+});
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..cdd86bcc66
--- /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 != window) {
+ 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..a59fa6bec2
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/message-target.js
@@ -0,0 +1,178 @@
+'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();
+ access_handle.close();
+ } catch (error) {
+ success = false;
+ }
+
+ message_source.postMessage(
+ { type: 'receive-sync-access-handle-result', success },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'create-file-system-observer':
+ // Attempt to create a file system observer with a dummy callback.
+ // Respond with whether creating the observer succeeded.
+ function dummyCallback(records, observer) {};
+
+ let createObserverSuccess = true;
+ try {
+ const observer = new FileSystemObserver(dummyCallback);
+ } catch (error) {
+ createObserverSuccess = false;
+ }
+
+ message_source.postMessage(
+ {
+ type: 'receive-create-file-system-observer-result',
+ createObserverSuccess
+ },
+ {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..400b2c507b
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/sandboxed-fs-test-helpers.js
@@ -0,0 +1,28 @@
+// 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.
+
+function getFileSystemType() {
+ return 'sandboxed';
+}
+
+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..9f109bbccd
--- /dev/null
+++ b/testing/web-platform/tests/fs/resources/test-helpers.js
@@ -0,0 +1,382 @@
+// A special path component meaning "this directory."
+const kCurrentDirectory = '.';
+
+// A special path component meaning "the parent directory."
+const kParentDirectory = '..';
+
+// The lock modes of a writable file stream.
+const WFS_MODES = ['siloed', 'exclusive'];
+
+// The lock modes of an access handle.
+const SAH_MODES = ['readwrite', 'read-only', 'readwrite-unsafe'];
+
+// Possible return values of testLockAccess.
+const LOCK_ACCESS = {
+ SHARED: 'shared',
+ EXCLUSIVE: 'exclusive',
+};
+
+function primitiveModesAreContentious(exclusiveMode, mode1, mode2) {
+ return mode1 != mode2 || mode1 === exclusiveMode;
+}
+
+function sahModesAreContentious(mode1, mode2) {
+ return primitiveModesAreContentious('readwrite', mode1, mode2);
+}
+
+function wfsModesAreContentious(mode1, mode2) {
+ return primitiveModesAreContentious('exclusive', mode1, mode2);
+}
+
+// Array of separators used to separate components in hierarchical paths.
+// Consider both '/' and '\' as path separators to ensure file names are
+// platform-agnostic.
+let 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.
+ }
+ });
+}
+
+function createFileHandles(dir, ...fileNames) {
+ return Promise.all(
+ fileNames.map(fileName => dir.getFileHandle(fileName, {create: true})));
+}
+
+// Releases a lock created by one of the create*WithCleanup functions below.
+async function releaseLock(lockPromise) {
+ const result = await lockPromise;
+ if (result?.close) {
+ await result.close();
+ }
+}
+
+function cleanupLockPromise(t, lockPromise) {
+ return cleanup(t, lockPromise, () => releaseLock(lockPromise));
+}
+
+function createWFSWithCleanup(t, fileHandle, wfsOptions) {
+ return cleanupLockPromise(t, fileHandle.createWritable(wfsOptions));
+}
+
+// Returns createWFSWithCleanup bound with wfsOptions.
+function createWFSWithCleanupFactory(wfsOptions) {
+ return (t, fileHandle) => createWFSWithCleanup(t, fileHandle, wfsOptions);
+}
+
+function createSAHWithCleanup(t, fileHandle, sahOptions) {
+ return cleanupLockPromise(t, fileHandle.createSyncAccessHandle(sahOptions));
+}
+
+// Returns createSAHWithCleanup bound with sahOptions.
+function createSAHWithCleanupFactory(sahOptions) {
+ return (t, fileHandle) => createSAHWithCleanup(t, fileHandle, sahOptions);
+}
+
+function createMoveWithCleanup(
+ t, fileHandle, fileName = 'unique-file-name.test') {
+ return cleanupLockPromise(t, fileHandle.move(fileName));
+}
+
+function createRemoveWithCleanup(t, fileHandle) {
+ return cleanupLockPromise(t, fileHandle.remove({recursive: true}));
+}
+
+// For each key in `testFuncs` if there is a matching key in `testDescs`,
+// creates a directory_test passing the respective key's value for the func and
+// description arguments. If there is not a matching key in `testDescs`, the
+// test is not created. This will throw if `testDescs` contains a key that is
+// not in `testFuncs`.
+function selectDirectoryTests(testDescs, testFuncs) {
+ for (const testDesc in testDescs) {
+ if (!testFuncs.hasOwnProperty(testDesc)) {
+ throw new Error(
+ 'Passed a test description in testDescs that wasn\'t in testFuncs.');
+ }
+ directory_test(testFuncs[testDesc], testDescs[testDesc]);
+ }
+}
+
+// Adds tests to test the interaction between a lock created by `createLock1`
+// and a lock created by `createLock2`.
+//
+// The description of each test is passed in through `testDescs`. If a test
+// description is omitted, it is not run.
+//
+// For all tests, `createLock1` is called first.
+function generateCrossLockTests(createLock1, createLock2, testDescs) {
+ if (testDescs === undefined) {
+ throw new Error('Must pass testDescs.');
+ }
+ selectDirectoryTests(testDescs, {
+
+ // This tests that a lock can't be acquired on a file that already has a
+ // lock of another type.
+ sameFile: async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ createLock1(t, fileHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, fileHandle));
+ },
+
+ // This tests that a lock on one file does not interfere with the creation
+ // of a lock on another file.
+ diffFile: async (t, rootDir) => {
+ const [fooFileHandle, barFileHandle] =
+ await createFileHandles(rootDir, 'foo.test', 'bar.test');
+
+ createLock1(t, fooFileHandle);
+ await createLock2(t, barFileHandle);
+ },
+
+ // This tests that after a lock has been acquired on a file and then
+ // released, another lock of another type can be acquired. This will fail if
+ // `createLock1` and `createLock2` create the same shared lock.
+ acquireAfterRelease: async (t, rootDir) => {
+ let [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const lockPromise = createLock1(t, fileHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, fileHandle));
+
+ await releaseLock(lockPromise);
+ // Recreate the file in case releasing the lock moves/removes it.
+ [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+ await createLock2(t, fileHandle);
+ },
+
+ // This tests that after multiple locks of some shared lock type have been
+ // acquired on a file and then all released, another lock of another lock
+ // type can be acquired.
+ multiAcquireAfterRelease: async (t, rootDir) => {
+ const [fileHandle] = await createFileHandles(rootDir, 'BFS.test');
+
+ const lock1 = await createLock1(t, fileHandle);
+ const lock2 = await createLock1(t, fileHandle);
+
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, fileHandle));
+ await lock1.close();
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, fileHandle));
+ await lock2.close();
+
+ await createLock2(t, fileHandle);
+ },
+
+ // This tests that a lock taken on a directory prevents a lock being
+ // acquired on a file contained within that directory.
+ takeDirThenFile: async (t, rootDir) => {
+ const dirHandle = await rootDir.getDirectoryHandle('foo', {create: true});
+ const [fileHandle] = await createFileHandles(dirHandle, 'BFS.test');
+
+ createLock1(t, dirHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, fileHandle));
+ },
+
+ // This tests that a lock acquired on a file prevents a lock being acquired
+ // on an ancestor of that file.
+ takeFileThenDir: async (t, rootDir) => {
+ const grandparentHandle =
+ await rootDir.getDirectoryHandle('foo', {create: true});
+ const parentHandle =
+ await grandparentHandle.getDirectoryHandle('bar', {create: true});
+ let [fileHandle] = await createFileHandles(parentHandle, 'BFS.test');
+
+ // Test parent handle.
+ const lock1 = createLock1(t, fileHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, parentHandle));
+
+ // Release the lock so we can recreate it.
+ await releaseLock(lock1);
+ // Recreate the file in case releasing the lock moves/removes it.
+ [fileHandle] = await createFileHandles(parentHandle, 'BFS.test');
+
+ // Test grandparent handle.
+ createLock1(t, fileHandle);
+ await promise_rejects_dom(
+ t, 'NoModificationAllowedError', createLock2(t, grandparentHandle));
+ },
+ });
+}
+
+// Tests whether the multiple locks can be created by createLock on a file
+// handle or if only one can. Returns LOCK_ACCESS.SHARED for the former and
+// LOCK_ACCESS.EXCLUSIVE for the latter.
+async function testLockAccess(t, fileHandle, createLock) {
+ createLock(t, fileHandle);
+
+ let access;
+ try {
+ await createLock(t, fileHandle);
+ access = LOCK_ACCESS.SHARED;
+ } catch (e) {
+ access = LOCK_ACCESS.EXCLUSIVE;
+ assert_throws_dom('NoModificationAllowedError', () => {
+ throw e;
+ });
+ }
+
+ return access;
+}
+
+// Creates a test with description `testDesc` to test behavior of the BFCache
+// with `testFunc`.
+function createBFCacheTest(testFunc, testDesc) {
+ // In the remote context `rc`, calls the `funcName` export of
+ // `bfcache-test-page.js` with `args`.
+ //
+ // Will import `bfcache-test-page.js` if it hasn't been imported already.
+ function executeFunc(rc, funcName, args) {
+ return rc.executeScript(async (funcName, args) => {
+ if (self.testPageFuncs === undefined) {
+ self.testPageFuncs =
+ (await import('/fs/resources/bfcache-test-page.js'));
+ }
+ return await self.testPageFuncs[funcName](...args);
+ }, [funcName, args]);
+ }
+
+ promise_test(async t => {
+ const rcHelper = new RemoteContextHelper();
+
+ // Open a window with noopener so that BFCache will work.
+ const backRc = await rcHelper.addWindow(null, {features: 'noopener'});
+ let curRc = backRc;
+
+ // Functions given to the test to control the BFCache test.
+ const testControls = {
+ // Returns an array of functions that bind `executeFunc` with curRc and
+ // their respective function name from `funcName`.
+ getRemoteFuncs: (...funcNames) => {
+ return funcNames.map(
+ funcName => (...args) => executeFunc(curRc, funcName, args));
+ },
+ forward: async () => {
+ if (curRc !== backRc) {
+ throw new Error('Can only navigate forward once.');
+ }
+ prepareForBFCache(curRc);
+ curRc = await curRc.navigateToNew();
+ },
+ back: async (shouldRestoreFromBFCache) => {
+ if (curRc === backRc) {
+ throw new Error(
+ 'Can\'t navigate back if you haven\'t navigated forward.');
+ }
+ await curRc.historyBack();
+ curRc = backRc;
+ if (shouldRestoreFromBFCache) {
+ await assertImplementsBFCacheOptional(curRc);
+ } else {
+ await assertNotRestoredFromBFCache(curRc);
+ }
+ },
+ assertBFCacheEligibility(shouldRestoreFromBFCache) {
+ return assertBFCacheEligibility(curRc, shouldRestoreFromBFCache);
+ }
+ };
+
+ await testFunc(t, testControls);
+ }, testDesc);
+}
diff --git a/testing/web-platform/tests/fs/root-name.https.any.js b/testing/web-platform/tests/fs/root-name.https.any.js
new file mode 100644
index 0000000000..650a7a64ee
--- /dev/null
+++ b/testing/web-platform/tests/fs/root-name.https.any.js
@@ -0,0 +1,6 @@
+'use strict';
+
+promise_test(async test => {
+ let root = await navigator.storage.getDirectory();
+ assert_equals(root.name, '');
+}, 'getDirectory returns a directory whose name is the empty string');
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..9e114619bf
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-IndexedDB.js
@@ -0,0 +1,145 @@
+'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.');
+
+directory_test(async (t, root_dir) => {
+ const expected_root_name = '';
+ assert_equals(root_dir.name, expected_root_name);
+
+ const db = await createDatabase(t, db => {
+ const store = db.createObjectStore('store', {keyPath: 'key'});
+ });
+
+ const value = [ root_dir ];
+ 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);
+
+ const actual = result.value[ 0 ];
+ assert_equals(actual.name, expected_root_name);
+ assert_true(await root_dir.isSameEntry(actual));
+}, 'Store and retrieve the root directory from IndexedDB.');
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..c06e940d7e
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemBaseHandle-buckets.js
@@ -0,0 +1,42 @@
+'use strict';
+
+directory_test(async (t, root_dir) => {
+ await prepareForBucketTest(t);
+
+ 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) => {
+ await prepareForBucketTest(t);
+
+ 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');
+
+directory_test(async (t, root_dir) => {
+ await prepareForBucketTest(t);
+
+ const inboxBucket = await navigator.storageBuckets.open('inbox', {quota: 500});
+ const inboxRootDir = await inboxBucket.getDirectory();
+
+ // Short file succeeds.
+ const file =
+ await createFileWithContents(t, 'mtime.txt', 'short file', inboxRootDir);
+
+ // Longer file fails.
+ return promise_rejects_dom(
+ t, 'QuotaExceededError',
+ createFileWithContents(
+ t, 'mtime2.txt',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum',
+ inboxRootDir));
+}, 'Bucket quota restricts the size of a file that can be created');
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..b9eafaf0d8
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-create-sync-access-handle.js
@@ -0,0 +1,27 @@
+'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) => {
+ const fileSystemType = getFileSystemType();
+ assert_true(
+ fileSystemType == 'sandboxed' || fileSystemType == 'local',
+ 'File system type should be sandboxed or local.');
+ const expect_success = fileSystemType == '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..be9fbcca6e
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-getFile.js
@@ -0,0 +1,52 @@
+'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');
+
+directory_test(async (t, root) => {
+ const fileName = "fileAttributesTest.txt";
+
+ const fileHandle = await createEmptyFile(t, fileName, root);
+ assert_equals(fileHandle.name, fileName);
+
+ const file = await fileHandle.getFile();
+ assert_equals(file.name, fileName);
+}, 'getFile() returns expected name');
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..c5d08f305d
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemFileHandle-move.js
@@ -0,0 +1,359 @@
+// 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 handle = await createFileWithContents(t, 'file-before', 'foo', root);
+ await promise_rejects_js(t, TypeError, handle.move('test/test'));
+ 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 path separators 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/FileSystemObserver.js b/testing/web-platform/tests/fs/script-tests/FileSystemObserver.js
new file mode 100644
index 0000000000..2c8fd57f7c
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemObserver.js
@@ -0,0 +1,57 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /fs/resources/messaging-helpers.js
+// /service-worker/resources/test-helpers.sub.js
+
+promise_test(async t => {
+ function dummyCallback(records, observer) {};
+ let success = true;
+ try {
+ const observer = new FileSystemObserver(dummyCallback);
+ } catch (error) {
+ success = false;
+ }
+ assert_true(success);
+}, 'Creating a FileSystemObserver from a window succeeds');
+
+promise_test(async t => {
+ const dedicated_worker =
+ create_dedicated_worker(t, kDedicatedWorkerMessageTarget);
+ dedicated_worker.postMessage({type: 'create-file-system-observer'});
+
+ const event_watcher = new EventWatcher(t, dedicated_worker, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_true(response.createObserverSuccess);
+}, 'Creating a FileSystemObserver from a dedicated worker succeeds');
+
+if (self.SharedWorker !== undefined) {
+ promise_test(async t => {
+ const shared_worker = new SharedWorker(kSharedWorkerMessageTarget);
+ shared_worker.port.start();
+ shared_worker.port.postMessage({type: 'create-file-system-observer'});
+
+ const event_watcher = new EventWatcher(t, shared_worker.port, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_true(response.createObserverSuccess);
+ }, 'Creating a FileSystemObserver from a shared worker succeeds');
+}
+
+promise_test(async t => {
+ const scope = `${kServiceWorkerMessageTarget}?create-observer`;
+ const registration =
+ await create_service_worker(t, kServiceWorkerMessageTarget, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ registration.active.postMessage({type: 'create-file-system-observer'});
+
+ const event_watcher = new EventWatcher(t, navigator.serviceWorker, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_false(response.createObserverSuccess);
+}, 'Creating a FileSystemObserver from a service worker fails');
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..246f420d0f
--- /dev/null
+++ b/testing/web-platform/tests/fs/script-tests/FileSystemWritableFileStream-write.js
@@ -0,0 +1,364 @@
+'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, 'write_string_with_offset_after_seek', root);
+ const stream = await handle.createWritable();
+
+ await stream.write('1234567890');
+ await stream.write({type: 'seek', position: 0});
+ 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 after seek');
+
+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');
+
+directory_test(async (t, root) => {
+ const handle = await createFileWithContents(t, 'file.txt', 'contents', root);
+ const stream = await handle.createWritable({mode: 'exclusive'});
+
+ await stream.write('12345');
+ await promise_rejects_js(
+ t, TypeError, stream.write({type: 'write', data: null}),
+ 'write with null data');
+
+ // The file contents should not have been changed.
+ assert_equals(await getFileContents(handle), 'contents');
+
+ // The file's lock was released.
+ const newStream = await handle.createWritable({mode: 'exclusive'});
+ await newStream.close();
+}, 'an errored writable stream releases its lock');
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');