summaryrefslogtreecommitdiffstats
path: root/dom/fs/test
diff options
context:
space:
mode:
Diffstat (limited to 'dom/fs/test')
-rw-r--r--dom/fs/test/common/.eslintrc.js14
-rw-r--r--dom/fs/test/common/dummy.js0
-rw-r--r--dom/fs/test/common/mochitest.toml15
-rw-r--r--dom/fs/test/common/moz.build21
-rw-r--r--dom/fs/test/common/nsresult.js9
-rw-r--r--dom/fs/test/common/test_basics.js375
-rw-r--r--dom/fs/test/common/test_fileSystemDirectoryHandle.js196
-rw-r--r--dom/fs/test/common/test_syncAccessHandle.js237
-rw-r--r--dom/fs/test/common/test_writableFileStream.js153
-rw-r--r--dom/fs/test/common/xpcshell.toml10
-rw-r--r--dom/fs/test/crashtests/1798773.html19
-rw-r--r--dom/fs/test/crashtests/1800470.html28
-rw-r--r--dom/fs/test/crashtests/1809759.html10
-rw-r--r--dom/fs/test/crashtests/1816710.html8
-rw-r--r--dom/fs/test/crashtests/1841702.html16
-rw-r--r--dom/fs/test/crashtests/1844619.html11
-rw-r--r--dom/fs/test/crashtests/1858820.html19
-rw-r--r--dom/fs/test/crashtests/crashtests.list10
-rw-r--r--dom/fs/test/crashtests/sw1844619.js21
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.cpp94
-rw-r--r--dom/fs/test/gtest/FileSystemMocks.h340
-rw-r--r--dom/fs/test/gtest/TestHelpers.cpp61
-rw-r--r--dom/fs/test/gtest/TestHelpers.h69
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp234
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp144
-rw-r--r--dom/fs/test/gtest/api/TestFileSystemHandle.cpp131
-rw-r--r--dom/fs/test/gtest/api/moz.build21
-rw-r--r--dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp64
-rw-r--r--dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp352
-rw-r--r--dom/fs/test/gtest/child/moz.build20
-rw-r--r--dom/fs/test/gtest/moz.build25
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp183
-rw-r--r--dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp474
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp185
-rw-r--r--dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp1059
-rw-r--r--dom/fs/test/gtest/parent/datamodel/moz.build22
-rw-r--r--dom/fs/test/gtest/parent/moz.build21
-rw-r--r--dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp179
-rw-r--r--dom/fs/test/gtest/shared/moz.build16
-rw-r--r--dom/fs/test/mochitest/head.js82
-rw-r--r--dom/fs/test/mochitest/mochitest.toml35
-rw-r--r--dom/fs/test/mochitest/test_basics.html28
-rw-r--r--dom/fs/test/mochitest/test_basics_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html28
-rw-r--r--dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_syncAccessHandle_worker.html22
-rw-r--r--dom/fs/test/mochitest/test_writableFileStream.html31
-rw-r--r--dom/fs/test/mochitest/test_writableFileStream_worker.html22
-rw-r--r--dom/fs/test/mochitest/worker/.eslintrc.js12
-rw-r--r--dom/fs/test/mochitest/worker/dummy.js0
-rw-r--r--dom/fs/test/mochitest/worker/head.js22
-rw-r--r--dom/fs/test/mochitest/worker/mochitest.toml15
-rw-r--r--dom/fs/test/mochitest/worker/test_basics_worker.js11
-rw-r--r--dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js13
-rw-r--r--dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js16
-rw-r--r--dom/fs/test/mochitest/worker/test_writableFileStream_worker.js16
-rw-r--r--dom/fs/test/moz.build16
-rw-r--r--dom/fs/test/xpcshell/head.js100
-rw-r--r--dom/fs/test/xpcshell/moz.build13
-rw-r--r--dom/fs/test/xpcshell/test_basics.js14
-rw-r--r--dom/fs/test/xpcshell/test_basics_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js14
-rw-r--r--dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_syncAccessHandle_worker.js8
-rw-r--r--dom/fs/test/xpcshell/test_writableFileStream.js14
-rw-r--r--dom/fs/test/xpcshell/test_writableFileStream_worker.js8
-rw-r--r--dom/fs/test/xpcshell/worker/.eslintrc.js12
-rw-r--r--dom/fs/test/xpcshell/worker/dummy.js0
-rw-r--r--dom/fs/test/xpcshell/worker/head.js22
-rw-r--r--dom/fs/test/xpcshell/worker/moz.build13
-rw-r--r--dom/fs/test/xpcshell/worker/test_basics_worker.js11
-rw-r--r--dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js13
-rw-r--r--dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js13
-rw-r--r--dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js13
-rw-r--r--dom/fs/test/xpcshell/xpcshell.toml16
75 files changed, 5579 insertions, 0 deletions
diff --git a/dom/fs/test/common/.eslintrc.js b/dom/fs/test/common/.eslintrc.js
new file mode 100644
index 0000000000..d805cf6e0f
--- /dev/null
+++ b/dom/fs/test/common/.eslintrc.js
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ globals: {
+ Assert: true,
+ exported_symbols: true,
+ require_module: true,
+ Utils: true,
+ },
+};
diff --git a/dom/fs/test/common/dummy.js b/dom/fs/test/common/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/common/dummy.js
diff --git a/dom/fs/test/common/mochitest.toml b/dom/fs/test/common/mochitest.toml
new file mode 100644
index 0000000000..3da39b96b8
--- /dev/null
+++ b/dom/fs/test/common/mochitest.toml
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files = [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_syncAccessHandle.js",
+ "test_writableFileStream.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/common/moz.build b/dom/fs/test/common/moz.build
new file mode 100644
index 0000000000..65d62c9cda
--- /dev/null
+++ b/dom/fs/test/common/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += [
+ "mochitest.toml",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell.toml",
+]
+
+TESTING_JS_MODULES.dom.fs.test.common += [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_syncAccessHandle.js",
+ "test_writableFileStream.js",
+]
diff --git a/dom/fs/test/common/nsresult.js b/dom/fs/test/common/nsresult.js
new file mode 100644
index 0000000000..6e59b947a1
--- /dev/null
+++ b/dom/fs/test/common/nsresult.js
@@ -0,0 +1,9 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const nsresult = {
+ NS_ERROR_NOT_IMPLEMENTED: Cr.NS_ERROR_NOT_IMPLEMENTED,
+};
+exported_symbols.nsresult = nsresult;
diff --git a/dom/fs/test/common/test_basics.js b/dom/fs/test/common/test_basics.js
new file mode 100644
index 0000000000..f1cb1c222e
--- /dev/null
+++ b/dom/fs/test/common/test_basics.js
@@ -0,0 +1,375 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test must be first, since we need the actor not to be created already.
+exported_symbols.testGetDirectoryTwice = async function () {
+ const promise1 = navigator.storage.getDirectory();
+ const promise2 = navigator.storage.getDirectory();
+
+ await Promise.all([promise1, promise2]);
+
+ Assert.ok(true, "Should not have thrown");
+};
+
+exported_symbols.testGetDirectoryDoesNotThrow = async function () {
+ await navigator.storage.getDirectory();
+
+ Assert.ok(true, "Should not have thrown");
+};
+
+exported_symbols.testGetDirectoryKindIsDirectory = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ Assert.equal(root.kind, "directory");
+};
+
+exported_symbols.testDirectoryHandleStringConversion = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ Assert.equal(
+ "" + root,
+ "[object FileSystemDirectoryHandle]",
+ "Is directoryHandle convertible to string?"
+ );
+};
+
+exported_symbols.testNewDirectoryHandleFromPrototype = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ try {
+ Object.create(root.prototype);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Should have thrown");
+ Assert.ok(ex instanceof TypeError, "Threw the right error type");
+ }
+};
+
+exported_symbols.testIsSameEntryRoot = async function () {
+ const root = await navigator.storage.getDirectory();
+ try {
+ await root.move(root);
+ Assert.ok(false, "root should not be movable");
+ } catch (ex) {
+ Assert.ok(true, "root isn't movable");
+ }
+};
+
+exported_symbols.testDirectoryHandleSupportsKeysIterator = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.keys();
+ Assert.ok(!!it, "Does root support keys iterator?");
+};
+
+exported_symbols.testKeysIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.keys();
+ Assert.ok(!!it, "Does root support keys iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testDirectoryHandleSupportsValuesIterator = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.values();
+ Assert.ok(!!it, "Does root support values iterator?");
+};
+
+exported_symbols.testValuesIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.values();
+ Assert.ok(!!it, "Does root support values iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testDirectoryHandleSupportsEntriesIterator =
+ async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.entries();
+ Assert.ok(!!it, "Does root support entries iterator?");
+ };
+
+exported_symbols.testEntriesIteratorNextIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ const it = await root.entries();
+ Assert.ok(!!it, "Does root support entries iterator?");
+
+ const item = await it.next();
+ Assert.ok(!!item, "Should return an item");
+};
+
+exported_symbols.testGetFileHandleIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+
+ const item = await root.getFileHandle("fileName", allowCreate);
+ Assert.ok(!!item, "Should return an item");
+
+ await root.removeEntry("fileName");
+};
+
+exported_symbols.testGetDirectoryHandleIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+
+ const item = await root.getDirectoryHandle("dirName", allowCreate);
+ Assert.ok(!!item, "Should return an item");
+
+ await root.removeEntry("dirName");
+};
+
+exported_symbols.testRemoveEntryIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const removeOptions = { recursive: true };
+ const allowCreate = { create: true };
+
+ // Ensure file and directory items exists
+ await root.getFileHandle("fileName", allowCreate);
+ await root.getDirectoryHandle("dirName", allowCreate);
+ await root.removeEntry("fileName", removeOptions);
+ await root.removeEntry("dirName", removeOptions);
+ try {
+ await root.removeEntry("doesNotExist", removeOptions);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Should have thrown");
+ Assert.equal(
+ ex.message,
+ "Entry not found",
+ "Threw the right error message"
+ );
+ }
+};
+
+exported_symbols.testResolveIsCallable = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+ const item = await root.getFileHandle("fileName", allowCreate);
+
+ let path = await root.resolve(item);
+ Assert.equal(path.length, 1);
+ Assert.equal(path[0], "fileName", "Resolve got the right path");
+
+ await root.removeEntry("fileName");
+};
+
+exported_symbols.testFileType = async function () {
+ const root = await navigator.storage.getDirectory();
+ const allowCreate = { create: true };
+ const nameStem = "testFileType";
+ const empty = "";
+
+ const extensions = [
+ "txt",
+ "jS",
+ "JSON",
+ "css",
+ "html",
+ "htm",
+ "xhtml",
+ "xml",
+ "xhtml+xml",
+ "png",
+ "apng",
+ "jPg",
+ "Jpeg",
+ "pdF",
+ "out",
+ "sh",
+ "ExE",
+ "psid",
+ "EXE ",
+ " EXE",
+ "EX\uff65",
+ "\udbff\udbff\udbff",
+ // XXX: Invalid surrogate combos like "\udc00\udc00\udc00" may map to the same names impacting cleanup.
+ "js\udbff",
+ "\udc00js",
+ "???",
+ "\root",
+ empty,
+ "AXS",
+ "dll",
+ "ocx",
+ "1",
+ "ps1",
+ "cmd",
+ "xpi",
+ "swf",
+ ];
+
+ const expectedTypes = [
+ "text/plain",
+ "application/javascript",
+ "application/json",
+ "text/css",
+ "text/html",
+ "text/html",
+ "application/xhtml+xml",
+ "text/xml",
+ empty,
+ "image/png",
+ "image/apng",
+ "image/jpeg",
+ "image/jpeg",
+ "application/pdf",
+ empty,
+ "application/x-sh",
+ "application/octet-stream",
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ empty,
+ "application/olescript",
+ "application/x-msdownload",
+ "application/octet-stream",
+ empty,
+ empty,
+ "text/plain",
+ "application/x-xpinstall",
+ "application/x-shockwave-flash",
+ ];
+
+ Assert.equal(extensions.length, expectedTypes.length);
+
+ await Promise.all(
+ extensions.map(async (ext, i) => {
+ const fileName = nameStem + "." + ext;
+ const fileHandle = await root.getFileHandle(fileName, allowCreate);
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, fileHandle.name);
+ Assert.equal(fileObject.type, expectedTypes[i]);
+ await root.removeEntry(fileName);
+ })
+ );
+};
+
+exported_symbols.testContentTypeChangesOnFileMove = async function () {
+ const allowCreate = { create: true };
+ const root = await navigator.storage.getDirectory();
+ const oldName = "testFile.txt";
+ const oldType = "text/plain";
+ const subdir = await root.getDirectoryHandle("subdir", allowCreate);
+
+ const fileHandle = await root.getFileHandle(oldName, allowCreate);
+
+ async function checkMove(newName, newType) {
+ Assert.equal(fileHandle.name, newName, "Has filename changed?");
+ {
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, newName, "Is the fileobject renamed?");
+ Assert.equal(fileObject.type, newType, "Is the fileobject type updated?");
+ }
+ }
+
+ async function restoreTest() {
+ await fileHandle.move(root, oldName);
+ await checkMove(oldName, oldType);
+ }
+
+ // No name change
+ await checkMove(oldName, oldType);
+ await fileHandle.move(subdir);
+ await checkMove(oldName, oldType);
+ await restoreTest();
+
+ // With name change
+
+ async function testMoveWithParams(testName, testType) {
+ async function testFileMoveCall(...combo) {
+ await fileHandle.move(...combo);
+ await checkMove(testName, testType);
+ await restoreTest();
+ }
+
+ await testFileMoveCall(subdir, testName);
+ await testFileMoveCall(root, testName);
+ await testFileMoveCall(testName);
+ }
+
+ const testParams = {
+ "testFile.json": "application/json",
+ testFile: oldType,
+ "testFile.äüö": "",
+ };
+
+ for (const [aName, aType] of Object.entries(testParams)) {
+ await testMoveWithParams(aName, aType);
+ }
+};
+
+exported_symbols.testContentTypeChangesOnDirMove = async function () {
+ const allowCreate = { create: true };
+ const root = await navigator.storage.getDirectory();
+ const oldName = "testFile.txt";
+ const oldType = "text/plain";
+ const subDirOrig = await root.getDirectoryHandle("subDirOrig", allowCreate);
+ const subDirOther = await root.getDirectoryHandle("subDirOther", allowCreate);
+ const subSubDir = await subDirOrig.getDirectoryHandle(
+ "subSubDir",
+ allowCreate
+ );
+
+ const testName = "testFile.json";
+ const testType = "application/json";
+
+ async function checkMove(newName, newType) {
+ const fileHandle = await subSubDir.getFileHandle(newName, allowCreate);
+
+ Assert.equal(fileHandle.name, newName, "Has filename changed?");
+ {
+ const fileObject = await fileHandle.getFile();
+ Assert.equal(fileObject.name, newName, "Is the fileobject renamed?");
+ Assert.equal(fileObject.type, newType, "Is the fileobject type updated?");
+ }
+ }
+
+ async function restoreTest() {
+ await subSubDir.move(subDirOrig, "subSubDir");
+ await checkMove(oldName, oldType);
+ }
+
+ await checkMove(oldName, oldType);
+
+ // No name change
+ await subSubDir.move(subDirOther, "other");
+ await checkMove(oldName, oldType);
+ await restoreTest();
+
+ // With name change
+
+ async function testDirMoveCall(...combo) {
+ await subSubDir.move(...combo);
+ await checkMove(testName, testType);
+ await restoreTest();
+ }
+
+ await testDirMoveCall(subDirOther);
+ await testDirMoveCall(subDirOther, testName);
+ await testDirMoveCall(subDirOrig, testName);
+ await testDirMoveCall(subDirOrig);
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_fileSystemDirectoryHandle.js b/dom/fs/test/common/test_fileSystemDirectoryHandle.js
new file mode 100644
index 0000000000..9142e47380
--- /dev/null
+++ b/dom/fs/test/common/test_fileSystemDirectoryHandle.js
@@ -0,0 +1,196 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+exported_symbols.smokeTest = async function smokeTest() {
+ const storage = navigator.storage;
+ const subdirectoryNames = new Set(["Documents", "Downloads", "Music"]);
+ const allowCreate = { create: true };
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ let it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let elem = await it.next();
+ Assert.ok(elem.done, "Is root directory empty?");
+
+ for (let dirName of subdirectoryNames) {
+ await root.getDirectoryHandle(dirName, allowCreate);
+ Assert.ok(true, "Was it possible to add subdirectory " + dirName + "?");
+ }
+ }
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we refresh the root directory?");
+
+ let it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let hasElements = false;
+ let hangGuard = 0;
+ for await (let [key, elem] of root.entries()) {
+ Assert.ok(elem, "Is element not non-empty?");
+ Assert.equal("directory", elem.kind, "Is found item a directory?");
+ Assert.ok(
+ elem.name.length >= 1 && elem.name.match("^[A-Za-z]{1,64}"),
+ "Are names of the elements strings?"
+ );
+ Assert.equal(key, elem.name);
+ Assert.ok(subdirectoryNames.has(elem.name), "Is name among known names?");
+ hasElements = true;
+ ++hangGuard;
+ if (hangGuard == 10) {
+ break; // Exit if there is a hang
+ }
+ }
+
+ Assert.ok(hasElements, "Is values container now non-empty?");
+ Assert.equal(3, hangGuard, "Do we only have three elements?");
+
+ {
+ it = await root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+ let elem = await it.next();
+
+ await elem.value.getDirectoryHandle("Trash", allowCreate);
+ let subit = elem.value.values();
+ Assert.ok(!!elem, "Is element not non-empty?");
+ let subdirResult = await subit.next();
+ let subdir = subdirResult.value;
+ Assert.ok(!!subdir, "Is element not non-empty?");
+ Assert.equal("directory", subdir.kind, "Is found item a directory?");
+ Assert.equal("Trash", subdir.name, "Is found item a directory?");
+ }
+
+ const wipeEverything = { recursive: true };
+ for (let dirName of subdirectoryNames) {
+ await root.removeEntry(dirName, wipeEverything);
+ Assert.ok(
+ true,
+ "Was it possible to remove subdirectory " + dirName + "?"
+ );
+ }
+ }
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we refresh the root directory?");
+
+ let it = root.values();
+ Assert.ok(!!it, "Does root have values iterator?");
+
+ let elem = await it.next();
+ Assert.ok(elem.done, "Is root directory empty?");
+ }
+};
+
+exported_symbols.quotaTest = async function () {
+ const storage = navigator.storage;
+ const allowCreate = { create: true };
+
+ {
+ let root = await storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const usageAtStart = await Utils.getCachedOriginUsage();
+ Assert.ok(true, "usageAtStart: " + usageAtStart);
+
+ const writable = await fileHandle.createWritable();
+ Assert.ok(!!writable, "Can we create writable file stream?");
+
+ const usageAtWritableCreated = await Utils.getCachedOriginUsage();
+ Assert.equal(
+ usageAtWritableCreated - usageAtStart,
+ 0,
+ "Did usage increase when writable was created?"
+ );
+
+ const buffer = new ArrayBuffer(42);
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const result = await writable.write(buffer);
+ Assert.equal(result, undefined, "Can we write entire buffer?");
+
+ const usageAtWriteDone = await Utils.getCachedOriginUsage();
+ // Note: Usage should change only on close after 1824305
+ Assert.equal(
+ usageAtWriteDone - usageAtWritableCreated,
+ buffer.byteLength,
+ "Is write immediately reflected in usage?"
+ );
+
+ await writable.close();
+
+ const usageAtWritableClosed = await Utils.getCachedOriginUsage();
+
+ Assert.equal(
+ usageAtWritableClosed - usageAtWritableCreated,
+ buffer.byteLength,
+ "Did usage increase by the amount of bytes written?"
+ );
+
+ await root.removeEntry("test.txt");
+
+ const usageAtFileDeleted = await Utils.getCachedOriginUsage();
+
+ Assert.equal(
+ usageAtFileDeleted,
+ usageAtWritableCreated,
+ "Is usage back to the value before any writing when the file is removed?"
+ );
+ }
+};
+
+exported_symbols.pagedIterationTest = async function () {
+ const root = await navigator.storage.getDirectory();
+
+ for await (let contentItem of root.keys()) {
+ await root.removeEntry(contentItem, { recursive: true });
+ }
+
+ const allowCreate = { create: true };
+
+ // When half of the buffer is iterated, a request for the second half is sent.
+ // We test that the this boundary is crossed smoothly.
+ // After the buffer is filled, a request for more items is sent. The
+ // items are placed in the first half of the buffer.
+ // This boundary should also be crossed without problems.
+ // Currently, the buffer is half-filled at 1024.
+ const itemBatch = 3 + 2 * 1024;
+ for (let i = 0; i <= itemBatch; ++i) {
+ await root.getDirectoryHandle("" + i, allowCreate);
+ }
+
+ let result = 0;
+ let sum = 0;
+ const handles = new Set();
+ let isUnique = true;
+ for await (let [key, elem] of root.entries()) {
+ result += key.length;
+ sum += parseInt(elem.name);
+ if (handles.has(key)) {
+ // Asserting here is slow and verbose
+ isUnique = false;
+ break;
+ }
+ handles.add(key);
+ }
+ Assert.ok(isUnique);
+ Assert.equal(result, 7098);
+ Assert.equal(sum, (itemBatch * (itemBatch + 1)) / 2);
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_syncAccessHandle.js b/dom/fs/test/common/test_syncAccessHandle.js
new file mode 100644
index 0000000000..ac7e0ef769
--- /dev/null
+++ b/dom/fs/test/common/test_syncAccessHandle.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const allowCreate = { create: true };
+
+exported_symbols.test0 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ try {
+ await root.getFileHandle("test.txt");
+ Assert.ok(false, "Opened file that shouldn't exist");
+ } catch (e) {
+ dump("caught exception when we tried to open a non-existant file\n");
+ }
+};
+
+exported_symbols.test1 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+ await handle.close();
+ handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create second SyncAccessHandle to same file");
+ await handle.close();
+};
+
+exported_symbols.test2 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't open file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+ await handle.close();
+
+ await root.removeEntry("test.txt");
+ try {
+ handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Didn't remove file!");
+ if (handle) {
+ await handle.close();
+ }
+ } catch (e) {
+ dump("Caught exception trying to create accesshandle to deleted file\n");
+ }
+};
+
+exported_symbols.test3 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ let dir = await root.getDirectoryHandle("dir", allowCreate);
+ Assert.ok(!!dir, "Can we create a directory?");
+
+ // XXX not implemented yet
+ //const path = await root.resolve(dir);
+ //Assert.ok(path == ["dir"], "Wrong path: " + path);
+
+ let dir2 = await dir.getDirectoryHandle("dir", allowCreate);
+ Assert.ok(!!dir, "Can we create dir/dir?");
+
+ // XXX not implemented yet
+ //const path = await root.resolve(dir2);
+ //Assert.ok(path == ["dir", "dir"], "Wrong path: " + path);
+
+ let dir3 = await dir.getDirectoryHandle("bar", allowCreate);
+ Assert.ok(!!dir3, "Can we create dir/bar?");
+
+ // This should fail
+ try {
+ await root.getDirectoryHandle("bar");
+ Assert.ok(!dir, "we shouldn't be able to get bar unless we create it");
+ } catch (e) {
+ dump("caught exception when we tried to get a non-existant dir\n");
+ }
+
+ const testFile = await dir2.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file in dir2");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle in dir2");
+ await handle.close();
+};
+
+exported_symbols.test4 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't access existing file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle to existing file");
+
+ // Write a sentence to the end of the file.
+ const encoder = new TextEncoder();
+ const writeBuffer = encoder.encode("Thank you for reading this.");
+ const writeSize = handle.write(writeBuffer);
+ Assert.ok(!!writeSize);
+
+ // Read it back
+ // Get size of the file.
+ let fileSize = await handle.getSize();
+ Assert.ok(fileSize == writeBuffer.byteLength);
+ // Read file content to a buffer.
+ const readBuffer = new ArrayBuffer(fileSize);
+ const readSize = handle.read(readBuffer, { at: 0 });
+ Assert.ok(!!readSize);
+ //Assert.ok(readBuffer == writeBuffer);
+
+ await handle.truncate(5);
+ fileSize = await handle.getSize();
+ Assert.ok(fileSize == 5);
+
+ await handle.flush();
+ await handle.close();
+};
+
+exported_symbols.test5 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't create file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+
+ try {
+ const testFile2 = await root.getFileHandle("test2.txt", allowCreate);
+ let handle2 = await testFile2.createSyncAccessHandle();
+ Assert.ok(!!handle2, "can't create SyncAccessHandle to second file!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ Assert.ok(false, "Failed to create second file");
+ }
+
+ await handle.close();
+};
+
+exported_symbols.test6 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't get file");
+ let handle = await testFile.createSyncAccessHandle();
+ Assert.ok(!!handle, "Can't create SyncAccessHandle");
+
+ try {
+ let handle2 = await testFile.createSyncAccessHandle();
+ Assert.ok(!handle2, "Shouldn't create SyncAccessHandle!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ // should always happen
+ dump("caught exception when we tried to get 2 SyncAccessHandles\n");
+ }
+
+ // test that locks work across multiple connections for an origin
+ try {
+ let root2 = await navigator.storage.getDirectory();
+ Assert.ok(root2, "Can we access the root2 directory?");
+
+ const testFile2 = await root2.getFileHandle("test.txt");
+ Assert.ok(!!testFile2, "Can't get file");
+ let handle2 = await testFile2.createSyncAccessHandle();
+ Assert.ok(!handle2, "Shouldn't create SyncAccessHandle (2)!");
+ if (handle2) {
+ await handle2.close();
+ }
+ } catch (e) {
+ // should always happen
+ dump("caught exception when we tried to get 2 SyncAccessHandles\n");
+ }
+
+ if (handle) {
+ await handle.close();
+ }
+};
+
+exported_symbols.quotaTest = async function () {
+ const shrinkedStorageSizeKB = 5 * 1024;
+ const defaultDatabaseSize = 491520;
+
+ // Shrink storage size to 5MB.
+ await Utils.shrinkStorageSize(shrinkedStorageSizeKB);
+
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ // Fill entire storage.
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const accessHandle = await fileHandle.createSyncAccessHandle();
+ Assert.ok(!!accessHandle, "Can we create sync access handle?");
+
+ const buffer = new ArrayBuffer(
+ shrinkedStorageSizeKB * 1024 - defaultDatabaseSize
+ );
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const written = accessHandle.write(buffer);
+ Assert.equal(written, buffer.byteLength, "Can we write entire buffer?");
+
+ // Try to write one more byte.
+ const fileHandle2 = await root.getFileHandle("test2.txt", allowCreate);
+ Assert.ok(!!fileHandle2, "Can we get file handle?");
+
+ const accessHandle2 = await fileHandle2.createSyncAccessHandle();
+ Assert.ok(!!accessHandle2, "Can we create sync access handle?");
+
+ const buffer2 = new ArrayBuffer(1);
+ Assert.ok(!!buffer2, "Can we create array buffer?");
+
+ const written2 = accessHandle2.write(buffer2);
+ Assert.equal(written2, 0, "Can we write beyond the limit?");
+
+ await accessHandle.close();
+ await accessHandle2.close();
+
+ await Utils.restoreStorageSize();
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/test_writableFileStream.js b/dom/fs/test/common/test_writableFileStream.js
new file mode 100644
index 0000000000..016c53bf3b
--- /dev/null
+++ b/dom/fs/test/common/test_writableFileStream.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const allowCreate = { create: true };
+
+exported_symbols.test0 = async function () {
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(!!root, "Can we access the root directory?");
+
+ const testFile = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!testFile, "Can't access existing file");
+ let writable = await testFile.createWritable();
+ Assert.ok(!!writable, "Can't create WritableFileStream to existing file");
+
+ // Write a sentence to the end of the file.
+ const encoder = new TextEncoder();
+ const writeBuffer = encoder.encode("Thank you for reading this.");
+ try {
+ dump("Trying to write...\n");
+ await writable.write(writeBuffer);
+ dump("closing...\n");
+ await writable.close();
+ } catch (e) {
+ Assert.ok(false, "Couldn't write to WritableFileStream: " + e);
+ }
+
+ // Read it back
+ // Get size of the file.
+ let file = await testFile.getFile();
+ Assert.ok(
+ !!file,
+ "Can't create File to file written with WritableFileStream"
+ );
+ let fileSize = file.size;
+ Assert.ok(fileSize == writeBuffer.byteLength);
+};
+
+exported_symbols.quotaTest = async function () {
+ const shrinkedStorageSizeKB = 5 * 1024;
+ const defaultDatabaseSize = 491547;
+
+ // Shrink storage size to 5MB.
+ await Utils.shrinkStorageSize(shrinkedStorageSizeKB);
+
+ let root = await navigator.storage.getDirectory();
+ Assert.ok(root, "Can we access the root directory?");
+
+ // Fill entire storage.
+ const fileHandle = await root.getFileHandle("test.txt", allowCreate);
+ Assert.ok(!!fileHandle, "Can we get file handle?");
+
+ const writable = await fileHandle.createWritable();
+ Assert.ok(!!writable, "Can we create writable file stream?");
+
+ const buffer = new ArrayBuffer(
+ shrinkedStorageSizeKB * 1024 - defaultDatabaseSize
+ );
+ Assert.ok(!!buffer, "Can we create array buffer?");
+
+ const result = await writable.write(buffer);
+ Assert.equal(result, undefined, "Can we write entire buffer?");
+
+ // Try to write one more byte.
+ const fileHandle2 = await root.getFileHandle("test2.txt", allowCreate);
+ Assert.ok(!!fileHandle2, "Can we get file handle?");
+
+ const writable2 = await fileHandle2.createWritable();
+ Assert.ok(!!writable2, "Can we create writable file stream?");
+
+ const buffer2 = new ArrayBuffer(1);
+ Assert.ok(!!buffer2, "Can we create array buffer?");
+
+ try {
+ await writable2.write(buffer2);
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(true, "Did throw");
+ Assert.ok(DOMException.isInstance(ex), "Threw DOMException");
+ Assert.equal(ex.name, "QuotaExceededError", "Threw right DOMException");
+ }
+
+ await writable.close();
+ // writable2 is already closed because of the failed write above
+
+ await Utils.restoreStorageSize();
+};
+
+exported_symbols.bug1823445 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1823445.txt";
+ let handle = await root.getFileHandle(testFileName, allowCreate);
+ let writable = await handle.createWritable();
+ await writable.write("abcdefghijklmnop");
+ await writable.close();
+
+ handle = await root.getFileHandle(testFileName);
+ writable = await handle.createWritable({ keepExistingData: false });
+ await writable.write("12345");
+ await writable.close();
+
+ handle = await root.getFileHandle(testFileName);
+ const file = await handle.getFile();
+ const text = await file.text();
+ Assert.equal(text, "12345");
+};
+
+exported_symbols.bug1824993 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1824993.txt";
+ const handle = await root.getFileHandle(testFileName, allowCreate);
+ {
+ const writable = await handle.createWritable();
+ await writable.write("test");
+
+ {
+ const file = await handle.getFile();
+ const contents = await file.text();
+ Assert.equal(contents, "");
+ }
+
+ await writable.abort();
+ }
+
+ const file = await handle.getFile();
+ const contents = await file.text();
+ Assert.equal(contents, "");
+};
+
+exported_symbols.bug1825018 = async function () {
+ const root = await navigator.storage.getDirectory();
+ const testFileName = "test1825018.txt";
+ const handle = await root.getFileHandle(testFileName, allowCreate);
+ const writable = await handle.createWritable();
+ try {
+ await writable.write({ type: "truncate" });
+ } catch (e) {
+ // Called write without size throws an error as expected
+ }
+
+ try {
+ await writable.abort();
+ await root.removeEntry(testFileName);
+ } catch (e) {
+ Assert.ok(false, e.message);
+ }
+};
+
+for (const [key, value] of Object.entries(exported_symbols)) {
+ Object.defineProperty(value, "name", {
+ value: key,
+ writable: false,
+ });
+}
diff --git a/dom/fs/test/common/xpcshell.toml b/dom/fs/test/common/xpcshell.toml
new file mode 100644
index 0000000000..9f086b65fe
--- /dev/null
+++ b/dom/fs/test/common/xpcshell.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = [
+ "nsresult.js",
+ "test_basics.js",
+ "test_fileSystemDirectoryHandle.js",
+ "test_writableFileStream.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/crashtests/1798773.html b/dom/fs/test/crashtests/1798773.html
new file mode 100644
index 0000000000..893dfcfc59
--- /dev/null
+++ b/dom/fs/test/crashtests/1798773.html
@@ -0,0 +1,19 @@
+<script id="worker1" type="javascript/worker">
+self.onmessage = async function () {
+ const xhr = new XMLHttpRequest()
+ self.onerror = () => {
+ xhr.open("POST", "FOOBAR", false)
+ xhr.send()
+ }
+ self.reportError(undefined)
+ self.dir = await self.navigator.storage.getDirectory()
+}
+</script>
+<script>
+window.addEventListener('load', async () => {
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: "text/javascript" })
+ let worker = new Worker(window.URL.createObjectURL(blob))
+ worker.postMessage([], [])
+ setTimeout(() => {window.location.reload(true)})
+})
+</script>
diff --git a/dom/fs/test/crashtests/1800470.html b/dom/fs/test/crashtests/1800470.html
new file mode 100644
index 0000000000..a7d5dfa8bb
--- /dev/null
+++ b/dom/fs/test/crashtests/1800470.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script id="worker1" type="javascript/worker">
+ self.onmessage = async function (e) {
+ const directory = await navigator.storage.getDirectory();
+ const file = await directory.getFileHandle("500014c3-f683-4551-bb26-08025c9be332", {
+ create: true,
+ });
+ const stream = await file.createWritable({});
+ const regex = new RegExp(".*");
+ await stream.abort(regex);
+ self.postMessage("done");
+ self.close();
+ }
+ </script>
+ <script>
+ var worker;
+ document.addEventListener('DOMContentLoaded', () => {
+ const buffer = new ArrayBuffer(1);
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: 'text/javascript' });
+ worker = new Worker(window.URL.createObjectURL(blob));
+ worker.postMessage([buffer], [buffer]);
+ worker.onmessage = function() {document.documentElement.removeAttribute("class"); }
+ });
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/1809759.html b/dom/fs/test/crashtests/1809759.html
new file mode 100644
index 0000000000..b9df8de02f
--- /dev/null
+++ b/dom/fs/test/crashtests/1809759.html
@@ -0,0 +1,10 @@
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ document.location.search = '?'
+ let a = self.navigator.storage
+ let xhr = new XMLHttpRequest()
+ xhr.open('POST', 'FOOBAR', false)
+ xhr.send()
+ await a.getDirectory()
+})
+</script>
diff --git a/dom/fs/test/crashtests/1816710.html b/dom/fs/test/crashtests/1816710.html
new file mode 100644
index 0000000000..f7641e009d
--- /dev/null
+++ b/dom/fs/test/crashtests/1816710.html
@@ -0,0 +1,8 @@
+<script>
+ window.addEventListener('load', async () => {
+ const dir = await navigator.storage.getDirectory();
+ const file = await dir.getFileHandle('555b8afb-96ac-4fe3-8cec', { create: true });
+ const writable = await file.createWritable({});
+ setTimeout('self.close()', 2000)
+ })
+</script>
diff --git a/dom/fs/test/crashtests/1841702.html b/dom/fs/test/crashtests/1841702.html
new file mode 100644
index 0000000000..0509972ae8
--- /dev/null
+++ b/dom/fs/test/crashtests/1841702.html
@@ -0,0 +1,16 @@
+<script id="worker1" type="javascript/worker">
+self.onmessage = async function(e) {
+ let a = await e.data[0].getFileHandle("c21deba4-fb73-4407-94f8-2e3782bf3f23", {"create": true})
+ self.close()
+ await a.createWritable({})
+}
+</script>
+
+<script>
+window.addEventListener("load", async () => {
+ let a = await self.clientInformation.storage.getDirectory()
+ const blob = new Blob([document.querySelector('#worker1').textContent], { type: "text/javascript" })
+ let worker = new Worker(window.URL.createObjectURL(blob))
+ worker.postMessage([a], [])
+})
+</script>
diff --git a/dom/fs/test/crashtests/1844619.html b/dom/fs/test/crashtests/1844619.html
new file mode 100644
index 0000000000..43a85e94d9
--- /dev/null
+++ b/dom/fs/test/crashtests/1844619.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener('load', async () => {
+ await navigator.serviceWorker.register('sw1844619.js?1619678955', {})
+ await navigator.serviceWorker.register('sw1844619.js?4246054133', {})
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/1858820.html b/dom/fs/test/crashtests/1858820.html
new file mode 100644
index 0000000000..ad758b96e0
--- /dev/null
+++ b/dom/fs/test/crashtests/1858820.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <script>
+ window.addEventListener("load", async () => {
+ let a = document.createElement("iframe")
+ document.documentElement.appendChild(a)
+ let b = await a.contentWindow.clientInformation.storage.getDirectory()
+ let c = await b.getFileHandle("80e2d2c3-0712-4ccd-94b5-e2dd1732ea09", {"create": true})
+ let d = await c.createWritable({ })
+ setTimeout(async () => {
+ document.documentElement.removeAttribute("class");
+ await d.truncate(1);
+ }, 1000)
+ document.replaceChildren(document.documentElement, document.documentElement)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/fs/test/crashtests/crashtests.list b/dom/fs/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..a083cb80b2
--- /dev/null
+++ b/dom/fs/test/crashtests/crashtests.list
@@ -0,0 +1,10 @@
+# StorageManager isn't enabled on Android
+defaults skip-if(Android) pref(dom.fs.enabled,true) pref(dom.fs.writable_file_stream.enabled,true)
+
+load 1798773.html
+load 1800470.html
+load 1809759.html
+load 1816710.html
+load 1841702.html
+HTTP load 1844619.html
+HTTP load 1858820.html
diff --git a/dom/fs/test/crashtests/sw1844619.js b/dom/fs/test/crashtests/sw1844619.js
new file mode 100644
index 0000000000..dd221844f2
--- /dev/null
+++ b/dom/fs/test/crashtests/sw1844619.js
@@ -0,0 +1,21 @@
+async function timeout (cmd) {
+ const timer = new Promise((resolve, reject) => {
+ const id = setTimeout(() => {
+ clearTimeout(id)
+ reject(new Error('Promise timed out!'))
+ }, 750)
+ })
+ return Promise.race([cmd, timer])
+}
+
+(async () => {
+ const root = await navigator.storage.getDirectory()
+ const blob = new Blob(['A'])
+ const sub = await root.getDirectoryHandle('a', { 'create': true })
+ const file = await root.getFileHandle('b', { 'create': true })
+ await file.move(sub)
+ const stream = await file.createWritable({})
+ await stream.write(blob)
+ const sub2 = await root.getDirectoryHandle('a', {})
+ await sub2.move(root, 'X')
+})()
diff --git a/dom/fs/test/gtest/FileSystemMocks.cpp b/dom/fs/test/gtest/FileSystemMocks.cpp
new file mode 100644
index 0000000000..1ad8e77c56
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemMocks.h"
+
+#include <string>
+
+#include "ErrorList.h"
+#include "gtest/gtest-assertion-result.h"
+#include "js/RootingAPI.h"
+#include "jsapi.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "nsContentUtils.h"
+#include "nsISupports.h"
+
+namespace mozilla::dom::fs::test {
+
+nsIGlobalObject* GetGlobal() {
+ AutoJSAPI jsapi;
+ DebugOnly<bool> ok = jsapi.Init(xpc::PrivilegedJunkScope());
+ MOZ_ASSERT(ok);
+
+ JSContext* cx = jsapi.cx();
+ mozilla::dom::GlobalObject globalObject(cx, JS::CurrentGlobalOrNull(cx));
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(globalObject.GetAsSupports());
+ MOZ_ASSERT(global);
+
+ return global.get();
+}
+
+nsresult GetAsString(const RefPtr<Promise>& aPromise, nsAString& aString) {
+ AutoJSAPI jsapi;
+ DebugOnly<bool> ok = jsapi.Init(xpc::PrivilegedJunkScope());
+ MOZ_ASSERT(ok);
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> promiseObj(cx, aPromise->PromiseObj());
+ JS::Rooted<JS::Value> vp(cx, JS::GetPromiseResult(promiseObj));
+
+ switch (aPromise->State()) {
+ case Promise::PromiseState::Pending: {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ case Promise::PromiseState::Resolved: {
+ if (nsContentUtils::StringifyJSON(cx, vp, aString,
+ UndefinedIsNullStringLiteral)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ case Promise::PromiseState::Rejected: {
+ if (vp.isInt32()) {
+ int32_t errorCode = vp.toInt32();
+ aString.AppendInt(errorCode);
+
+ return NS_OK;
+ }
+
+ if (!vp.isObject()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Exception> exception;
+ UNWRAP_OBJECT(Exception, &vp, exception);
+ if (!exception) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ aString.Append(NS_ConvertUTF8toUTF16(
+ GetStaticErrorName(static_cast<nsresult>(exception->Result()))));
+
+ return NS_OK;
+ }
+
+ default:
+ break;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+mozilla::ipc::PrincipalInfo GetPrincipalInfo() {
+ return mozilla::ipc::PrincipalInfo{mozilla::ipc::SystemPrincipalInfo{}};
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/FileSystemMocks.h b/dom/fs/test/gtest/FileSystemMocks.h
new file mode 100644
index 0000000000..1926b2e86a
--- /dev/null
+++ b/dom/fs/test/gtest/FileSystemMocks.h
@@ -0,0 +1,340 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
+#define DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
+
+#include <memory> // We don't have a mozilla shared pointer for pod types
+
+#include "TestHelpers.h"
+#include "fs/FileSystemChildFactory.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "js/Promise.h"
+#include "js/RootingAPI.h"
+#include "jsapi.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsIGlobalObject.h"
+#include "nsISupports.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla::dom::fs {
+
+inline std::ostream& operator<<(std::ostream& aOut,
+ const FileSystemEntryMetadata& aMetadata) {
+ return aOut;
+}
+
+namespace test {
+
+nsIGlobalObject* GetGlobal();
+
+nsresult GetAsString(const RefPtr<Promise>& aPromise, nsAString& aString);
+
+mozilla::ipc::PrincipalInfo GetPrincipalInfo();
+
+class MockFileSystemRequestHandler : public FileSystemRequestHandler {
+ public:
+ MOCK_METHOD(void, GetRootHandle,
+ (RefPtr<FileSystemManager> aManager, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetDirectoryHandle,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aDirectory, bool aCreate,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetFileHandle,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aFile, bool aCreate,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetFile,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemEntryMetadata& aFile, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, GetEntries,
+ (RefPtr<FileSystemManager> & aManager, const EntryId& aDirectory,
+ PageNumber aPage, RefPtr<Promise> aPromise,
+ RefPtr<FileSystemEntryMetadataArray>& aSink,
+ ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, RemoveEntry,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemChildMetadata& aEntry, bool aRecursive,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, MoveEntry,
+ (RefPtr<FileSystemManager> & aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry,
+ const FileSystemChildMetadata& aNewEntry,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, RenameEntry,
+ (RefPtr<FileSystemManager> & aManager, FileSystemHandle* aHandle,
+ FileSystemEntryMetadata* const aEntry, const Name& aName,
+ RefPtr<Promise> aPromise, ErrorResult& aError),
+ (override));
+
+ MOCK_METHOD(void, Resolve,
+ (RefPtr<FileSystemManager> & aManager,
+ const FileSystemEntryPair& aEndpoints, RefPtr<Promise> aPromise,
+ ErrorResult& aError),
+ (override));
+};
+
+class WaitablePromiseListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void ClearDone() = 0;
+
+ virtual bool IsDone() const = 0;
+
+ virtual PromiseNativeHandler* AsHandler() = 0;
+
+ protected:
+ virtual ~WaitablePromiseListener() = default;
+};
+
+template <class SuccessHandler, class ErrorHandler,
+ uint32_t MilliSeconds = 2000u>
+class TestPromiseListener : public PromiseNativeHandler,
+ public WaitablePromiseListener {
+ public:
+ TestPromiseListener()
+ : mIsDone(std::make_shared<bool>(false)), mOnSuccess(), mOnError() {
+ ClearDone();
+ }
+
+ // nsISupports implementation
+
+ NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ NS_INTERFACE_TABLE0(TestPromiseListener)
+
+ return rv;
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestPromiseListener, override)
+
+ // PromiseNativeHandler implementation
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
+ timer->Cancel();
+ *isDone = true;
+ });
+
+ mOnSuccess();
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aError) override {
+ mozilla::ScopeExit flagAsDone([isDone = mIsDone, timer = mTimer] {
+ timer->Cancel();
+ *isDone = true;
+ });
+
+ if (aValue.isInt32()) {
+ mOnError(static_cast<nsresult>(aValue.toInt32()));
+ return;
+ }
+
+ ASSERT_TRUE(aValue.isObject());
+ JS::Rooted<JSObject*> exceptionObject(aCx, &aValue.toObject());
+
+ RefPtr<Exception> exception;
+ UNWRAP_OBJECT(Exception, exceptionObject, exception);
+ if (exception) {
+ mOnError(static_cast<nsresult>(exception->Result()));
+ return;
+ }
+ }
+
+ // WaitablePromiseListener implementation
+
+ void ClearDone() override {
+ *mIsDone = false;
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ auto timerCallback = [isDone = mIsDone](nsITimer* aTimer) {
+ *isDone = true;
+ FAIL() << "Timed out!";
+ };
+ const char* timerName = "fs::TestPromiseListener::ClearDone";
+ auto res = NS_NewTimerWithCallback(timerCallback, MilliSeconds,
+ nsITimer::TYPE_ONE_SHOT, timerName);
+ if (res.isOk()) {
+ mTimer = res.unwrap();
+ }
+ }
+
+ bool IsDone() const override { return *mIsDone; }
+
+ PromiseNativeHandler* AsHandler() override { return this; }
+
+ SuccessHandler& GetSuccessHandler() { return mOnSuccess; }
+
+ SuccessHandler& GetErrorHandler() { return mOnError; }
+
+ protected:
+ virtual ~TestPromiseListener() = default;
+
+ std::shared_ptr<bool> mIsDone; // We pass this to a callback
+
+ nsCOMPtr<nsITimer> mTimer;
+
+ SuccessHandler mOnSuccess;
+
+ ErrorHandler mOnError;
+};
+
+class TestFileSystemManagerChild : public FileSystemManagerChild {
+ public:
+ MOCK_METHOD(void, SendGetRootHandle,
+ (mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse> &&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetDirectoryHandle,
+ (const FileSystemGetHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetFileHandle,
+ (const FileSystemGetHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetHandleResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetAccessHandle,
+ (const FileSystemGetAccessHandleRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetAccessHandleResponse>&&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetWritable,
+ (const FileSystemGetWritableRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetWritableFileStreamResponse>&&
+ aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetFile,
+ (const FileSystemGetFileRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetFileResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendResolve,
+ (const FileSystemResolveRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemResolveResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendGetEntries,
+ (const FileSystemGetEntriesRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemGetEntriesResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(
+ void, SendRemoveEntry,
+ (const FileSystemRemoveEntryRequest& request,
+ mozilla::ipc::ResolveCallback<FileSystemRemoveEntryResponse>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject),
+ (override));
+
+ MOCK_METHOD(void, Shutdown, (), (override));
+
+ protected:
+ virtual ~TestFileSystemManagerChild() = default;
+};
+
+class TestFileSystemChildFactory final : public FileSystemChildFactory {
+ public:
+ explicit TestFileSystemChildFactory(TestFileSystemManagerChild* aChild)
+ : mChild(aChild) {}
+
+ already_AddRefed<FileSystemManagerChild> Create() const override {
+ return RefPtr<TestFileSystemManagerChild>(mChild).forget();
+ }
+
+ ~TestFileSystemChildFactory() = default;
+
+ private:
+ TestFileSystemManagerChild* mChild;
+};
+
+struct MockExpectMe {
+ MOCK_METHOD0(InvokeMe, void());
+
+ template <class... Args>
+ void operator()(Args...) {
+ InvokeMe();
+ }
+};
+
+template <nsresult Expected>
+struct NSErrorMatcher {
+ void operator()(nsresult aErr) { ASSERT_NSEQ(Expected, aErr); }
+};
+
+struct FailOnCall {
+ template <class... Args>
+ void operator()(Args...) {
+ FAIL();
+ }
+};
+
+} // namespace test
+} // namespace mozilla::dom::fs
+
+#define MOCK_PROMISE_LISTENER(name, ...) \
+ using name = mozilla::dom::fs::test::TestPromiseListener<__VA_ARGS__>;
+
+MOCK_PROMISE_LISTENER(
+ ExpectNotImplemented, mozilla::dom::fs::test::FailOnCall,
+ mozilla::dom::fs::test::NSErrorMatcher<NS_ERROR_NOT_IMPLEMENTED>);
+
+MOCK_PROMISE_LISTENER(ExpectResolveCalled, mozilla::dom::fs::test::MockExpectMe,
+ mozilla::dom::fs::test::FailOnCall);
+
+#endif // DOM_FS_TEST_GTEST_FILESYSTEMMOCKS_H_
diff --git a/dom/fs/test/gtest/TestHelpers.cpp b/dom/fs/test/gtest/TestHelpers.cpp
new file mode 100644
index 0000000000..b028530f19
--- /dev/null
+++ b/dom/fs/test/gtest/TestHelpers.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TestHelpers.h"
+
+#include "gtest/gtest.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "nsString.h"
+
+namespace testing::internal {
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsAString& s1,
+ const nsAString& s2) {
+ if (s1.Equals(s2)) {
+ return ::testing::AssertionSuccess();
+ }
+
+ return ::testing::internal::EqFailure(
+ s1_expression, s2_expression,
+ std::string(NS_ConvertUTF16toUTF8(s1).get()),
+ std::string(NS_ConvertUTF16toUTF8(s2).get()),
+ /* ignore case */ false);
+}
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsACString& s1,
+ const nsACString& s2) {
+ if (s1.Equals(s2)) {
+ return ::testing::AssertionSuccess();
+ }
+
+ return ::testing::internal::EqFailure(s1_expression, s2_expression,
+ std::string(s1), std::string(s2),
+ /* ignore case */ false);
+}
+
+} // namespace testing::internal
+
+namespace mozilla::dom::fs::test {
+
+quota::OriginMetadata GetTestOriginMetadata() {
+ return quota::OriginMetadata{""_ns,
+ "example.com"_ns,
+ "http://example.com"_ns,
+ "http://example.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+const Origin& GetTestOrigin() {
+ static const Origin origin = "http://example.com"_ns;
+ return origin;
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/TestHelpers.h b/dom/fs/test/gtest/TestHelpers.h
new file mode 100644
index 0000000000..bfbcb9840c
--- /dev/null
+++ b/dom/fs/test/gtest/TestHelpers.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_FS_TEST_GTEST_TESTHELPERS_H_
+#define DOM_FS_TEST_GTEST_TESTHELPERS_H_
+
+#include "ErrorList.h"
+#include "gtest/gtest.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+namespace testing::internal {
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsAString& s1,
+ const nsAString& s2);
+
+GTEST_API_ ::testing::AssertionResult CmpHelperSTREQ(const char* s1_expression,
+ const char* s2_expression,
+ const nsACString& s1,
+ const nsACString& s2);
+
+} // namespace testing::internal
+
+#define ASSERT_NSEQ(lhs, rhs) \
+ ASSERT_STREQ(GetStaticErrorName((lhs)), GetStaticErrorName((rhs)))
+
+#define TEST_TRY_UNWRAP_META(tempVar, target, expr) \
+ auto MOZ_REMOVE_PAREN(tempVar) = (expr); \
+ ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isOk()) \
+ << GetStaticErrorName( \
+ mozilla::ToNSResult(MOZ_REMOVE_PAREN(tempVar).unwrapErr())); \
+ MOZ_REMOVE_PAREN(target) = MOZ_REMOVE_PAREN(tempVar).unwrap();
+
+#define TEST_TRY_UNWRAP_ERR_META(tempVar, target, expr) \
+ auto MOZ_REMOVE_PAREN(tempVar) = (expr); \
+ ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isErr()); \
+ MOZ_REMOVE_PAREN(target) = \
+ mozilla::ToNSResult(MOZ_REMOVE_PAREN(tempVar).unwrapErr());
+
+#define TEST_TRY_UNWRAP(target, expr) \
+ TEST_TRY_UNWRAP_META(MOZ_UNIQUE_VAR(testVar), target, expr)
+
+#define TEST_TRY_UNWRAP_ERR(target, expr) \
+ TEST_TRY_UNWRAP_ERR_META(MOZ_UNIQUE_VAR(testVar), target, expr)
+
+namespace mozilla::dom {
+
+namespace quota {
+
+struct OriginMetadata;
+
+} // namespace quota
+
+namespace fs::test {
+
+quota::OriginMetadata GetTestOriginMetadata();
+
+const Origin& GetTestOrigin();
+
+} // namespace fs::test
+} // namespace mozilla::dom
+
+#endif // DOM_FS_TEST_GTEST_TESTHELPERS_H_
diff --git a/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp
new file mode 100644
index 0000000000..6aadd226e9
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemMocks.h"
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemDirectoryHandleBinding.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+using ::testing::_;
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemDirectoryHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mRequestHandler = MakeUnique<MockFileSystemRequestHandler>();
+ mMetadata = FileSystemEntryMetadata("dir"_ns, u"Directory"_ns,
+ /* directory */ true);
+ mName = u"testDir"_ns;
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ const IterableIteratorBase::IteratorType mIteratorType =
+ IterableIteratorBase::IteratorType::Keys;
+ UniquePtr<MockFileSystemRequestHandler> mRequestHandler;
+ FileSystemEntryMetadata mMetadata;
+ nsString mName;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemDirectoryHandle, constructDirectoryHandleRefPointer) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+}
+
+TEST_F(TestFileSystemDirectoryHandle, initIterator) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ RefPtr<FileSystemDirectoryHandle::iterator_t> iterator =
+ new FileSystemDirectoryHandle::iterator_t(dirHandle.get(), mIteratorType);
+ IgnoredErrorResult rv;
+ dirHandle->InitAsyncIteratorData(iterator->Data(), mIteratorType, rv);
+ ASSERT_TRUE(iterator->Data().mImpl);
+}
+
+class MockFileSystemDirectoryIteratorImpl final
+ : public FileSystemDirectoryIterator::Impl {
+ public:
+ MOCK_METHOD(already_AddRefed<Promise>, Next,
+ (nsIGlobalObject * aGlobal, RefPtr<FileSystemManager>& aManager,
+ ErrorResult& aError),
+ (override));
+};
+
+TEST_F(TestFileSystemDirectoryHandle, isNextPromiseReturned) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ auto mockIter = MakeRefPtr<MockFileSystemDirectoryIteratorImpl>();
+ IgnoredErrorResult error;
+ EXPECT_CALL(*mockIter, Next(_, _, _))
+ .WillOnce(::testing::Return(Promise::Create(mGlobal, error)));
+
+ RefPtr<FileSystemDirectoryHandle::iterator_t> iterator =
+ MakeAndAddRef<FileSystemDirectoryHandle::iterator_t>(dirHandle.get(),
+ mIteratorType);
+ iterator->Data().mImpl = std::move(mockIter);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise =
+ dirHandle->GetNextIterationResult(iterator.get(), rv);
+ ASSERT_TRUE(promise);
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isHandleKindDirectory) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ ASSERT_EQ(FileSystemHandleKind::Directory, dirHandle->Kind());
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isFileHandleReturned) {
+ EXPECT_CALL(*mRequestHandler, GetFileHandle(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetFileOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetFileHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesGetFileHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetFileOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetFileHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isDirectoryHandleReturned) {
+ EXPECT_CALL(*mRequestHandler, GetDirectoryHandle(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetDirectoryOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetDirectoryHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesGetDirectoryHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemGetDirectoryOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->GetDirectoryHandle(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isRemoveEntrySuccessful) {
+ EXPECT_CALL(*mRequestHandler, RemoveEntry(_, _, _, _, _))
+ .WillOnce(::testing::ReturnArg<3>());
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemRemoveOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->RemoveEntry(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesRemoveEntryFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ FileSystemRemoveOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->RemoveEntry(mName, options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, isResolveSuccessful) {
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata,
+ mRequestHandler.release());
+
+ ASSERT_TRUE(dirHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->Resolve(*dirHandle, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemDirectoryHandle, doesResolveFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemDirectoryHandle> dirHandle =
+ MakeAndAddRef<FileSystemDirectoryHandle>(mGlobal, mManager, mMetadata);
+
+ ASSERT_TRUE(dirHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->Resolve(*dirHandle, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp
new file mode 100644
index 0000000000..263c1f2ed1
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemFileHandle.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemMocks.h"
+#include "fs/FileSystemChildFactory.h"
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemFileHandleBinding.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemFileHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mRequestHandler = MakeUnique<MockFileSystemRequestHandler>();
+ mMetadata =
+ FileSystemEntryMetadata("file"_ns, u"File"_ns, /* directory */ false);
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ UniquePtr<MockFileSystemRequestHandler> mRequestHandler;
+ FileSystemEntryMetadata mMetadata;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemFileHandle, constructFileHandleRefPointer) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+}
+
+TEST_F(TestFileSystemFileHandle, isHandleKindFile) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ ASSERT_EQ(FileSystemHandleKind::File, fileHandle->Kind());
+}
+
+TEST_F(TestFileSystemFileHandle, isFileReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->GetFile(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesGetFileFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->GetFile(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemFileHandle, isWritableReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ FileSystemCreateWritableOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateWritable(options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesCreateWritableFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ FileSystemCreateWritableOptions options;
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateWritable(options, rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+TEST_F(TestFileSystemFileHandle, isSyncAccessHandleReturned) {
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateSyncAccessHandle(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+}
+
+TEST_F(TestFileSystemFileHandle, doesCreateSyncAccessHandleFailOnNullGlobal) {
+ mGlobal = nullptr;
+ RefPtr<FileSystemFileHandle> fileHandle = MakeAndAddRef<FileSystemFileHandle>(
+ mGlobal, mManager, mMetadata, mRequestHandler.release());
+
+ ASSERT_TRUE(fileHandle);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->CreateSyncAccessHandle(rv);
+
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_ERROR_UNEXPECTED));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/TestFileSystemHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemHandle.cpp
new file mode 100644
index 0000000000..19cdc98a84
--- /dev/null
+++ b/dom/fs/test/gtest/api/TestFileSystemHandle.cpp
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemMocks.h"
+#include "fs/FileSystemChildFactory.h"
+#include "gtest/gtest.h"
+#include "mozilla/dom/FileSystemDirectoryHandle.h"
+#include "mozilla/dom/FileSystemFileHandle.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemHandleBinding.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemHandle : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mDirMetadata = FileSystemEntryMetadata("dir"_ns, u"Directory"_ns,
+ /* directory */ true);
+ mFileMetadata =
+ FileSystemEntryMetadata("file"_ns, u"File"_ns, /* directory */ false);
+ mManager = MakeAndAddRef<FileSystemManager>(mGlobal, nullptr);
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+
+ if (!mManager->IsShutdown()) {
+ mManager->Shutdown();
+ }
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ FileSystemEntryMetadata mDirMetadata;
+ FileSystemEntryMetadata mFileMetadata;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemHandle, createAndDestroyHandles) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ EXPECT_TRUE(dirHandle);
+ EXPECT_TRUE(fileHandle);
+}
+
+TEST_F(TestFileSystemHandle, areFileNamesAsExpected) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ auto GetEntryName = [](const RefPtr<FileSystemHandle>& aHandle) {
+ DOMString domName;
+ aHandle->GetName(domName);
+ nsString result;
+ domName.ToString(result);
+ return result;
+ };
+
+ const nsString& dirName = GetEntryName(dirHandle);
+ EXPECT_TRUE(mDirMetadata.entryName().Equals(dirName));
+
+ const nsString& fileName = GetEntryName(fileHandle);
+ EXPECT_TRUE(mFileMetadata.entryName().Equals(fileName));
+}
+
+TEST_F(TestFileSystemHandle, isParentObjectReturned) {
+ ASSERT_TRUE(mGlobal);
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+
+ ASSERT_EQ(mGlobal, dirHandle->GetParentObject());
+}
+
+TEST_F(TestFileSystemHandle, areHandleKindsAsExpected) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ EXPECT_EQ(FileSystemHandleKind::Directory, dirHandle->Kind());
+ EXPECT_EQ(FileSystemHandleKind::File, fileHandle->Kind());
+}
+
+TEST_F(TestFileSystemHandle, isDifferentEntry) {
+ RefPtr<FileSystemHandle> dirHandle =
+ new FileSystemDirectoryHandle(mGlobal, mManager, mDirMetadata);
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = dirHandle->IsSameEntry(*fileHandle, rv);
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+ ASSERT_TRUE(promise);
+ ASSERT_EQ(Promise::PromiseState::Resolved, promise->State());
+
+ nsString result;
+ ASSERT_NSEQ(NS_OK, GetAsString(promise, result));
+ ASSERT_STREQ(u"false"_ns, result);
+}
+
+TEST_F(TestFileSystemHandle, isSameEntry) {
+ RefPtr<FileSystemHandle> fileHandle =
+ new FileSystemFileHandle(mGlobal, mManager, mFileMetadata);
+
+ IgnoredErrorResult rv;
+ RefPtr<Promise> promise = fileHandle->IsSameEntry(*fileHandle, rv);
+ ASSERT_TRUE(rv.ErrorCodeIs(NS_OK));
+ ASSERT_TRUE(promise);
+ ASSERT_EQ(Promise::PromiseState::Resolved, promise->State());
+
+ nsString result;
+ ASSERT_NSEQ(NS_OK, GetAsString(promise, result));
+ ASSERT_STREQ(u"true"_ns, result);
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/api/moz.build b/dom/fs/test/gtest/api/moz.build
new file mode 100644
index 0000000000..eb8416a3ba
--- /dev/null
+++ b/dom/fs/test/gtest/api/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemDirectoryHandle.cpp",
+ "TestFileSystemFileHandle.cpp",
+ "TestFileSystemHandle.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/api",
+ "/dom/fs/include",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp b/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp
new file mode 100644
index 0000000000..48d63cfc36
--- /dev/null
+++ b/dom/fs/test/gtest/child/TestFileSystemBackgroundRequestHandler.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemBackgroundRequestHandler.h"
+#include "FileSystemMocks.h"
+#include "gtest/gtest.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/PFileSystemManager.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemBackgroundRequestHandler : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // TODO: Fix the test to not depend on CreateFileSystemManagerParent
+ // failure because of the pref set to false.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", false);
+
+ mFileSystemManagerChild = MakeAndAddRef<TestFileSystemManagerChild>();
+ }
+
+ void TearDown() override {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefs->SetBoolPref("dom.fs.enabled", true);
+ }
+
+ RefPtr<FileSystemBackgroundRequestHandler>
+ GetFileSystemBackgroundRequestHandler() {
+ return MakeRefPtr<FileSystemBackgroundRequestHandler>(
+ new TestFileSystemChildFactory(mFileSystemManagerChild));
+ }
+
+ mozilla::ipc::PrincipalInfo mPrincipalInfo = GetPrincipalInfo();
+ RefPtr<TestFileSystemManagerChild> mFileSystemManagerChild;
+};
+
+TEST_F(TestFileSystemBackgroundRequestHandler,
+ isCreateFileSystemManagerChildSuccessful) {
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce([fileSystemManagerChild =
+ static_cast<void*>(mFileSystemManagerChild.get())]() {
+ static_cast<TestFileSystemManagerChild*>(fileSystemManagerChild)
+ ->FileSystemManagerChild::Shutdown();
+ });
+
+ bool done = false;
+ auto testable = GetFileSystemBackgroundRequestHandler();
+ testable->CreateFileSystemManagerChild(mPrincipalInfo)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done](bool) { done = true; }, [&done](nsresult) { done = true; });
+ // MozPromise should be rejected
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [&done]() { return done; });
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp b/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp
new file mode 100644
index 0000000000..c2832103af
--- /dev/null
+++ b/dom/fs/test/gtest/child/TestFileSystemRequestHandler.cpp
@@ -0,0 +1,352 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemBackgroundRequestHandler.h"
+#include "FileSystemEntryMetadataArray.h"
+#include "FileSystemMocks.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "gtest/gtest.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FileBlobImpl.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/FileSystemManagerChild.h"
+#include "mozilla/dom/IPCBlob.h"
+#include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/StorageManager.h"
+#include "mozilla/ipc/FileDescriptorUtils.h"
+#include "mozilla/ipc/IPCCore.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIFile.h"
+
+using ::testing::_;
+using ::testing::ByRef;
+using ::testing::Invoke;
+using ::testing::Return;
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemRequestHandler : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ mListener = MakeAndAddRef<ExpectResolveCalled>();
+
+ mChild = FileSystemChildMetadata("parent"_ns, u"ChildName"_ns);
+ mEntry = FileSystemEntryMetadata("myid"_ns, u"EntryName"_ns,
+ /* directory */ false);
+ mName = u"testDir"_ns;
+ mFileSystemManagerChild = MakeAndAddRef<TestFileSystemManagerChild>();
+ mManager = MakeAndAddRef<FileSystemManager>(
+ mGlobal, nullptr,
+ MakeRefPtr<FileSystemBackgroundRequestHandler>(
+ mFileSystemManagerChild));
+ }
+
+ void TearDown() override {
+ if (!mManager->IsShutdown()) {
+ EXPECT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+ }
+ }
+
+ already_AddRefed<Promise> GetDefaultPromise() {
+ IgnoredErrorResult rv;
+ RefPtr<Promise> result = Promise::Create(mGlobal, rv);
+ mListener->ClearDone();
+ result->AppendNativeHandler(mListener->AsHandler());
+
+ return result.forget();
+ }
+
+ already_AddRefed<Promise> GetSimplePromise() {
+ IgnoredErrorResult rv;
+ RefPtr<Promise> result = Promise::Create(mGlobal, rv);
+
+ return result.forget();
+ }
+
+ already_AddRefed<Promise> GetShutdownPromise() {
+ RefPtr<Promise> promise = GetDefaultPromise();
+ EXPECT_CALL(*mFileSystemManagerChild, Shutdown())
+ .WillOnce(Invoke([promise]() { promise->MaybeResolveWithUndefined(); }))
+ .WillOnce(Return());
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+
+ return promise.forget();
+ }
+
+ UniquePtr<FileSystemRequestHandler> GetFileSystemRequestHandler() {
+ return MakeUnique<FileSystemRequestHandler>();
+ }
+
+ void ShutdownFileSystemManager() {
+ RefPtr<Promise> promise = GetShutdownPromise();
+
+ mManager->Shutdown();
+
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+ ASSERT_TRUE(mManager->IsShutdown());
+ }
+
+ nsIGlobalObject* mGlobal = GetGlobal();
+ RefPtr<ExpectResolveCalled> mListener;
+
+ FileSystemChildMetadata mChild;
+ FileSystemEntryMetadata mEntry;
+ nsString mName;
+ RefPtr<TestFileSystemManagerChild> mFileSystemManagerChild;
+ RefPtr<FileSystemManager> mManager;
+};
+
+TEST_F(TestFileSystemRequestHandler, isGetRootHandleSuccessful) {
+ auto fakeResponse = [](auto&& aResolve, auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetRootHandle(_, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetRootHandle(mManager, promise, IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetRootHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetRootHandle(mManager, GetSimplePromise(),
+ error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetDirectoryHandle(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetDirectoryHandle(mManager, mChild,
+ /* create */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetDirectoryHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetDirectoryHandle(
+ mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileHandleSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ EntryId expected = "expected"_ns;
+ FileSystemGetHandleResponse response(expected);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetFileHandle(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetFileHandle(mManager, mChild, /* create */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileHandleBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetFileHandle(
+ mManager, mChild, /* aCreate */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ // We have to create a temporary file
+ nsCOMPtr<nsIFile> tmpfile;
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpfile));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpfile->AppendNative("GetFileTestBlob"_ns);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpfile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ auto blob = MakeRefPtr<FileBlobImpl>(tmpfile);
+
+ TimeStamp last_modified_ms = 0;
+ ContentType type = "txt"_ns;
+ IPCBlob file;
+ IPCBlobUtils::Serialize(blob, file);
+
+ nsTArray<Name> path;
+ path.AppendElement(u"root"_ns);
+ path.AppendElement(u"Trash"_ns);
+
+ FileSystemFileProperties properties(last_modified_ms, file, type, path);
+ FileSystemGetFileResponse response(properties);
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetFile(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ RefPtr<Promise> promise = GetDefaultPromise();
+ auto testable = GetFileSystemRequestHandler();
+ testable->GetFile(mManager, mEntry, promise, IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetFileBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetFile(mManager, mEntry, GetSimplePromise(),
+ error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetAccessHandleBlockedAfterShutdown) {
+ RefPtr<Promise> promise = GetShutdownPromise();
+
+ mManager->Shutdown();
+
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+ ASSERT_TRUE(mManager->IsShutdown());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetAccessHandle(mManager, mEntry,
+ GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetWritableBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetWritable(
+ mManager, mEntry, /* aKeepData */ false, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetEntriesSuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ nsTArray<FileSystemEntryMetadata> files;
+ nsTArray<FileSystemEntryMetadata> directories;
+ FileSystemDirectoryListing listing(files, directories);
+ FileSystemGetEntriesResponse response(listing);
+ aResolve(std::move(response));
+ };
+
+ RefPtr<ExpectResolveCalled> listener = MakeAndAddRef<ExpectResolveCalled>();
+ IgnoredErrorResult rv;
+ listener->ClearDone();
+ EXPECT_CALL(listener->GetSuccessHandler(), InvokeMe());
+
+ RefPtr<Promise> promise = Promise::Create(mGlobal, rv);
+ promise->AppendNativeHandler(listener);
+
+ EXPECT_CALL(*mFileSystemManagerChild, SendGetEntries(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ auto testable = GetFileSystemRequestHandler();
+ RefPtr<FileSystemEntryMetadataArray> sink;
+
+ testable->GetEntries(mManager, mEntry.entryId(), /* page */ 0, promise, sink,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [listener]() { return listener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isGetEntriesBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ RefPtr<FileSystemEntryMetadataArray> sink;
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->GetEntries(mManager, mEntry.entryId(),
+ /* aPage */ 0, GetSimplePromise(),
+ sink, error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+TEST_F(TestFileSystemRequestHandler, isRemoveEntrySuccessful) {
+ auto fakeResponse = [](const auto& /* aRequest */, auto&& aResolve,
+ auto&& /* aReject */) {
+ FileSystemRemoveEntryResponse response(mozilla::void_t{});
+ aResolve(std::move(response));
+ };
+
+ EXPECT_CALL(mListener->GetSuccessHandler(), InvokeMe());
+ EXPECT_CALL(*mFileSystemManagerChild, SendRemoveEntry(_, _, _))
+ .WillOnce(Invoke(fakeResponse));
+
+ auto testable = GetFileSystemRequestHandler();
+ RefPtr<Promise> promise = GetDefaultPromise();
+ testable->RemoveEntry(mManager, mChild, /* recursive */ true, promise,
+ IgnoredErrorResult());
+ SpinEventLoopUntil("Promise is fulfilled or timeout"_ns,
+ [this]() { return mListener->IsDone(); });
+}
+
+TEST_F(TestFileSystemRequestHandler, isRemoveEntryBlockedAfterShutdown) {
+ ASSERT_NO_FATAL_FAILURE(ShutdownFileSystemManager());
+
+ IgnoredErrorResult error;
+ GetFileSystemRequestHandler()->RemoveEntry(
+ mManager, mChild, /* aRecursive */ true, GetSimplePromise(), error);
+
+ ASSERT_TRUE(error.Failed());
+ ASSERT_TRUE(error.ErrorCodeIs(NS_ERROR_ILLEGAL_DURING_SHUTDOWN));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/child/moz.build b/dom/fs/test/gtest/child/moz.build
new file mode 100644
index 0000000000..c305ab1f2e
--- /dev/null
+++ b/dom/fs/test/gtest/child/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemBackgroundRequestHandler.cpp",
+ "TestFileSystemRequestHandler.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/child",
+ "/dom/fs/include",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/moz.build b/dom/fs/test/gtest/moz.build
new file mode 100644
index 0000000000..81be2a3d33
--- /dev/null
+++ b/dom/fs/test/gtest/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += [
+ "api",
+ "child",
+ "parent",
+ "shared",
+]
+
+UNIFIED_SOURCES = [
+ "FileSystemMocks.cpp",
+ "TestHelpers.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+]
diff --git a/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
new file mode 100644
index 0000000000..1764763dd5
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemHashSource.h"
+#include "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozilla/Array.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "nsContentUtils.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using mozilla::dom::fs::data::FileSystemHashSource;
+
+namespace {
+
+constexpr size_t sha256ByteLength = 32u;
+
+constexpr size_t kExpectedLength = 52u;
+
+std::wstring asWide(const nsString& aStr) {
+ std::wstring result;
+ result.reserve(aStr.Length());
+ for (const auto* it = aStr.BeginReading(); it != aStr.EndReading(); ++it) {
+ result.push_back(static_cast<wchar_t>(*it));
+ }
+ return result;
+}
+
+} // namespace
+
+TEST(TestFileSystemHashSource, isHashLengthAsExpected)
+{
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, result.Length());
+};
+
+TEST(TestFileSystemHashSource, areNestedNameHashesValidAndUnequal)
+{
+ EntryId emptyParent = ""_ns;
+ Name name = u"a"_ns;
+ const size_t nestingNumber = 500u;
+
+ nsTHashSet<EntryId> results;
+ nsTHashSet<Name> names;
+
+ auto previousParent = emptyParent;
+ for (size_t i = 0; i < nestingNumber; ++i) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(previousParent, name));
+
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ // Is the same hash encountered?
+ ASSERT_FALSE(results.Contains(result));
+ ASSERT_TRUE(results.Insert(result, mozilla::fallible));
+
+ // Is the same name encountered?
+ ASSERT_FALSE(names.Contains(encoded));
+ ASSERT_TRUE(names.Insert(encoded, mozilla::fallible));
+
+ previousParent = result;
+ }
+};
+
+TEST(TestFileSystemHashSource, areNameCombinationHashesUnequal)
+{
+ EntryId emptyParent = ""_ns;
+
+ mozilla::Array<Name, 2> inputs = {u"a"_ns, u"b"_ns};
+ nsTArray<EntryId> results;
+ nsTArray<Name> names;
+
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(emptyParent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ results.AppendElement(result);
+ names.AppendElement(encoded);
+ }
+
+ nsTArray<EntryId> more_results;
+ nsTArray<Name> more_names;
+ for (const auto& parent : results) {
+ for (const auto& name : inputs) {
+ TEST_TRY_UNWRAP(EntryId result,
+ FileSystemHashSource::GenerateHash(parent, name));
+ TEST_TRY_UNWRAP(Name encoded,
+ FileSystemHashSource::EncodeHash(FileId(result)));
+
+ // Validity checks
+ ASSERT_TRUE(mozilla::IsAscii(encoded))
+ << encoded;
+ Name upperCaseVersion;
+ nsContentUtils::ASCIIToUpper(encoded, upperCaseVersion);
+ ASSERT_STREQ(asWide(upperCaseVersion).c_str(), asWide(encoded).c_str());
+
+ more_results.AppendElement(result);
+ more_names.AppendElement(encoded);
+ }
+ }
+
+ results.AppendElements(more_results);
+ names.AppendElements(more_names);
+
+ // Is the same hash encountered?
+ for (size_t i = 0; i < results.Length(); ++i) {
+ for (size_t j = i + 1; j < results.Length(); ++j) {
+ ASSERT_STRNE(results[i].get(), results[j].get());
+ }
+ }
+
+ // Is the same name encountered?
+ for (size_t i = 0; i < names.Length(); ++i) {
+ for (size_t j = i + 1; j < names.Length(); ++j) {
+ ASSERT_STRNE(asWide(names[i]).c_str(), asWide(names[j]).c_str());
+ }
+ }
+};
+
+TEST(TestFileSystemHashSource, encodeGeneratedHash)
+{
+ Name expected = u"HF6FOFV72G3NMDEJKYMVRIFJO4X5ZNZCF2GM7Q4Y5Q3E7NPQKSLA"_ns;
+ ASSERT_EQ(kExpectedLength, expected.Length());
+
+ EntryId parent = "a"_ns;
+ Name name = u"b"_ns;
+ TEST_TRY_UNWRAP(EntryId entry,
+ FileSystemHashSource::GenerateHash(parent, name));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(Name result, FileSystemHashSource::EncodeHash(FileId(entry)));
+ ASSERT_EQ(kExpectedLength, result.Length());
+ ASSERT_STREQ(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Generate further hashes
+ TEST_TRY_UNWRAP(entry, FileSystemHashSource::GenerateHash(entry, result));
+ ASSERT_EQ(sha256ByteLength, entry.Length());
+
+ TEST_TRY_UNWRAP(result, FileSystemHashSource::EncodeHash(FileId(entry)));
+
+ // Always the same length
+ ASSERT_EQ(kExpectedLength, result.Length());
+
+ // Encoded versions should differ
+ ASSERT_STRNE(asWide(expected).c_str(), asWide(result).c_str());
+
+ // Padding length should have been stripped
+ char16_t padding = u"="_ns[0];
+ const int32_t paddingStart = result.FindChar(padding);
+ ASSERT_EQ(-1, paddingStart);
+};
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
new file mode 100644
index 0000000000..d62dfb1229
--- /dev/null
+++ b/dom/fs/test/gtest/parent/TestFileSystemQuotaClient.cpp
@@ -0,0 +1,474 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemParentTypes.h"
+#include "TestHelpers.h"
+#include "datamodel/FileSystemDataManager.h"
+#include "datamodel/FileSystemDatabaseManager.h"
+#include "datamodel/FileSystemFileManager.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/FileSystemQuotaClientFactory.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+
+namespace mozilla::dom::fs::test {
+
+quota::OriginMetadata GetTestQuotaOriginMetadata() {
+ return quota::OriginMetadata{""_ns,
+ "quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ "http://quotaexample.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemQuotaClient
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static const int sPage = 64 * 512;
+ // ExceedsPreallocation value may depend on platform and sqlite version!
+ static const int sExceedsPreallocation = sPage;
+
+ protected:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(
+ ClearStoragesForOrigin(GetTestQuotaOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+
+ static const Name& GetTestFileName() {
+ static Name testFileName = []() {
+ nsCString testCFileName;
+ testCFileName.SetLength(sExceedsPreallocation);
+ std::fill(testCFileName.BeginWriting(), testCFileName.EndWriting(), 'x');
+ return NS_ConvertASCIItoUTF16(testCFileName.BeginReading(),
+ sExceedsPreallocation);
+ }();
+
+ return testFileName;
+ }
+
+ static uint64_t BytesOfName(const Name& aName) {
+ return static_cast<uint64_t>(aName.Length() * sizeof(Name::char_type));
+ }
+
+ static const nsCString& GetTestData() {
+ static const nsCString sTestData = "There is a way out of every box"_ns;
+ return sTestData;
+ }
+
+ static void CreateNewEmptyFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const FileSystemChildMetadata& aFileSlot, EntryId& aEntryId) {
+ // The file should not exist yet
+ Result<EntryId, QMResult> existingTestFile =
+ aDatabaseManager->GetOrCreateFile(aFileSlot, /* create */ false);
+ ASSERT_TRUE(existingTestFile.isErr());
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR,
+ ToNSResult(existingTestFile.unwrapErr()));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(aEntryId, aDatabaseManager->GetOrCreateFile(
+ aFileSlot, /* create */ true));
+ }
+
+ static void WriteDataToFile(
+ data::FileSystemDatabaseManager* const aDatabaseManager,
+ const EntryId& aEntryId, const nsCString& aData) {
+ TEST_TRY_UNWRAP(FileId fileId, aDatabaseManager->EnsureFileId(aEntryId));
+ ASSERT_FALSE(fileId.IsEmpty());
+
+ ContentType type;
+ TimeStamp lastModMilliS = 0;
+ Path path;
+ nsCOMPtr<nsIFile> fileObj;
+ ASSERT_NSEQ(NS_OK,
+ aDatabaseManager->GetFile(aEntryId, fileId, FileMode::EXCLUSIVE,
+ type, lastModMilliS, path, fileObj));
+
+ uint32_t written = 0;
+ ASSERT_NE(written, aData.Length());
+
+ const quota::OriginMetadata& testOriginMeta = GetTestQuotaOriginMetadata();
+
+ TEST_TRY_UNWRAP(nsCOMPtr<nsIOutputStream> fileStream,
+ quota::CreateFileOutputStream(
+ quota::PERSISTENCE_TYPE_DEFAULT, testOriginMeta,
+ quota::Client::FILESYSTEM, fileObj));
+
+ auto finallyClose = MakeScopeExit(
+ [&fileStream]() { ASSERT_NSEQ(NS_OK, fileStream->Close()); });
+ ASSERT_NSEQ(NS_OK,
+ fileStream->Write(aData.get(), aData.Length(), &written));
+
+ ASSERT_EQ(aData.Length(), written);
+ }
+
+ /* Static for use in callbacks */
+ static void CreateRegisteredDataManager(
+ Registered<data::FileSystemDataManager>& aRegisteredDataManager) {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestQuotaOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&aRegisteredDataManager,
+ &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) mutable {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+ aRegisteredDataManager = std::move(registeredDataManager);
+ },
+ [&done](nsresult rejectValue) {
+ auto doneOnReturn = MakeScopeExit([&done]() { done = true; });
+
+ ASSERT_NSEQ(NS_OK, rejectValue);
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+
+ ASSERT_TRUE(aRegisteredDataManager);
+ ASSERT_TRUE(aRegisteredDataManager->IsOpen());
+ ASSERT_TRUE(aRegisteredDataManager->MutableDatabaseManagerPtr());
+ }
+
+ static void CheckUsageEqualTo(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_EQ(actual, expected);
+ }
+
+ static void CheckUsageGreaterThan(const quota::UsageInfo& aUsage,
+ uint64_t expected) {
+ EXPECT_TRUE(aUsage.FileUsage().isNothing());
+ auto dbUsage = aUsage.DatabaseUsage();
+ ASSERT_TRUE(dbUsage.isSome());
+ const auto actual = dbUsage.value();
+ ASSERT_GT(actual, expected);
+ }
+};
+
+TEST_F(TestFileSystemQuotaClient, CheckUsageBeforeAnyFilesOnDisk) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](const RefPtr<quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ ASSERT_FALSE(isCanceled);
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ // After initialization,
+ // * database size is not zero
+ // * GetUsageForOrigin and InitOrigin should agree
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageGreaterThan(usageNow, 0u));
+ const auto initialDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, initialDbUsage));
+
+ // Create a new file
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+
+ // After a new file has been created (only in the database),
+ // * database size has increased
+ // * GetUsageForOrigin and InitOrigin should agree
+ const auto expectedUse = initialDbUsage + 2 * sPage;
+
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedUse));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // For uninitialized database, file usage is nothing
+ auto checkTask =
+ [&isCanceled](const RefPtr<mozilla::dom::quota::Client>& quotaClient) {
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ GetTestQuotaOriginMetadata(), isCanceled));
+
+ ASSERT_TRUE(usageNow.DatabaseUsage().isNothing());
+ EXPECT_TRUE(usageNow.FileUsage().isNothing());
+ };
+
+ PerformOnIOThread(std::move(checkTask),
+ RefPtr<mozilla::dom::quota::Client>{quotaClient});
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, WritesToFilesShouldIncreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ // const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // In this test we don't lock the file -> no rescan is expected
+ // and InitOrigin should return the previous value
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // When data manager unlocks the file, it should call update
+ // but in this test we call it directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ // Disk usage should have increased after writing
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // The usage values should now agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, TrackedFilesOnInitOriginShouldCauseRescan) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ EntryId* testFileId = new EntryId();
+ auto cleanupFileId = MakeScopeExit([&testFileId] { delete testFileId; });
+
+ auto fileCreation = [&testFileId](data::FileSystemDatabaseManager* dbm) {
+ const Origin& testOrigin = GetTestQuotaOriginMetadata().mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId someId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, someId));
+ testFileId->Append(someId);
+ };
+
+ auto writingToFile =
+ [&isCanceled, testFileId](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ TEST_TRY_UNWRAP(
+ quota::UsageInfo usageNow,
+ quotaClient->GetUsageForOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const auto& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, *testFileId, testData));
+
+ // We don't call update now - because the file is tracked and
+ // InitOrigin should perform a rescan
+ TEST_TRY_UNWRAP(
+ usageNow, quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ // As always, the cached and scanned values should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(
+ CheckUsageEqualTo(usageNow, expectedTotalUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ PerformOnIOThread(std::move(fileCreation),
+ rdm->MutableDatabaseManagerPtr());
+
+ // This should force a rescan
+ TEST_TRY_UNWRAP(FileId fileId, rdm->LockExclusive(*testFileId));
+ ASSERT_FALSE(fileId.IsEmpty());
+ PerformOnIOThread(std::move(writingToFile), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemQuotaClient, RemovingFileShouldDecreaseUsage) {
+ auto backgroundTask = []() {
+ mozilla::Atomic<bool> isCanceled{false};
+ auto ioTask = [&isCanceled](
+ const RefPtr<mozilla::dom::quota::Client>& quotaClient,
+ data::FileSystemDatabaseManager* dbm) {
+ const quota::OriginMetadata& testOriginMeta =
+ GetTestQuotaOriginMetadata();
+ const Origin& testOrigin = testOriginMeta.mOrigin;
+
+ TEST_TRY_UNWRAP(const EntryId rootId, data::GetRootHandle(testOrigin));
+ FileSystemChildMetadata fileData(rootId, GetTestFileName());
+
+ EntryId testFileId;
+ ASSERT_NO_FATAL_FAILURE(CreateNewEmptyFile(dbm, fileData, testFileId));
+ TEST_TRY_UNWRAP(quota::UsageInfo usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_TRUE(usageNow.DatabaseUsage().isSome());
+ const auto testFileDbUsage = usageNow.DatabaseUsage().value();
+
+ // Fill the file with some content
+ const nsCString& testData = GetTestData();
+ const auto expectedTotalUsage = testFileDbUsage + testData.Length();
+
+ ASSERT_NO_FATAL_FAILURE(WriteDataToFile(dbm, testFileId, testData));
+
+ // Currently, usage is expected to be updated on unlock by data manager
+ // but here UpdateUsage() is called directly
+ ASSERT_NSEQ(NS_OK, dbm->UpdateUsage(FileId(testFileId)));
+
+ // At least some file disk usage should have appeared after unlocking
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, expectedTotalUsage));
+
+ TEST_TRY_UNWRAP(bool wasRemoved,
+ dbm->RemoveFile({rootId, GetTestFileName()}));
+ ASSERT_TRUE(wasRemoved);
+
+ // Removes cascade and usage table should be up to date immediately
+ TEST_TRY_UNWRAP(usageNow,
+ quotaClient->InitOrigin(quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+
+ // GetUsageForOrigin should agree
+ TEST_TRY_UNWRAP(usageNow, quotaClient->GetUsageForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT,
+ testOriginMeta, isCanceled));
+
+ ASSERT_NO_FATAL_FAILURE(CheckUsageEqualTo(usageNow, testFileDbUsage));
+ };
+
+ RefPtr<mozilla::dom::quota::Client> quotaClient = fs::CreateQuotaClient();
+ ASSERT_TRUE(quotaClient);
+
+ // Initialize database
+ Registered<data::FileSystemDataManager> rdm;
+ ASSERT_NO_FATAL_FAILURE(CreateRegisteredDataManager(rdm));
+
+ // Run tests with an initialized database
+ PerformOnIOThread(std::move(ioTask), std::move(quotaClient),
+ rdm->MutableDatabaseManagerPtr());
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
new file mode 100644
index 0000000000..484def797f
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileSystemDataManager.h"
+#include "TestHelpers.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+
+namespace mozilla::dom::fs::test {
+
+class TestFileSystemDataManager
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ static void SetUpTestCase() { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ static void TearDownTestCase() {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetTestOriginMetadata()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+TEST_F(TestFileSystemDataManager, GetOrCreateFileSystemDataManager) {
+ auto backgroundTask = []() {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager> registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingOpen) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm1;
+
+ Registered<data::FileSystemDataManager> rdm2;
+
+ {
+ bool done1 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm1, &done1](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm1 = std::move(registeredDataManager);
+
+ done1 = true;
+ },
+ [&done1](nsresult rejectValue) { done1 = true; });
+
+ bool done2 = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm2, &done2](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm2 = std::move(registeredDataManager);
+
+ done2 = true;
+ },
+ [&done2](nsresult rejectValue) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm1 = rdm1.unwrap();
+
+ RefPtr<data::FileSystemDataManager> dm2 = rdm2.unwrap();
+
+ {
+ bool done1 = false;
+
+ dm1->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done1](const BoolPromise::ResolveOrRejectValue&) { done1 = true; });
+
+ bool done2 = false;
+
+ dm2->OnClose()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&done2](const BoolPromise::ResolveOrRejectValue&) { done2 = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&done1, &done2]() { return done1 && done2; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+TEST_F(TestFileSystemDataManager,
+ GetOrCreateFileSystemDataManager_PendingClose) {
+ auto backgroundTask = []() {
+ Registered<data::FileSystemDataManager> rdm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&rdm, &done](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ ASSERT_TRUE(registeredDataManager->IsOpen());
+
+ rdm = std::move(registeredDataManager);
+
+ done = true;
+ },
+ [&done](nsresult rejectValue) { done = true; });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+
+ RefPtr<data::FileSystemDataManager> dm = rdm.unwrap();
+
+ Unused << dm;
+
+ {
+ bool done = false;
+
+ data::FileSystemDataManager::GetOrCreateFileSystemDataManager(
+ GetTestOriginMetadata())
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](Registered<data::FileSystemDataManager>
+ registeredDataManager) {
+ RefPtr<data::FileSystemDataManager> dataManager =
+ registeredDataManager.get();
+
+ registeredDataManager = nullptr;
+
+ return dataManager->OnClose();
+ },
+ [](nsresult rejectValue) {
+ return BoolPromise::CreateAndReject(rejectValue, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue&) {
+ done = true;
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+ };
+
+ PerformOnBackgroundThread(std::move(backgroundTask));
+}
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
new file mode 100644
index 0000000000..9782e534e9
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersions.cpp
@@ -0,0 +1,1059 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+
+#include "ErrorList.h"
+#include "FileSystemDataManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemDatabaseManagerVersion002.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "SchemaVersion002.h"
+#include "TestHelpers.h"
+#include "gtest/gtest.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Array.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/test/QuotaManagerDependencyFixture.h"
+#include "nsContentUtils.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsNetCID.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+
+namespace mozilla::dom::fs::test {
+
+using data::FileSystemDatabaseManagerVersion001;
+using data::FileSystemDatabaseManagerVersion002;
+using data::FileSystemFileManager;
+
+quota::OriginMetadata GetOriginMetadataSample() {
+ return quota::OriginMetadata{""_ns,
+ "firefox.com"_ns,
+ "http://firefox.com"_ns,
+ "http://firefox.com"_ns,
+ /* aIsPrivate */ false,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+}
+
+class TestFileSystemDatabaseManagerVersionsBase
+ : public quota::test::QuotaManagerDependencyFixture {
+ public:
+ void SetUp() override { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); }
+
+ void TearDown() override {
+ EXPECT_NO_FATAL_FAILURE(ClearStoragesForOrigin(GetOriginMetadataSample()));
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+};
+
+class TestFileSystemDatabaseManagerVersions
+ : public TestFileSystemDatabaseManagerVersionsBase,
+ public ::testing::WithParamInterface<DatabaseVersion> {
+ public:
+ static void AssertEntryIdMoved(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static void AssertEntryIdCollision(const EntryId& aOriginal,
+ const EntryId& aMoved) {
+ switch (sVersion) {
+ case 1: {
+ // We generated a new entryId
+ ASSERT_NE(aOriginal, aMoved);
+ break;
+ }
+ case 2: {
+ // We get the same entryId for the same input
+ ASSERT_EQ(aOriginal, aMoved);
+ break;
+ }
+ default: {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+ }
+
+ static DatabaseVersion sVersion;
+};
+
+// This is a minimal mock to allow us to safely call the lock methods
+// while avoiding assertions
+class MockFileSystemDataManager final : public data::FileSystemDataManager {
+ public:
+ MockFileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : FileSystemDataManager(aOriginMetadata, nullptr, std::move(aIOTarget),
+ std::move(aIOTaskQueue)) {}
+
+ void SetDatabaseManager(data::FileSystemDatabaseManager* aDatabaseManager) {
+ mDatabaseManager =
+ UniquePtr<data::FileSystemDatabaseManager>(aDatabaseManager);
+ }
+
+ virtual ~MockFileSystemDataManager() {
+ mDatabaseManager->Close();
+ mDatabaseManager = nullptr;
+
+ // Need to avoid assertions
+ mState = State::Closed;
+ }
+};
+
+static void MakeDatabaseManagerVersions(
+ const DatabaseVersion aVersion,
+ RefPtr<MockFileSystemDataManager>& aDataManager,
+ FileSystemDatabaseManagerVersion001*& aDatabaseManager) {
+ TEST_TRY_UNWRAP(auto storageService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ const auto flags = mozIStorageService::CONNECTION_DEFAULT;
+ ResultConnection connection;
+
+ nsresult rv = storageService->OpenSpecialDatabase(kMozStorageMemoryStorageKey,
+ VoidCString(), flags,
+ getter_AddRefs(connection));
+ ASSERT_NSEQ(NS_OK, rv);
+
+ auto fmRes = FileSystemFileManager::CreateFileSystemFileManager(
+ GetOriginMetadataSample());
+ ASSERT_FALSE(fmRes.isErr());
+
+ const Origin& testOrigin = GetTestOrigin();
+
+ if (1 == aVersion) {
+ TEST_TRY_UNWRAP(
+ TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion001::InitializeConnection(connection, testOrigin));
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ TEST_TRY_UNWRAP(TestFileSystemDatabaseManagerVersions::sVersion,
+ SchemaVersion002::InitializeConnection(
+ connection, *fmRes.inspect(), testOrigin));
+ }
+ ASSERT_NE(0, TestFileSystemDatabaseManagerVersions::sVersion);
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ QM_TRY_UNWRAP(auto streamTransportService,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_STREAMTRANSPORTSERVICE_CONTRACTID),
+ QM_VOID);
+
+ quota::OriginMetadata originMetadata = GetOriginMetadataSample();
+
+ nsCString taskQueueName("OPFS "_ns + originMetadata.mOrigin);
+
+ RefPtr<TaskQueue> ioTaskQueue =
+ TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());
+
+ aDataManager = MakeRefPtr<MockFileSystemDataManager>(
+ originMetadata, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ if (1 == aVersion) {
+ aDatabaseManager = new FileSystemDatabaseManagerVersion001(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ } else {
+ ASSERT_EQ(2, aVersion);
+
+ aDatabaseManager = new FileSystemDatabaseManagerVersion002(
+ aDataManager, std::move(connection), fmRes.unwrap(), rootId);
+ }
+
+ aDataManager->SetDatabaseManager(aDatabaseManager);
+}
+
+DatabaseVersion TestFileSystemDatabaseManagerVersions::sVersion = 0;
+
+TEST_P(TestFileSystemDatabaseManagerVersions,
+ smokeTestCreateRemoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> dataManager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, dataManager, dm));
+ ASSERT_TRUE(dm);
+ // if any of these exit early, we have to close
+ auto autoClose = MakeScopeExit([dm] { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChild, dm->GetOrCreateDirectory(
+ firstChildMeta, /* create */ true));
+
+ int32_t dbVersion = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, dbVersion));
+ ASSERT_EQ(1u, entries.directories().Length());
+ ASSERT_EQ(0u, entries.files().Length());
+
+ const auto& firstItemRef = entries.directories()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ TEST_TRY_UNWRAP(
+ EntryId firstChildClone,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ FileSystemChildMetadata secondChildMeta(firstChild, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ FileSystemEntryPair shortPair(firstChild, secondChild);
+ TEST_TRY_UNWRAP(Path shortPath, dm->Resolve(shortPair));
+ ASSERT_EQ(1u, shortPath.Length());
+ ASSERT_EQ(u"Second"_ns, shortPath[0]);
+
+ FileSystemEntryPair longPair(rootId, secondChild);
+ TEST_TRY_UNWRAP(Path longPath, dm->Resolve(longPair));
+ ASSERT_EQ(2u, longPath.Length());
+ ASSERT_EQ(u"First"_ns, longPath[0]);
+ ASSERT_EQ(u"Second"_ns, longPath[1]);
+
+ FileSystemEntryPair wrongPair(secondChild, rootId);
+ TEST_TRY_UNWRAP(Path emptyPath, dm->Resolve(wrongPair));
+ ASSERT_TRUE(emptyPath.IsEmpty());
+
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(firstChild, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(0u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(firstChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(firstChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Second"_ns);
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateRemoveFiles) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ nsresult rv = NS_OK;
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ // If creating is not allowed, getting a file from empty root fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Creating a file under empty root succeeds
+ TEST_TRY_UNWRAP(EntryId firstChild,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+
+ // Second time, the same file is returned
+ TEST_TRY_UNWRAP(EntryId firstChildClone,
+ dm->GetOrCreateFile(firstChildMeta, /* create */ true));
+ ASSERT_EQ(firstChild, firstChildClone);
+
+ // Directory listing returns the created file
+ PageNumber page = 0;
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing entries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(0u, entries.directories().Length());
+ ASSERT_EQ(1u, entries.files().Length());
+
+ auto& firstItemRef = entries.files()[0];
+ ASSERT_TRUE(u"First"_ns == firstItemRef.entryName())
+ << firstItemRef.entryName();
+ ASSERT_EQ(firstChild, firstItemRef.entryId());
+
+ FileId fileId = FileId(firstItemRef.entryId()); // Default
+
+ ContentType type;
+ TimeStamp lastModifiedMilliSeconds;
+ Path path;
+ nsCOMPtr<nsIFile> file;
+ rv = dm->GetFile(firstItemRef.entryId(), fileId, FileMode::EXCLUSIVE, type,
+ lastModifiedMilliSeconds, path, file);
+ ASSERT_NSEQ(NS_OK, rv);
+
+ const int64_t nowMilliSeconds = PR_Now() / 1000;
+ ASSERT_GE(nowMilliSeconds, lastModifiedMilliSeconds);
+ const int64_t expectedMaxDelayMilliSeconds = 100;
+ const int64_t actualDelay = nowMilliSeconds - lastModifiedMilliSeconds;
+ ASSERT_LT(actualDelay, expectedMaxDelayMilliSeconds);
+
+ ASSERT_EQ(1u, path.Length());
+ ASSERT_STREQ(u"First"_ns, path[0]);
+
+ ASSERT_NE(nullptr, file);
+
+ // Getting the file entry as directory fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Getting or creating the file entry as directory also fails
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ // Creating a file with non existing parent hash fails
+
+ EntryId notAChildHash = "0123456789abcdef0123456789abcdef"_ns;
+ FileSystemChildMetadata notAChildMeta(notAChildHash, u"Dummy"_ns);
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(notAChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // We create a directory under root
+ FileSystemChildMetadata secondChildMeta(rootId, u"Second"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId secondChild,
+ dm->GetOrCreateDirectory(secondChildMeta, /* create */ true));
+
+ // The root should now contain the existing file and the new directory
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries,
+ dm->GetDirectoryEntries(rootId, page));
+ ASSERT_EQ(1u, fEntries.directories().Length());
+ ASSERT_EQ(1u, fEntries.files().Length());
+
+ const auto& secItemRef = fEntries.directories()[0];
+ ASSERT_TRUE(u"Second"_ns == secItemRef.entryName())
+ << secItemRef.entryName();
+ ASSERT_EQ(secondChild, secItemRef.entryId());
+
+ // Create a file under the new directory
+ FileSystemChildMetadata thirdChildMeta(secondChild, u"Third"_ns);
+ TEST_TRY_UNWRAP(EntryId thirdChild,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+
+ FileSystemEntryPair entryPair(rootId, thirdChild);
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve(entryPair));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_EQ(u"Second"_ns, entryPath[0]);
+ ASSERT_EQ(u"Third"_ns, entryPath[1]);
+
+ // If recursion is not allowed, the non-empty new directory may not be
+ // removed
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->RemoveDirectory(secondChildMeta, /* recursive */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+
+ // If recursion is allowed, the new directory goes away.
+ TEST_TRY_UNWRAP(bool isDeleted,
+ dm->RemoveDirectory(secondChildMeta, /* recursive */ true));
+ ASSERT_TRUE(isDeleted);
+
+ // The file under the removed directory is no longer accessible.
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateFile(thirdChildMeta, /* create */ true));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error?
+
+ // The deletion is reflected by the root directory listing
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing nEntries,
+ dm->GetDirectoryEntries(rootId, 0));
+ ASSERT_EQ(0u, nEntries.directories().Length());
+ ASSERT_EQ(1u, nEntries.files().Length());
+
+ const auto& fileItemRef = nEntries.files()[0];
+ ASSERT_TRUE(u"First"_ns == fileItemRef.entryName())
+ << fileItemRef.entryName();
+ ASSERT_EQ(firstChild, fileItemRef.entryId());
+
+ dm->Close();
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+TEST_P(TestFileSystemDatabaseManagerVersions, smokeTestCreateMoveDirectories) {
+ const DatabaseVersion version = GetParam();
+
+ auto ioTask = [version]() {
+ // Ensure that FileSystemDataManager lives for the lifetime of the test
+ RefPtr<MockFileSystemDataManager> datamanager;
+ FileSystemDatabaseManagerVersion001* dm = nullptr;
+ ASSERT_NO_FATAL_FAILURE(
+ MakeDatabaseManagerVersions(version, datamanager, dm));
+ auto closeAtExit = MakeScopeExit([&dm]() { dm->Close(); });
+
+ TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(GetTestOrigin()));
+
+ FileSystemEntryMetadata rootMeta{rootId, u"root"_ns,
+ /* is directory */ true};
+
+ {
+ // Sanity check: no existing items should be found
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_TRUE(contents.files().IsEmpty());
+ }
+
+ // Create subdirectory
+ FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns);
+ TEST_TRY_UNWRAP(
+ EntryId firstChildDir,
+ dm->GetOrCreateDirectory(firstChildMeta, /* create */ true));
+
+ {
+ // Check that directory listing is as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ // Try to move subdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDir = moved;
+ }
+
+ {
+ // Try to move subdirectory under itself
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create subsubdirectory
+ FileSystemChildMetadata firstChildDescendantMeta(firstChildDir,
+ u"Descendant"_ns);
+ TEST_TRY_UNWRAP(EntryId firstChildDescendant,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+
+ TEST_TRY_UNWRAP(
+ Path subSubDirPath,
+ dm->Resolve({rootId, contents.directories()[0].entryId()}));
+ ASSERT_EQ(2u, subSubDirPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), subSubDirPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDirPath[1]);
+ }
+
+ {
+ // Try to move subsubdirectory to its current location
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under itself
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{src.entryId(), src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move subdirectory under its descendant
+ FileSystemEntryMetadata src{firstChildDir, firstChildMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_INVALID_MODIFICATION_ERR, rv);
+ }
+
+ {
+ // Try to move root under its subsubdirectory
+ FileSystemEntryMetadata src{rootId, rootMeta.entryName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDescendant, src.entryName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ // Create file in the subdirectory with already existing subsubdirectory
+ FileSystemChildMetadata testFileMeta(firstChildDir, u"Subfile"_ns);
+ TEST_TRY_UNWRAP(EntryId testFile,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+
+ // Get handles to the original locations of the entries
+ FileSystemEntryMetadata subSubDir;
+ FileSystemEntryMetadata subSubFile;
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, /* page */ 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+
+ subSubDir = contents.directories()[0];
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), subSubDir.entryName());
+
+ subSubFile = contents.files()[0];
+ ASSERT_STREQ(testFileMeta.childName(), subSubFile.entryName());
+ ASSERT_EQ(testFile, subSubFile.entryId());
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Try to move file to its current location
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move subsubdirectory under a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{testFile,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->MoveEntry(src, dest));
+ ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv);
+ }
+
+ {
+ // Silently overwrite a directory with a file using rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ const FileSystemChildMetadata& dest = firstChildDescendantMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, testFileMeta));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to rename directory to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ const FileSystemChildMetadata& dest = testFileMeta;
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move directory back and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(testFileMeta, /* create */ true));
+ ASSERT_EQ(testFile, testFileCheck);
+ }
+
+ {
+ // Move file one level up
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, src.entryName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Check that listings are as expected
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ ASSERT_NO_FATAL_FAILURE(
+ AssertEntryIdMoved(subSubFile.entryId(), testFile));
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, subSubFile.entryId()}));
+ if (1 == sVersion) {
+ ASSERT_EQ(1u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ } else {
+ ASSERT_EQ(2, sVersion);
+ // Per spec, path result is empty when no path exists.
+ ASSERT_TRUE(entryPath.IsEmpty());
+ }
+ }
+
+ {
+ // Try to get a handle to the old item
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile(testFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+
+ {
+ // Try to move + rename file one level down to collide with a
+ // subSubDirectory, silently overwriting it
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Restore filename, move file to its original location and recreate
+ // the overwritten directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ // Rename file first and then try to move it to collide with
+ // subSubDirectory, silently overwriting it
+ {
+ // Rename
+ FileSystemEntryMetadata src{testFile, testFileMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Try to move one level down
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Move the file back and recreate the directory
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+
+ FileSystemChildMetadata oldLocation{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Is there still something out there?
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateFile(oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId firstChildDescendantCheck,
+ dm->GetOrCreateDirectory(oldLocation, /* create */ true));
+ ASSERT_EQ(firstChildDescendant, firstChildDescendantCheck);
+ }
+
+ {
+ // Try to move subSubDirectory one level up to quietly overwrite a file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Move subSubDirectory back one level down and recreate the file
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+
+ FileSystemChildMetadata oldLocation{rootId,
+ firstChildDescendantMeta.childName()};
+
+ // We should no longer find anything there
+ TEST_TRY_UNWRAP_ERR(nsresult rv, dm->GetOrCreateDirectory(
+ oldLocation, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(EntryId testFileCheck,
+ dm->GetOrCreateFile(oldLocation, /* create */ true));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdCollision(testFile, testFileCheck));
+ testFile = testFileCheck;
+ }
+
+ // Create a new file in the subsubdirectory
+ FileSystemChildMetadata newFileMeta{firstChildDescendant,
+ testFileMeta.childName()};
+ EntryId oldFirstChildDescendant = firstChildDescendant;
+
+ TEST_TRY_UNWRAP(EntryId newFile,
+ dm->GetOrCreateFile(newFileMeta, /* create */ true));
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Move subSubDirectory one level up and rename it to testFile's old name
+ FileSystemEntryMetadata src{firstChildDescendant,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{rootId, testFileMeta.childName()};
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ {
+ // Try to get handles to the moved items
+ TEST_TRY_UNWRAP_ERR(nsresult rv,
+ dm->GetOrCreateDirectory(firstChildDescendantMeta,
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ // Still under the same parent which was moved
+ if (1 == sVersion) {
+ TEST_TRY_UNWRAP(EntryId handle,
+ dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_EQ(handle, newFile);
+
+ TEST_TRY_UNWRAP(
+ handle, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_EQ(handle, firstChildDescendant);
+ } else if (2 == sVersion) {
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile(newFileMeta, /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_FALSE(newFileCheck.IsEmpty());
+ } else {
+ ASSERT_FALSE(false)
+ << "Unknown database version";
+ }
+ }
+
+ {
+ // Check that new file path is as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ // Move first file and subSubDirectory back one level down keeping the names
+ {
+ FileSystemEntryMetadata src{testFile,
+ firstChildDescendantMeta.childName(),
+ /* is directory */ false};
+ FileSystemChildMetadata dest{firstChildDir,
+ firstChildDescendantMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ testFile = moved;
+ }
+
+ {
+ // Then move the directory
+ FileSystemEntryMetadata src{firstChildDescendant,
+ testFileMeta.childName(),
+ /* is directory */ true};
+ FileSystemChildMetadata dest{firstChildDir, testFileMeta.childName()};
+
+ // Flag is ignored
+ TEST_TRY_UNWRAP(EntryId moved, dm->MoveEntry(src, dest));
+ ASSERT_FALSE(moved.IsEmpty());
+ firstChildDescendant = moved;
+ }
+
+ // Check that listings are as expected
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(rootId, 0u));
+ ASSERT_TRUE(contents.files().IsEmpty());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDir, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_EQ(1u, contents.directories().Length());
+ ASSERT_STREQ(firstChildDescendantMeta.childName(),
+ contents.files()[0].entryName());
+ ASSERT_STREQ(testFileMeta.childName(),
+ contents.directories()[0].entryName());
+ }
+
+ {
+ TEST_TRY_UNWRAP(FileSystemDirectoryListing contents,
+ dm->GetDirectoryEntries(firstChildDescendant, 0u));
+ ASSERT_EQ(1u, contents.files().Length());
+ ASSERT_TRUE(contents.directories().IsEmpty());
+ ASSERT_STREQ(testFileMeta.childName(), contents.files()[0].entryName());
+ }
+
+ // Names are swapped
+ {
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, testFile}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(firstChildDescendantMeta.childName(), entryPath[1]);
+ }
+
+ {
+ TEST_TRY_UNWRAP(Path entryPath,
+ dm->Resolve({rootId, firstChildDescendant}));
+ ASSERT_EQ(2u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ }
+
+ {
+ // Check that new file path is also as expected
+ TEST_TRY_UNWRAP(
+ EntryId newFileCheck,
+ dm->GetOrCreateFile({firstChildDescendant, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NO_FATAL_FAILURE(AssertEntryIdMoved(newFileCheck, newFile));
+ newFile = newFileCheck;
+
+ TEST_TRY_UNWRAP(Path entryPath, dm->Resolve({rootId, newFile}));
+ ASSERT_EQ(3u, entryPath.Length());
+ ASSERT_STREQ(firstChildMeta.childName(), entryPath[0]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[1]);
+ ASSERT_STREQ(testFileMeta.childName(), entryPath[2]);
+ }
+
+ {
+ // Try to get handles to the old items
+ TEST_TRY_UNWRAP_ERR(
+ nsresult rv, dm->GetOrCreateFile({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv,
+ dm->GetOrCreateFile({rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory({rootId, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(rv,
+ dm->GetOrCreateDirectory(
+ {rootId, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({firstChildDir, testFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateDirectory(
+ {firstChildDir, firstChildDescendantMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv);
+
+ TEST_TRY_UNWRAP_ERR(
+ rv, dm->GetOrCreateFile({testFile, newFileMeta.childName()},
+ /* create */ false));
+ ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv);
+ }
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+INSTANTIATE_TEST_SUITE_P(TestDatabaseManagerVersions,
+ TestFileSystemDatabaseManagerVersions,
+ testing::Values(1, 2));
+
+} // namespace mozilla::dom::fs::test
diff --git a/dom/fs/test/gtest/parent/datamodel/moz.build b/dom/fs/test/gtest/parent/datamodel/moz.build
new file mode 100644
index 0000000000..6369aec649
--- /dev/null
+++ b/dom/fs/test/gtest/parent/datamodel/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemDataManager.cpp",
+ "TestFileSystemDataManagerVersions.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent",
+ "/dom/fs/parent/datamodel",
+ "/dom/fs/test/gtest",
+ "/dom/fs/test/gtest/parent",
+]
diff --git a/dom/fs/test/gtest/parent/moz.build b/dom/fs/test/gtest/parent/moz.build
new file mode 100644
index 0000000000..9197c6ade2
--- /dev/null
+++ b/dom/fs/test/gtest/parent/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ["datamodel"]
+
+UNIFIED_SOURCES = [
+ "TestFileSystemHashSource.cpp",
+ "TestFileSystemQuotaClient.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/parent",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp b/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp
new file mode 100644
index 0000000000..ed38026634
--- /dev/null
+++ b/dom/fs/test/gtest/shared/TestFileSystemHelpers.cpp
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+class TestObject {
+ public:
+ explicit TestObject(uint32_t aExpectedAddRefCnt = 0,
+ uint32_t aExpectedAddRegCnt = 0)
+ : mExpectedAddRefCnt(aExpectedAddRefCnt),
+ mExpectedAddRegCnt(aExpectedAddRegCnt),
+ mAddRefCnt(0),
+ mAddRegCnt(0),
+ mRefCnt(0),
+ mRegCnt(0),
+ mClosed(false) {}
+
+ uint32_t AddRef() {
+ mRefCnt++;
+ mAddRefCnt++;
+ return mRefCnt;
+ }
+
+ uint32_t Release() {
+ EXPECT_TRUE(mRefCnt > 0);
+ mRefCnt--;
+ if (mRefCnt == 0) {
+ delete this;
+ return 0;
+ }
+ return mRefCnt;
+ }
+
+ void Register() {
+ EXPECT_FALSE(mClosed);
+ mRegCnt++;
+ mAddRegCnt++;
+ }
+
+ void Unregister() {
+ EXPECT_FALSE(mClosed);
+ EXPECT_TRUE(mRegCnt > 0);
+ mRegCnt--;
+ if (mRegCnt == 0) {
+ mClosed = true;
+ }
+ }
+
+ void Foo() const {}
+
+ private:
+ ~TestObject() {
+ if (mExpectedAddRefCnt > 0) {
+ EXPECT_EQ(mAddRefCnt, mExpectedAddRefCnt);
+ }
+ if (mExpectedAddRegCnt > 0) {
+ EXPECT_EQ(mAddRegCnt, mExpectedAddRegCnt);
+ }
+ }
+
+ uint32_t mExpectedAddRefCnt;
+ uint32_t mExpectedAddRegCnt;
+ uint32_t mAddRefCnt;
+ uint32_t mAddRegCnt;
+ uint32_t mRefCnt;
+ uint32_t mRegCnt;
+ bool mClosed;
+};
+
+} // namespace
+
+TEST(TestFileSystemHelpers_Registered, Construct_Default)
+{
+ { Registered<TestObject> testObject; }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_Copy)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(2, 2));
+ Registered<TestObject> testObject2(testObject1);
+ testObject2 = nullptr;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_Move)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ Registered<TestObject> testObject2(std::move(testObject1));
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Construct_FromRefPtr)
+{
+ { Registered<TestObject> testObject(MakeRefPtr<TestObject>(1, 1)); }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_FromNullPtr)
+{
+ {
+ Registered<TestObject> testObject;
+ testObject = nullptr;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_Copy)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(2, 2));
+ Registered<TestObject> testObject2;
+ testObject2 = testObject1;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Assign_Move)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ Registered<TestObject> testObject2;
+ testObject2 = std::move(testObject1);
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Inspect)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ const RefPtr<TestObject>& testObject2 = testObject1.inspect();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Unwrap)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ RefPtr<TestObject> testObject2 = testObject1.unwrap();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Method_Get)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ TestObject* testObject2 = testObject1.get();
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Conversion_ToRawPtr)
+{
+ {
+ Registered<TestObject> testObject1(MakeRefPtr<TestObject>(1, 1));
+ TestObject* testObject2 = testObject1;
+ Unused << testObject2;
+ }
+}
+
+TEST(TestFileSystemHelpers_Registered, Operator_Dereference_ToRawPtr)
+{
+ {
+ Registered<TestObject> testObject(MakeRefPtr<TestObject>(1, 1));
+ testObject->Foo();
+ }
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/test/gtest/shared/moz.build b/dom/fs/test/gtest/shared/moz.build
new file mode 100644
index 0000000000..e8feae8c57
--- /dev/null
+++ b/dom/fs/test/gtest/shared/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestFileSystemHelpers.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/fs/shared",
+ "/dom/fs/test/gtest",
+]
diff --git a/dom/fs/test/mochitest/head.js b/dom/fs/test/mochitest/head.js
new file mode 100644
index 0000000000..167e7c0c52
--- /dev/null
+++ b/dom/fs/test/mochitest/head.js
@@ -0,0 +1,82 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ const { ModuleLoader } = await import(
+ "/tests/dom/quota/test/modules/ModuleLoader.mjs"
+ );
+
+ const base = window.location.href;
+
+ const depth = "../../../../";
+
+ const { Assert } = await import("/tests/dom/quota/test/modules/Assert.mjs");
+
+ const { Utils } = await import("/tests/dom/quota/test/modules/Utils.mjs");
+
+ const proto = {
+ Assert,
+ Cr: SpecialPowers.Cr,
+ navigator,
+ TextEncoder,
+ Utils,
+ };
+
+ require_module.moduleLoader = new ModuleLoader(base, depth, proto);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
+
+async function run_test_in_worker(script) {
+ const { runTestInWorker } = await import(
+ "/tests/dom/quota/test/modules/WorkerDriver.mjs"
+ );
+
+ const base = window.location.href;
+
+ const listener = {
+ onOk(value, message) {
+ ok(value, message);
+ },
+ onIs(a, b, message) {
+ is(a, b, message);
+ },
+ onInfo(message) {
+ info(message);
+ },
+ };
+
+ await runTestInWorker(script, base, listener);
+}
+
+// XXX This can be removed once we use <profile>/storage. See bug 1798015.
+async function removeAllEntries() {
+ const root = await navigator.storage.getDirectory();
+ for await (const value of root.values()) {
+ root.removeEntry(value.name, { recursive: true });
+ }
+}
+
+add_setup(async function () {
+ const { setStoragePrefs, clearStoragesForOrigin } = await import(
+ "/tests/dom/quota/test/modules/StorageUtils.mjs"
+ );
+
+ const optionalPrefsToSet = [
+ ["dom.fs.enabled", true],
+ ["dom.fs.writable_file_stream.enabled", true],
+ ["dom.workers.modules.enabled", true],
+ ];
+
+ await setStoragePrefs(optionalPrefsToSet);
+
+ SimpleTest.registerCleanupFunction(async function () {
+ await removeAllEntries();
+
+ await clearStoragesForOrigin(SpecialPowers.wrap(document).nodePrincipal);
+ });
+});
diff --git a/dom/fs/test/mochitest/mochitest.toml b/dom/fs/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..e138469768
--- /dev/null
+++ b/dom/fs/test/mochitest/mochitest.toml
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+skip-if = ["xorigin"]
+support-files = ["head.js"]
+
+# Skip all tests if xorigin since we'll fail GetStorage() with ePartitionForeignOrDeny
+
+["test_basics.html"]
+scheme = "https"
+skip-if = [
+ "os == 'win'", # Bug 1841281
+ "os == 'linux' && debug", # Bug 1841281
+ "os == 'mac' && debug", # Bug 1841281
+]
+
+["test_basics_worker.html"]
+scheme = "https"
+
+["test_fileSystemDirectoryHandle.html"]
+scheme = "https"
+
+["test_fileSystemDirectoryHandle_worker.html"]
+scheme = "https"
+
+["test_syncAccessHandle_worker.html"]
+scheme = "https"
+
+["test_writableFileStream.html"]
+scheme = "https"
+
+["test_writableFileStream_worker.html"]
+scheme = "https"
diff --git a/dom/fs/test/mochitest/test_basics.html b/dom/fs/test/mochitest/test_basics.html
new file mode 100644
index 0000000000..5c609233b5
--- /dev/null
+++ b/dom/fs/test/mochitest/test_basics.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_basics.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_basics_worker.html b/dom/fs/test/mochitest/test_basics_worker.html
new file mode 100644
index 0000000000..72ad7b8c41
--- /dev/null
+++ b/dom/fs/test/mochitest/test_basics_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_basics_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html
new file mode 100644
index 0000000000..35f3a72e2f
--- /dev/null
+++ b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_fileSystemDirectoryHandle.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html
new file mode 100644
index 0000000000..2f52079435
--- /dev/null
+++ b/dom/fs/test/mochitest/test_fileSystemDirectoryHandle_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_fileSystemDirectoryHandle_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_syncAccessHandle_worker.html b/dom/fs/test/mochitest/test_syncAccessHandle_worker.html
new file mode 100644
index 0000000000..42980b1d55
--- /dev/null
+++ b/dom/fs/test/mochitest/test_syncAccessHandle_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_syncAccessHandle_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_writableFileStream.html b/dom/fs/test/mochitest/test_writableFileStream.html
new file mode 100644
index 0000000000..3d39349ef5
--- /dev/null
+++ b/dom/fs/test/mochitest/test_writableFileStream.html
@@ -0,0 +1,31 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_writableFileStream.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/test_writableFileStream_worker.html b/dom/fs/test/mochitest/test_writableFileStream_worker.html
new file mode 100644
index 0000000000..a762e78c3f
--- /dev/null
+++ b/dom/fs/test/mochitest/test_writableFileStream_worker.html
@@ -0,0 +1,22 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>File System Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript">
+ add_task(async function worker() {
+ await run_test_in_worker("worker/test_writableFileStream_worker.js");
+ });
+ </script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/dom/fs/test/mochitest/worker/.eslintrc.js b/dom/fs/test/mochitest/worker/.eslintrc.js
new file mode 100644
index 0000000000..93bf938654
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/.eslintrc.js
@@ -0,0 +1,12 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+};
diff --git a/dom/fs/test/mochitest/worker/dummy.js b/dom/fs/test/mochitest/worker/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/dummy.js
diff --git a/dom/fs/test/mochitest/worker/head.js b/dom/fs/test/mochitest/worker/head.js
new file mode 100644
index 0000000000..72e1869bde
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/head.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ importScripts("/tests/dom/quota/test/modules/worker/ModuleLoader.js");
+
+ const base = location.href;
+
+ const depth = "../../../../../";
+
+ importScripts("/tests/dom/quota/test/modules/worker/Assert.js");
+
+ importScripts("/tests/dom/quota/test/modules/worker/Utils.js");
+
+ require_module.moduleLoader = new globalThis.ModuleLoader(base, depth);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
diff --git a/dom/fs/test/mochitest/worker/mochitest.toml b/dom/fs/test/mochitest/worker/mochitest.toml
new file mode 100644
index 0000000000..830c8f267d
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/mochitest.toml
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files = [
+ "head.js",
+ "test_basics_worker.js",
+ "test_fileSystemDirectoryHandle_worker.js",
+ "test_syncAccessHandle_worker.js",
+ "test_writableFileStream_worker.js",
+]
+
+["dummy.js"]
+skip-if = ["true"]
diff --git a/dom/fs/test/mochitest/worker/test_basics_worker.js b/dom/fs/test/mochitest/worker/test_basics_worker.js
new file mode 100644
index 0000000000..e4a4958071
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_basics_worker.js
@@ -0,0 +1,11 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module("dom/fs/test/common/test_basics.js");
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..d4ba0b387c
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_fileSystemDirectoryHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js b/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..be53a5a7e1
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_syncAccessHandle_worker.js
@@ -0,0 +1,16 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_syncAccessHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+});
diff --git a/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js b/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..f294a719db
--- /dev/null
+++ b/dom/fs/test/mochitest/worker/test_writableFileStream_worker.js
@@ -0,0 +1,16 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_writableFileStream.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ // We can't shrink storage size in a mochitest.
+ if (testItem.name != "quotaTest") {
+ add_task(testItem);
+ }
+ });
+});
diff --git a/dom/fs/test/moz.build b/dom/fs/test/moz.build
new file mode 100644
index 0000000000..5f4ac34fca
--- /dev/null
+++ b/dom/fs/test/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += [
+ "common",
+ "gtest",
+ "xpcshell",
+]
+
+MOCHITEST_MANIFESTS += [
+ "mochitest/mochitest.toml",
+ "mochitest/worker/mochitest.toml",
+]
diff --git a/dom/fs/test/xpcshell/head.js b/dom/fs/test/xpcshell/head.js
new file mode 100644
index 0000000000..4138e46ac9
--- /dev/null
+++ b/dom/fs/test/xpcshell/head.js
@@ -0,0 +1,100 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ const { ModuleLoader } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/ModuleLoader.sys.mjs"
+ );
+
+ const base = Services.io.newFileURI(do_get_file("")).spec;
+
+ const depth = "../../../../";
+
+ Cu.importGlobalProperties(["storage"]);
+
+ const { Utils } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/Utils.sys.mjs"
+ );
+
+ const proto = {
+ Assert,
+ Cr,
+ DOMException,
+ navigator: {
+ storage,
+ },
+ TextEncoder,
+ Utils,
+ };
+
+ require_module.moduleLoader = new ModuleLoader(base, depth, proto);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
+
+async function run_test_in_worker(script) {
+ const { runTestInWorker } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/WorkerDriver.sys.mjs"
+ );
+
+ const base = "resource://testing-common/dom/fs/test/xpcshell/";
+
+ const listener = {
+ onOk(value, message) {
+ ok(value, message);
+ },
+ onIs(a, b, message) {
+ Assert.equal(a, b, message);
+ },
+ onInfo(message) {
+ info(message);
+ },
+ };
+
+ await runTestInWorker(script, base, listener);
+}
+
+add_setup(async function () {
+ const {
+ setStoragePrefs,
+ clearStoragePrefs,
+ clearStoragesForOrigin,
+ resetStorage,
+ } = ChromeUtils.importESModule(
+ "resource://testing-common/dom/quota/test/modules/StorageUtils.sys.mjs"
+ );
+
+ const optionalPrefsToSet = [
+ ["dom.fs.enabled", true],
+ ["dom.fs.writable_file_stream.enabled", true],
+ ];
+
+ setStoragePrefs(optionalPrefsToSet);
+
+ registerCleanupFunction(async function () {
+ const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+
+ await clearStoragesForOrigin(principal);
+
+ Services.prefs.clearUserPref(
+ "dom.quotaManager.temporaryStorage.fixedLimit"
+ );
+
+ await resetStorage();
+
+ const optionalPrefsToClear = [
+ "dom.fs.enabled",
+ "dom.fs.writable_file_stream.enabled",
+ ];
+
+ clearStoragePrefs(optionalPrefsToClear);
+ });
+
+ do_get_profile();
+});
diff --git a/dom/fs/test/xpcshell/moz.build b/dom/fs/test/xpcshell/moz.build
new file mode 100644
index 0000000000..f18cc1c466
--- /dev/null
+++ b/dom/fs/test/xpcshell/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += [
+ "worker",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell.toml",
+]
diff --git a/dom/fs/test/xpcshell/test_basics.js b/dom/fs/test/xpcshell/test_basics.js
new file mode 100644
index 0000000000..9c70eaceb3
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_basics.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_basics.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_basics_worker.js b/dom/fs/test/xpcshell/test_basics_worker.js
new file mode 100644
index 0000000000..4569a6a88c
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_basics_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_basics_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js
new file mode 100644
index 0000000000..6349d9aab8
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_fileSystemDirectoryHandle.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..524b7352b4
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_fileSystemDirectoryHandle_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js b/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..377efab598
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_syncAccessHandle_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_syncAccessHandle_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/test_writableFileStream.js b/dom/fs/test/xpcshell/test_writableFileStream.js
new file mode 100644
index 0000000000..1ac9fdb793
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_writableFileStream.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testSet = "dom/fs/test/common/test_writableFileStream.js";
+
+ const testCases = await require_module(testSet);
+
+ Object.values(testCases).forEach(testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/test_writableFileStream_worker.js b/dom/fs/test/xpcshell/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..879ca6c0ca
--- /dev/null
+++ b/dom/fs/test/xpcshell/test_writableFileStream_worker.js
@@ -0,0 +1,8 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function worker() {
+ await run_test_in_worker("worker/test_writableFileStream_worker.js");
+});
diff --git a/dom/fs/test/xpcshell/worker/.eslintrc.js b/dom/fs/test/xpcshell/worker/.eslintrc.js
new file mode 100644
index 0000000000..93bf938654
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/.eslintrc.js
@@ -0,0 +1,12 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+};
diff --git a/dom/fs/test/xpcshell/worker/dummy.js b/dom/fs/test/xpcshell/worker/dummy.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/dummy.js
diff --git a/dom/fs/test/xpcshell/worker/head.js b/dom/fs/test/xpcshell/worker/head.js
new file mode 100644
index 0000000000..06a779841f
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/head.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function require_module(id) {
+ if (!require_module.moduleLoader) {
+ importScripts("/dom/quota/test/modules/worker/ModuleLoader.js");
+
+ const base = location.href;
+
+ const depth = "../../../../../";
+
+ importScripts("/dom/quota/test/modules/worker/Assert.js");
+
+ importScripts("/dom/quota/test/modules/worker/Utils.js");
+
+ require_module.moduleLoader = new globalThis.ModuleLoader(base, depth);
+ }
+
+ return require_module.moduleLoader.require(id);
+}
diff --git a/dom/fs/test/xpcshell/worker/moz.build b/dom/fs/test/xpcshell/worker/moz.build
new file mode 100644
index 0000000000..c0230d1927
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TESTING_JS_MODULES.dom.fs.test.xpcshell.worker += [
+ "head.js",
+ "test_basics_worker.js",
+ "test_fileSystemDirectoryHandle_worker.js",
+ "test_syncAccessHandle_worker.js",
+ "test_writableFileStream_worker.js",
+]
diff --git a/dom/fs/test/xpcshell/worker/test_basics_worker.js b/dom/fs/test/xpcshell/worker/test_basics_worker.js
new file mode 100644
index 0000000000..e4a4958071
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_basics_worker.js
@@ -0,0 +1,11 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module("dom/fs/test/common/test_basics.js");
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js b/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js
new file mode 100644
index 0000000000..d4ba0b387c
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_fileSystemDirectoryHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_fileSystemDirectoryHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js b/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js
new file mode 100644
index 0000000000..e6c6a96143
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_syncAccessHandle_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_syncAccessHandle.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js b/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js
new file mode 100644
index 0000000000..1e9bb12ae8
--- /dev/null
+++ b/dom/fs/test/xpcshell/worker/test_writableFileStream_worker.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function init() {
+ const testCases = await require_module(
+ "dom/fs/test/common/test_writableFileStream.js"
+ );
+ Object.values(testCases).forEach(async testItem => {
+ add_task(testItem);
+ });
+});
diff --git a/dom/fs/test/xpcshell/xpcshell.toml b/dom/fs/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..564c4f819b
--- /dev/null
+++ b/dom/fs/test/xpcshell/xpcshell.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+head = "head.js"
+
+["test_basics.js"]
+
+["test_basics_worker.js"]
+
+["test_fileSystemDirectoryHandle.js"]
+
+["test_fileSystemDirectoryHandle_worker.js"]
+
+["test_syncAccessHandle_worker.js"]
+
+["test_writableFileStream.js"]
+
+["test_writableFileStream_worker.js"]