summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/file-system-access/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/file-system-access/resources')
-rw-r--r--testing/web-platform/tests/file-system-access/resources/data/testfile.txt1
-rw-r--r--testing/web-platform/tests/file-system-access/resources/local-fs-test-helpers.js182
-rw-r--r--testing/web-platform/tests/file-system-access/resources/message-target-dedicated-worker.js9
-rw-r--r--testing/web-platform/tests/file-system-access/resources/message-target-service-worker.js9
-rw-r--r--testing/web-platform/tests/file-system-access/resources/message-target-shared-worker.js14
-rw-r--r--testing/web-platform/tests/file-system-access/resources/message-target.html22
-rw-r--r--testing/web-platform/tests/file-system-access/resources/message-target.js157
-rw-r--r--testing/web-platform/tests/file-system-access/resources/messaging-blob-helpers.js51
-rw-r--r--testing/web-platform/tests/file-system-access/resources/messaging-helpers.js187
-rw-r--r--testing/web-platform/tests/file-system-access/resources/messaging-serialize-helpers.js230
-rw-r--r--testing/web-platform/tests/file-system-access/resources/opaque-origin-sandbox.html39
-rw-r--r--testing/web-platform/tests/file-system-access/resources/test-helpers.js80
12 files changed, 981 insertions, 0 deletions
diff --git a/testing/web-platform/tests/file-system-access/resources/data/testfile.txt b/testing/web-platform/tests/file-system-access/resources/data/testfile.txt
new file mode 100644
index 0000000000..980a0d5f19
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/data/testfile.txt
@@ -0,0 +1 @@
+Hello World!
diff --git a/testing/web-platform/tests/file-system-access/resources/local-fs-test-helpers.js b/testing/web-platform/tests/file-system-access/resources/local-fs-test-helpers.js
new file mode 100644
index 0000000000..54961ae54b
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/local-fs-test-helpers.js
@@ -0,0 +1,182 @@
+// This file defines a directory_test() function that can be used to define
+// tests that require a FileSystemDirectoryHandle. The implementation of that
+// function in this file will ask the user to select an empty directory and uses
+// that directory.
+//
+// Another implementation of this function exists in
+// fs/resources/sandboxed-fs-test-helpers.js, where that version uses the
+// sandboxed file system instead.
+
+const directory_promise = (async () => {
+ await new Promise(resolve => {
+ window.addEventListener('DOMContentLoaded', resolve);
+ });
+
+ // Small delay to give chrome's test automation a chance to actually install
+ // itself.
+ await new Promise(resolve => step_timeout(resolve, 100));
+
+ await window.test_driver.bless(
+ 'show a file picker.<br />Please select an empty directory');
+ const entries = await self.showDirectoryPicker();
+ assert_true(entries instanceof FileSystemHandle);
+ assert_true(entries instanceof FileSystemDirectoryHandle);
+ for await (const entry of entries) {
+ assert_unreached('Selected directory is not empty');
+ }
+ return entries;
+})();
+
+function directory_test(func, description) {
+ promise_test(async t => {
+ const directory = await directory_promise;
+ // To be resilient against tests not cleaning up properly, cleanup before
+ // every test.
+ for await (let entry of directory.values()) {
+ await directory.removeEntry(
+ entry.name, {recursive: entry.kind === 'directory'});
+ }
+ await func(t, directory);
+ }, description);
+}
+
+directory_test(async (t, dir) => {
+ assert_equals(await dir.queryPermission({mode: 'read'}), 'granted');
+}, 'User succesfully selected an empty directory.');
+
+directory_test(async (t, dir) => {
+ const status = await dir.queryPermission({mode: 'readwrite'});
+ if (status == 'granted')
+ return;
+
+ await window.test_driver.bless('ask for write permission');
+ assert_equals(await dir.requestPermission({mode: 'readwrite'}), 'granted');
+}, 'User granted write access.');
+
+const child_frame_js = (origin, frameFn, done) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${done}", ${frameFn}("${origin}"));
+`;
+
+/**
+ * Context identifiers for executor subframes of framed tests. Individual
+ * contexts (or convenience context lists below) can be used to send JavaScript
+ * for evaluation in each frame (see framed_test below).
+ *
+ * Note that within framed tests:
+ * - firstParty represents the top-level document.
+ * - thirdParty represents an embedded context (iframe).
+ * - ancestorBit contexts include a cross-site ancestor iframe.
+ * - anonymousFrame contexts are third-party anonymous iframe contexts.
+ */
+const FRAME_CONTEXT = {
+ firstParty: 0,
+ thirdPartySameSite: 1,
+ thirdPartySameSite_AncestorBit: 2,
+ thirdPartyCrossSite: 3,
+ anonymousFrameSameSite: 4,
+ anonymousFrameSameSite_AncestorBit: 5,
+ anonymousFrameCrossSite: 6,
+};
+
+// TODO(crbug.com/1322897): Add AncestorBit contexts.
+const sameSiteContexts = [
+ FRAME_CONTEXT.firstParty,
+ FRAME_CONTEXT.thirdPartySameSite,
+ FRAME_CONTEXT.anonymousFrameSameSite,
+];
+
+// TODO(crbug.com/1322897): Add AncestorBit contexts.
+const crossSiteContexts = [
+ FRAME_CONTEXT.thirdPartyCrossSite,
+ FRAME_CONTEXT.anonymousFrameCrossSite,
+];
+
+// TODO(crbug.com/1322897): Add AncestorBit contexts.
+const childContexts = [
+ FRAME_CONTEXT.thirdPartySameSite,
+ FRAME_CONTEXT.thirdPartyCrossSite,
+ FRAME_CONTEXT.anonymousFrameSameSite,
+ FRAME_CONTEXT.anonymousFrameCrossSite,
+];
+
+/**
+ * Creates a promise test with same- & cross-site executor subframes.
+ *
+ * In addition to the standard testing object, the provided func will be called
+ * with a sendTo function. sendTo expects:
+ * - contexts: an Iterable of FRAME_CONTEXT constants representing the
+ * frame(s) in which the provided script will be concurrently run.
+ * - js_gen: a function which should generate a script string when called
+ * with a string token. sendTo will wait until a "done" message
+ * is sent to this queue.
+ */
+function framed_test(func, description) {
+ const same_site_origin = get_host_info().HTTPS_ORIGIN;
+ const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+ const frames = Object.values(FRAME_CONTEXT);
+
+ promise_test(async (t) => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ // Set up handles to all third party frames.
+ const handles = [
+ null, // firstParty
+ newIframe(same_site_origin), // thirdPartySameSite
+ null, // thirdPartySameSite_AncestorBit
+ newIframe(cross_site_origin), // thirdPartyCrossSite
+ newAnonymousIframe(same_site_origin), // anonymousFrameSameSite
+ null, // anonymousFrameSameSite_AncestorBit
+ newAnonymousIframe(cross_site_origin), // anonymousFrameCrossSite
+ ];
+ // Set up nested SameSite frames for ancestor bit contexts.
+ const setUpQueue = token();
+ send(newIframe(cross_site_origin),
+ child_frame_js(same_site_origin, "newIframe", setUpQueue));
+ handles[FRAME_CONTEXT.thirdPartySameSite_AncestorBit] =
+ await receive(setUpQueue);
+ send(newAnonymousIframe(cross_site_origin),
+ child_frame_js(same_site_origin, "newAnonymousIframe", setUpQueue));
+ handles[FRAME_CONTEXT.anonymousFrameSameSite_AncestorBit] =
+ await receive(setUpQueue);
+
+ const sendTo = (contexts, js_generator) => {
+ // Send to all contexts in parallel to minimize timeout concerns.
+ return Promise.all(contexts.map(async (context) => {
+ const queue = token();
+ const js_string = js_generator(queue, context);
+ switch (context) {
+ case FRAME_CONTEXT.firstParty:
+ // Code is executed directly in this frame via eval() rather
+ // than in a new context to avoid differences in API access.
+ eval(`(async () => {${js_string}})()`);
+ break;
+ case FRAME_CONTEXT.thirdPartySameSite:
+ case FRAME_CONTEXT.thirdPartyCrossSite:
+ case FRAME_CONTEXT.anonymousFrameSameSite:
+ case FRAME_CONTEXT.anonymousFrameCrossSite:
+ case FRAME_CONTEXT.thirdPartySameSite_AncestorBit:
+ case FRAME_CONTEXT.anonymousFrameSameSite_AncestorBit:
+ send(handles[context], js_string);
+ break;
+ default:
+ reject(`Cannot execute in context: ${context}`);
+ }
+ if (await receive(queue) != "done") {
+ reject(`Script failed in frame ${context}: ${js_string}`);
+ }
+ }));
+ };
+
+ await func(t, sendTo);
+ } catch (e) {
+ reject(e);
+ }
+ resolve();
+ });
+ }, description);
+}
diff --git a/testing/web-platform/tests/file-system-access/resources/message-target-dedicated-worker.js b/testing/web-platform/tests/file-system-access/resources/message-target-dedicated-worker.js
new file mode 100644
index 0000000000..26ff23ef8a
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/message-target-dedicated-worker.js
@@ -0,0 +1,9 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+add_message_event_handlers(/*receiver=*/self, /*target=*/self);
diff --git a/testing/web-platform/tests/file-system-access/resources/message-target-service-worker.js b/testing/web-platform/tests/file-system-access/resources/message-target-service-worker.js
new file mode 100644
index 0000000000..4a6174ae3b
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/message-target-service-worker.js
@@ -0,0 +1,9 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+add_message_event_handlers(/*receiver=*/self); \ No newline at end of file
diff --git a/testing/web-platform/tests/file-system-access/resources/message-target-shared-worker.js b/testing/web-platform/tests/file-system-access/resources/message-target-shared-worker.js
new file mode 100644
index 0000000000..6829c61d4c
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/message-target-shared-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+importScripts(
+ 'test-helpers.js',
+ 'messaging-serialize-helpers.js',
+ 'message-target.js'
+);
+
+self.addEventListener('connect', connect_event => {
+ const message_port = connect_event.ports[0];
+ add_message_event_handlers(
+ /*receiver=*/message_port, /*target=*/message_port);
+ message_port.start();
+}); \ No newline at end of file
diff --git a/testing/web-platform/tests/file-system-access/resources/message-target.html b/testing/web-platform/tests/file-system-access/resources/message-target.html
new file mode 100644
index 0000000000..32c7f0c56c
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/message-target.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src='test-helpers.js'></script>
+<script src='messaging-serialize-helpers.js'></script>
+<script src='message-target.js'></script>
+<script id="inline_script">
+ 'use strict'
+
+ if (window.parent !== null) {
+ window.parent.postMessage('LOADED', { targetOrigin: '*' });
+ }
+
+ if (window.opener !== null) {
+ window.opener.postMessage('LOADED', { targetOrigin: '*' });
+ }
+
+ // Use an undefined message target to send responses to
+ // MessageEvent::source instead.
+ const target = undefined;
+
+ add_message_event_handlers(
+ /*receiver=*/self, target, /*target_origin=*/'*');
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/file-system-access/resources/message-target.js b/testing/web-platform/tests/file-system-access/resources/message-target.js
new file mode 100644
index 0000000000..191b4748ab
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/message-target.js
@@ -0,0 +1,157 @@
+'use strict';
+
+// This script depends on the following scripts:
+// /file-system-access/resources/messaging-helpers.js
+// /file-system-access/resources/test-helpers.js
+
+// add_message_event_handlers() is the helper function used to setup all
+// message targets, including iframes and workers.
+//
+// Adds a message event handler and a message error handler to |receiver|.
+// The 'data' property from received MessageEvents must include a 'type'
+// property. The 'type' selects the test logic to run. Most message type
+// handlers use postMessage() to respond to the sender with test results.
+// The sender then validates the test results after receiving the response.
+//
+// Both |target| and |target_origin| are optional. |target| is used
+// to send message responses back to the sender. When omitted, the
+// 'source' from received MessageEvents is used instead.
+//
+// For window messaging, |target_origin| specifies the origin to receive
+// responses. Most window tests use '*' for the |target_origin|. Worker
+// and message port tests must use undefined for |target_origin| to avoid
+// exceptions.
+function add_message_event_handlers(receiver, target, target_origin) {
+ receiver.addEventListener('message', async function (message_event) {
+ const message_data = message_event.data;
+
+ // Reply to the sender using the 'source' from the received MessageEvent.
+ let message_source = message_event.source;
+ if (message_source === null) {
+ // However, some message senders, like DedicatedWorkers, don't include
+ // a source. Fallback to the target when the source is null.
+ message_source = target;
+ }
+
+ try {
+ switch (message_data.type) {
+ case 'receive-message-port':
+ // Receive a MessagePort to use as a message target for testing.
+ add_message_event_handlers(
+ /*receiver=*/message_data.message_port,
+ /*target=*/message_data.message_port);
+ message_data.message_port.start();
+ break;
+
+ case 'create-broadcast-channel':
+ // Create a BroadcastChannel to use as a message target for testing.
+ const broadcast_channel =
+ new BroadcastChannel(message_data.broadcast_channel_name);
+ add_message_event_handlers(
+ /*receiver=*/broadcast_channel,
+ /*target=*/broadcast_channel);
+ message_source.postMessage(
+ { type: 'broadcast-channel-created' },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'receive-file-system-handles':
+ // Receive a list of cloned FileSystemFileHandles. Access the
+ // properties of each FileSystemFileHandle by serializing the
+ // handle to a JavaScript object. Then respond with the serialized
+ // results, enabling the sender to verify that the cloned handle
+ // produced the expected property values from this execution context.
+ const serialized_handles = [];
+ const cloned_handles = message_data.cloned_handles;
+ for (let i = 0; i < cloned_handles.length; ++i) {
+ const serialized = await serialize_handle(cloned_handles[i]);
+ serialized_handles.push(serialized);
+ }
+ message_source.postMessage({
+ type: 'receive-serialized-file-system-handles',
+ serialized_handles,
+ // Respond with the cloned handles to create new clones for
+ // the sender to verify.
+ cloned_handles,
+ }, { targetOrigin: target_origin });
+ break;
+
+ case 'receive-serialized-file-system-handles':
+ // Do nothing. This message is meant for test runner validation.
+ // Other message targets may receive this message while testing
+ // broadcast channels.
+ break;
+
+ case 'create-file':
+ // Create a new file and then respond to the sender with it.
+ const directory = await navigator.storage.getDirectory();
+ const file_handle =
+ await directory.getFileHandle('temp-file', { create: true });
+ message_source.postMessage(
+ { type: 'receive-file', file_handle },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'create-directory':
+ // Create a new directory and then respond to the sender with it.
+ const parent_directory = await navigator.storage.getDirectory();
+ const directory_handle =
+ await parent_directory.getDirectoryHandle('temp-directory',
+ { create: true });
+ message_source.postMessage(
+ { type: 'receive-directory', directory_handle },
+ { targetOrigin: target_origin });
+ break;
+
+ case 'create-sync-access-handle':
+ // Receive a file and create a sync access handle out of it. Report
+ // success to the sender.
+ let success = true;
+ try {
+ const access_handle = await message_data.file_handle.createSyncAccessHandle();
+ access_handle.close();
+ } catch (error) {
+ success = false;
+ }
+
+ message_source.postMessage(
+ { type: 'receive-sync-access-handle-result', success },
+ { targetOrigin: target_origin });
+ break;
+
+ default:
+ throw `Unknown message type: '${message_data.type}'`;
+ }
+ } catch (error) {
+ // Respond with an error to trigger a failure in the sender's
+ // test runner.
+ message_source.postMessage(`ERROR: ${error}`,
+ { targetOrigin: target_origin });
+ }
+ });
+
+ receiver.addEventListener('messageerror', async function (message_event) {
+ // Select the target for message responses (see comment in 'message' event
+ // listener above).
+ let message_source = message_event.source;
+ if (message_source === null) {
+ message_source = target;
+ }
+
+ try {
+ // Respond with the MessageEvent's property values, enabling the sender
+ // to verify results.
+ const serialized_message_error_event =
+ serialize_message_error_event(message_event);
+ message_source.postMessage({
+ type: 'serialized-message-error',
+ serialized_message_error_event
+ }, { targetOrigin: target_origin });
+ } catch (error) {
+ // Respond with an error to trigger a failure in the sender's
+ // test runner.
+ message_source.postMessage(`ERROR: ${error}`,
+ { targetOrigin: target_origin });
+ }
+ });
+}
diff --git a/testing/web-platform/tests/file-system-access/resources/messaging-blob-helpers.js b/testing/web-platform/tests/file-system-access/resources/messaging-blob-helpers.js
new file mode 100644
index 0000000000..852f2e2d32
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/messaging-blob-helpers.js
@@ -0,0 +1,51 @@
+'use strict';
+
+// Creates a blob URL with the contents of 'message-target.html'. Use the
+// blob as an iframe src or a window.open() URL, which creates a same origin
+// message target.
+async function create_message_target_blob_url(test) {
+ const html = await create_message_target_html_without_subresources(test);
+ const blob = new Blob([html], { type: 'text/html' });
+ return URL.createObjectURL(blob);
+}
+
+// Creates a data URI with the contents of 'message-target.html'. Use the
+// data URI as an iframe src, which creates a cross origin message target.
+async function create_message_target_data_uri(test) {
+ const iframe_html =
+ await create_message_target_html_without_subresources(test);
+ return `data:text/html,${encodeURIComponent(iframe_html)}`;
+}
+
+// Constructs a version of 'message-target.html' without any subresources.
+// Enables the creation of blob URLs, data URIs and iframe srcdocs re-using
+// the contents of 'message-target.html'.
+async function create_message_target_html_without_subresources(test) {
+ const test_helpers_script = await fetch_text('resources/test-helpers.js');
+
+ const messaging_helpers_script =
+ await fetch_text('resources/messaging-helpers.js');
+
+ const messaging_serialize_helpers_script =
+ await fetch_text('resources/messaging-serialize-helpers.js');
+
+ const message_target_script =
+ await fetch_text('resources/message-target.js');
+
+ // Get the inline script code from 'message-target.html'.
+ const iframe = await add_iframe(test, { src: 'resources/message-target.html' });
+ const iframe_script =
+ iframe.contentWindow.document.getElementById('inline_script').outerHTML;
+ iframe.remove();
+
+ return '<!DOCTYPE html>' +
+ `<script>${test_helpers_script}</script>` +
+ `<script>${messaging_serialize_helpers_script}</script>` +
+ `<script>${message_target_script}</script>` +
+ `${iframe_script}`;
+}
+
+async function fetch_text(url) {
+ const response = await fetch(url);
+ return await response.text();
+}
diff --git a/testing/web-platform/tests/file-system-access/resources/messaging-helpers.js b/testing/web-platform/tests/file-system-access/resources/messaging-helpers.js
new file mode 100644
index 0000000000..55fc04ab81
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/messaging-helpers.js
@@ -0,0 +1,187 @@
+'use strict';
+
+// This script depends on the following script:
+// /file-system-access/resources/test-helpers.js
+// /service-workers/service-worker/resources/test-helpers.sub.js
+
+// Define the URL constants used for each type of message target, including
+// iframes and workers.
+const kDocumentMessageTarget = 'resources/message-target.html';
+const kSharedWorkerMessageTarget = 'resources/message-target-shared-worker.js';
+const kServiceWorkerMessageTarget =
+ 'resources/message-target-service-worker.js';
+const kDedicatedWorkerMessageTarget =
+ 'resources/message-target-dedicated-worker.js';
+
+function create_dedicated_worker(test, url) {
+ const dedicated_worker = new Worker(url);
+ test.add_cleanup(() => {
+ dedicated_worker.terminate();
+ });
+ return dedicated_worker;
+}
+
+async function create_service_worker(test, script_url, scope) {
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope);
+ test.add_cleanup(() => {
+ return registration.unregister();
+ });
+ return registration;
+}
+
+// Creates an iframe and waits to receive a message from the iframe.
+// Valid |options| include src, srcdoc and sandbox, which mirror the
+// corresponding iframe element properties.
+async function add_iframe(test, options) {
+ const iframe = document.createElement('iframe');
+
+ if (options.sandbox !== undefined) {
+ iframe.sandbox = options.sandbox;
+ }
+
+ if (options.src !== undefined) {
+ iframe.src = options.src;
+ }
+
+ if (options.srcdoc !== undefined) {
+ iframe.srcdoc = options.srcdoc;
+ }
+
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => {
+ iframe.remove();
+ });
+
+ await wait_for_loaded_message(self);
+ return iframe;
+}
+
+// Creates a child window using window.open() and waits to receive a message
+// from the child window.
+async function open_window(test, url) {
+ const child_window = window.open(url);
+ test.add_cleanup(() => {
+ child_window.close();
+ });
+ await wait_for_loaded_message(self);
+ return child_window;
+}
+
+// Wait until |receiver| gets a message event with the data set to 'LOADED'.
+// The postMessage() tests use messaging instead of the loaded event because
+// cross-origin child windows from window.open() do not dispatch the loaded
+// event to the parent window.
+async function wait_for_loaded_message(receiver) {
+ const message_promise = new Promise((resolve, reject) => {
+ receiver.addEventListener('message', message_event => {
+ if (message_event.data === 'LOADED') {
+ resolve();
+ } else {
+ reject('The message target must receive a "LOADED" message response.');
+ }
+ });
+ });
+ await message_promise;
+}
+
+// Sets up a new message channel. Sends one port to |target| and then returns
+// the other port.
+function create_message_channel(target, target_origin) {
+ const message_channel = new MessageChannel();
+
+ const message_data =
+ { type: 'receive-message-port', message_port: message_channel.port2 };
+ target.postMessage(
+ message_data,
+ {
+ transfer: [message_channel.port2],
+ targetOrigin: target_origin
+ });
+ message_channel.port1.start();
+ return message_channel.port1;
+}
+
+// Creates a variety of different FileSystemFileHandles for testing.
+async function create_file_system_handles(test, root) {
+ // Create some files to use with postMessage().
+ const empty_file = await createEmptyFile(test, 'empty-file', root);
+ const first_file = await createFileWithContents(
+ test, 'first-file-with-contents', 'first-text-content', root);
+ const second_file = await createFileWithContents(
+ test, 'second-file-with-contents', 'second-text-content', root);
+
+ // Create an empty directory to use with postMessage().
+ const empty_directory = await createDirectory(test, 'empty-directory', root);
+
+ // Create a directory containing both files and subdirectories to use
+ // with postMessage().
+ const directory_with_files =
+ await createDirectory(test, 'directory-with-files', root);
+ await createFileWithContents(test, 'first-file-in-directory',
+ 'first-directory-text-content', directory_with_files);
+ await createFileWithContents(test, 'second-file-in-directory',
+ 'second-directory-text-content', directory_with_files);
+ const subdirectory =
+ await createDirectory(test, 'subdirectory', directory_with_files);
+ await createFileWithContents(test, 'first-file-in-subdirectory',
+ 'first-subdirectory-text-content', subdirectory);
+
+ return [
+ empty_file,
+ first_file,
+ second_file,
+ // Include the same FileSystemFileHandle twice.
+ second_file,
+ empty_directory,
+ // Include the Same FileSystemDirectoryHandle object twice.
+ empty_directory,
+ directory_with_files
+ ];
+}
+
+// Tests sending an array of FileSystemHandles to |target| with postMessage().
+// The array includes both FileSystemFileHandles and FileSystemDirectoryHandles.
+// After receiving the message, |target| accesses all cloned handles by
+// serializing the properties of each handle to a JavaScript object.
+//
+// |target| then responds with the resulting array of serialized handles. The
+// response also includes the array of cloned handles, which creates more
+// clones. After receiving the response, this test runner verifies that both
+// the serialized handles and the cloned handles contain the expected properties.
+async function do_post_message_test(
+ test, root_dir, receiver, target, target_origin) {
+ // Create and send the handles to |target|.
+ const handles =
+ await create_file_system_handles(test, root_dir, target, target_origin);
+ target.postMessage(
+ { type: 'receive-file-system-handles', cloned_handles: handles },
+ { targetOrigin: target_origin });
+
+ // Wait for |target| to respond with results.
+ const event_watcher = new EventWatcher(test, receiver, 'message');
+ const message_event = await event_watcher.wait_for('message');
+ const response = message_event.data;
+
+ assert_equals(response.type, 'receive-serialized-file-system-handles',
+ 'The test runner must receive a "serialized-file-system-handles" ' +
+ `message response. Actual response: ${response}`);
+
+ // Verify the results.
+ const expected_serialized_handles = await serialize_handles(handles);
+
+ assert_equals_serialized_handles(
+ response.serialized_handles, expected_serialized_handles);
+
+ await assert_equals_cloned_handles(response.cloned_handles, handles);
+}
+
+// Runs the same test as do_post_message_test(), but uses a MessagePort.
+// This test starts by establishing a message channel between the test runner
+// and |target|. Afterwards, the test sends FileSystemHandles through the
+// message port channel.
+async function do_message_port_test(test, root_dir, target, target_origin) {
+ const message_port = create_message_channel(target, target_origin);
+ await do_post_message_test(
+ test, root_dir, /*receiver=*/ message_port, /*target=*/ message_port);
+}
diff --git a/testing/web-platform/tests/file-system-access/resources/messaging-serialize-helpers.js b/testing/web-platform/tests/file-system-access/resources/messaging-serialize-helpers.js
new file mode 100644
index 0000000000..ada68f43db
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/messaging-serialize-helpers.js
@@ -0,0 +1,230 @@
+'use strict';
+
+// This script depends on the following script:
+// /file-system-access/resources/test-helpers.js
+
+// Serializes an array of FileSystemHandles where each element can be either a
+// FileSystemFileHandle or FileSystemDirectoryHandle.
+async function serialize_handles(handle_array) {
+ const serialized_handle_array = [];
+ for (let i = 0; i < handle_array.length; ++i) {
+ serialized_handle_array.push(await serialize_handle(handle_array[i]));
+ }
+ return serialized_handle_array;
+}
+
+// Serializes either a FileSystemFileHandle or FileSystemDirectoryHandle.
+async function serialize_handle(handle) {
+ switch (handle.kind) {
+ case 'directory':
+ return await serialize_file_system_directory_handle(handle);
+ case 'file':
+ return await serialize_file_system_file_handle(handle);
+ default:
+ throw 'Object is not a FileSystemFileHandle or ' +
+ `FileSystemDirectoryHandle ${handle}`;
+ }
+}
+
+// Creates a dictionary for a FileSystemHandle base, which contains
+// serialized properties shared by both FileSystemFileHandle and
+// FileSystemDirectoryHandle.
+async function serialize_file_system_handle(handle) {
+ const read_permission =
+ await handle.queryPermission({ mode: 'read' });
+
+ const write_permission =
+ await handle.queryPermission({ mode: 'readwrite' })
+
+ return {
+ kind: handle.kind,
+ name: handle.name,
+ read_permission,
+ write_permission
+ };
+}
+
+// Create a dictionary with each property value in FileSystemFileHandle.
+// Also, reads the contents of the file to include with the returned
+// dictionary. Example output:
+// {
+// kind: "file",
+// name: "example-file-name"
+// read_permission: "granted",
+// write_permission: "granted",
+// contents: "example-file-contents"
+// }
+async function serialize_file_system_file_handle(file_handle) {
+ const contents = await getFileContents(file_handle);
+
+ const serialized_file_system_handle =
+ await serialize_file_system_handle(file_handle);
+
+ return Object.assign(serialized_file_system_handle, { contents });
+}
+
+// Create a dictionary with each property value in FileSystemDirectoryHandle.
+// Example output:
+// {
+// kind: "directory",
+// name: "example-directory-name"
+// read_permission: "granted",
+// write_permission: "granted",
+// files: [<first serialized file>, ...]
+// directories: [<first serialized subdirectory>, ...]
+// }
+async function serialize_file_system_directory_handle(directory_handle) {
+ // Serialize the contents of the directory.
+ const serialized_files = [];
+ const serialized_directories = [];
+ for await (const child_handle of directory_handle.values()) {
+ const serialized_child_handle = await serialize_handle(child_handle);
+ if (child_handle.kind === "directory") {
+ serialized_directories.push(serialized_child_handle);
+ } else {
+ serialized_files.push(serialized_child_handle);
+ }
+ }
+
+ // Order the serialized contents of the directory by name.
+ serialized_files.sort((left, right) => {
+ return left.name.localeCompare(right.name);
+ });
+ serialized_directories.sort((left, right) => {
+ return left.name.localeCompare(right.name);
+ });
+
+ // Serialize the directory's common properties shared by all
+ // FileSystemHandles.
+ const serialized_file_system_handle =
+ await serialize_file_system_handle(directory_handle);
+
+ return Object.assign(
+ serialized_file_system_handle,
+ { files: serialized_files, directories: serialized_directories });
+}
+
+// Verifies |left_array| is a clone of |right_array| where each element
+// is a cloned FileSystemHandle with the same properties and contents.
+async function assert_equals_cloned_handles(left_array, right_array) {
+ assert_equals(left_array.length, right_array.length,
+ 'Each array of FileSystemHandles must have the same length');
+
+ for (let i = 0; i < left_array.length; ++i) {
+ assert_not_equals(left_array[i], right_array[i],
+ 'Clones must create new FileSystemHandle instances.');
+
+ const left_serialized = await serialize_handle(left_array[i]);
+ const right_serialized = await serialize_handle(right_array[i]);
+ assert_equals_serialized_handle(left_serialized, right_serialized);
+ }
+}
+
+// Verifies |left_array| is the same as |right_array| where each element
+// is a serialized FileSystemHandle with the same properties.
+function assert_equals_serialized_handles(left_array, right_array) {
+ assert_equals(left_array.length, right_array.length,
+ 'Each array of serialized handles must have the same length');
+
+ for (let i = 0; i < left_array.length; ++i) {
+ assert_equals_serialized_handle(left_array[i], right_array[i]);
+ }
+}
+
+// Verifies each property of a serialized FileSystemFileHandle or
+// FileSystemDirectoryHandle.
+function assert_equals_serialized_handle(left, right) {
+ switch (left.kind) {
+ case 'directory':
+ assert_equals_serialized_file_system_directory_handle(left, right);
+ break;
+ case 'file':
+ assert_equals_serialized_file_system_file_handle(left, right);
+ break;
+ default:
+ throw 'Object is not a FileSystemFileHandle or ' +
+ `FileSystemDirectoryHandle ${left}`;
+ }
+}
+
+// Compares the output of serialize_file_system_handle() for
+// two FileSystemHandles.
+function assert_equals_serialized_file_system_handle(left, right) {
+ assert_equals(left.kind, right.kind,
+ 'Each FileSystemHandle instance must use the expected "kind".');
+
+ assert_equals(left.name, right.name,
+ 'Each FileSystemHandle instance must use the expected "name" ' +
+ ' property.');
+
+ assert_equals(left.read_permission, right.read_permission,
+ 'Each FileSystemHandle instance must have the expected read ' +
+ ' permission.');
+
+ assert_equals(left.write_permission, right.write_permission,
+ 'Each FileSystemHandle instance must have the expected write ' +
+ ' permission.');
+}
+
+// Compares the output of serialize_file_system_file_handle()
+// for two FileSystemFileHandle.
+function assert_equals_serialized_file_system_file_handle(left, right) {
+ assert_equals_serialized_file_system_handle(left, right);
+ assert_equals(left.contents, right.contents,
+ 'Each FileSystemFileHandle instance must have the same contents.');
+}
+
+// Compares the output of serialize_file_system_directory_handle()
+// for two FileSystemDirectoryHandles.
+function assert_equals_serialized_file_system_directory_handle(left, right) {
+ assert_equals_serialized_file_system_handle(left, right);
+
+ assert_equals(left.files.length, right.files.length,
+ 'Each FileSystemDirectoryHandle must contain the same number of ' +
+ 'file children');
+
+ for (let i = 0; i < left.files.length; ++i) {
+ assert_equals_serialized_file_system_file_handle(
+ left.files[i], right.files[i]);
+ }
+
+ assert_equals(left.directories.length, right.directories.length,
+ 'Each FileSystemDirectoryHandle must contain the same number of ' +
+ 'directory children');
+
+ for (let i = 0; i < left.directories.length; ++i) {
+ assert_equals_serialized_file_system_directory_handle(
+ left.directories[i], right.directories[i]);
+ }
+}
+
+// Creates a dictionary with interesting property values from MessageEvent.
+function serialize_message_error_event(message_error_event) {
+ return {
+ data: message_error_event.data,
+ origin: message_error_event.origin,
+ last_event_id: message_error_event.lastEventId,
+ has_source: (message_error_event.source !== null),
+ ports_length: message_error_event.ports.length
+ };
+}
+
+// Compares the output of serialize_message_error_event() with an
+// expected result.
+function assert_equals_serialized_message_error_event(
+ serialized_event, expected_origin, expected_has_source) {
+ assert_equals(serialized_event.data, null,
+ 'The message error event must set the "data" property to null.');
+
+ assert_equals(serialized_event.origin, expected_origin,
+ 'The message error event must have the expected "origin" property.');
+
+ assert_equals(serialized_event.last_event_id, "",
+ 'The message error event must set the "lastEventId" property to the empty string.');
+
+ assert_equals(serialized_event.has_source, expected_has_source,
+ 'The message error event must have the expected "source" property.');
+
+ assert_equals(serialized_event.ports_length, 0,
+ 'The message error event must not contain any message ports.');
+}
diff --git a/testing/web-platform/tests/file-system-access/resources/opaque-origin-sandbox.html b/testing/web-platform/tests/file-system-access/resources/opaque-origin-sandbox.html
new file mode 100644
index 0000000000..f489f889b3
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/opaque-origin-sandbox.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<script>
+ 'use strict'
+
+ // Sends two messages to its creator:
+ // (1) The result of showDirectoryPicker().
+ // (2) The result of navigator.storage.getDirectory().
+
+ function post_message(data) {
+ if (window.parent !== null) {
+ window.parent.postMessage(data, { targetOrigin: '*' });
+ }
+ if (window.opener !== null) {
+ window.opener.postMessage(data, { targetOrigin: '*' });
+ }
+ }
+
+ try {
+ window.showDirectoryPicker()
+ .then(() => {
+ post_message('showDirectoryPicker(): FULFILLED');
+ }).catch(error => {
+ post_message(`showDirectoryPicker(): REJECTED: ${error.name}`);
+ });
+ } catch (error) {
+ post_message(`showDirectoryPicker(): EXCEPTION: ${error.name}`);
+ }
+
+ try {
+ navigator.storage.getDirectory()
+ .then(() => {
+ post_message('navigator.storage.getDirectory(): FULFILLED');
+ }).catch(error => {
+ post_message(`navigator.storage.getDirectory(): REJECTED: ${error.name}`);
+ });
+ } catch (error) {
+ post_message(`navigator.storage.getDirectory(): EXCEPTION: ${error.name}`);
+ }
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/file-system-access/resources/test-helpers.js b/testing/web-platform/tests/file-system-access/resources/test-helpers.js
new file mode 100644
index 0000000000..893cd19848
--- /dev/null
+++ b/testing/web-platform/tests/file-system-access/resources/test-helpers.js
@@ -0,0 +1,80 @@
+// A special path component meaning "this directory."
+const kCurrentDirectory = '.';
+
+// A special path component meaning "the parent directory."
+const kParentDirectory = '..';
+
+// Array of separators used to separate components in hierarchical paths.
+let kPathSeparators;
+if (navigator.userAgent.includes('Windows NT')) {
+ // Windows uses both '/' and '\' as path separators.
+ kPathSeparators = ['/', '\\'];
+} else {
+ kPathSeparators = ['/'];
+}
+
+async function getFileSize(handle) {
+ const file = await handle.getFile();
+ return file.size;
+}
+
+async function getFileContents(handle) {
+ const file = await handle.getFile();
+ return new Response(file).text();
+}
+
+async function getDirectoryEntryCount(handle) {
+ let result = 0;
+ for await (let entry of handle) {
+ result++;
+ }
+ return result;
+}
+
+async function getSortedDirectoryEntries(handle) {
+ let result = [];
+ for await (let entry of handle.values()) {
+ if (entry.kind === 'directory')
+ result.push(entry.name + '/');
+ else
+ result.push(entry.name);
+ }
+ result.sort();
+ return result;
+}
+
+async function createDirectory(test, name, parent) {
+ const new_dir_handle = await parent.getDirectoryHandle(name, {create: true});
+ test.add_cleanup(async () => {
+ try {
+ await parent.removeEntry(name, {recursive: true});
+ } catch (e) {
+ // Ignore any errors when removing directories, as tests might
+ // have already removed the directory.
+ }
+ });
+ return new_dir_handle;
+}
+
+async function createEmptyFile(test, name, parent) {
+ const handle = await parent.getFileHandle(name, {create: true});
+ test.add_cleanup(async () => {
+ try {
+ await parent.removeEntry(name);
+ } catch (e) {
+ // Ignore any errors when removing files, as tests might already remove
+ // the file.
+ }
+ });
+ // Make sure the file is empty.
+ assert_equals(await getFileSize(handle), 0);
+ return handle;
+}
+
+async function createFileWithContents(test, name, contents, parent) {
+ const handle = await createEmptyFile(test, name, parent);
+ const writer = await handle.createWritable();
+ await writer.write(new Blob([contents]));
+ await writer.close();
+ return handle;
+}