'use strict'; // This script depends on the following scripts: // resources/test-helpers.js // resources/collecting-file-system-observer.js // resources/change-observer-scope-test.js // script-tests/FileSystemObserver-writable-file-stream.js promise_test(async t => { try { const observer = new FileSystemObserver(() => {}); } catch { assert_unreached(); } }, 'Creating a FileSystemObserver from a supported global succeeds'); directory_test(async (t, root_dir) => { const observer = new FileSystemObserver(() => {}); try { observer.unobserve(root_dir); } catch { assert_unreached(); } }, 'Calling unobserve() without a corresponding observe() shouldn\'t throw'); directory_test(async (t, root_dir) => { const observer = new FileSystemObserver(() => {}); try { observer.unobserve(root_dir); observer.unobserve(root_dir); } catch { assert_unreached(); } }, 'unobserve() is idempotent'); promise_test(async t => { const observer = new FileSystemObserver(() => {}); try { observer.disconnect(); } catch { assert_unreached(); } }, 'Calling disconnect() without observing shouldn\'t throw'); promise_test(async t => { const observer = new FileSystemObserver(() => {}); try { observer.disconnect(); observer.disconnect(); } catch { assert_unreached(); } }, 'disconnect() is idempotent'); directory_test(async (t, root_dir) => { const observer = new FileSystemObserver(() => {}); // Create a `FileSystemFileHandle` and delete its underlying file entry. const file = await root_dir.getFileHandle(getUniqueName(), {create: true}); await file.remove(); await promise_rejects_dom(t, 'NotFoundError', observer.observe(file)); }, 'observe() fails when file does not exist'); directory_test(async (t, root_dir) => { const observer = new FileSystemObserver(() => {}); // Create a `FileSystemDirectoryHandle` and delete its underlying file entry. const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); await dir.remove(); await promise_rejects_dom(t, 'NotFoundError', observer.observe(dir)); }, 'observe() fails when directory does not exist'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const path of scope_test.in_scope_paths(recursive)) { const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Create `file`. const file = await path.createHandle(); // Expect one "appeared" event to happen on `file`. const records = await observer.getRecords(); await assert_records_equal( watched_handle, records, [appearedEvent(file, path.relativePathComponents())]); observer.disconnect(); } } }, 'Creating a file through FileSystemDirectoryHandle.getFileHandle is reported as an "appeared" event if in scope'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const path of scope_test.in_scope_paths(recursive)) { const file = await path.createHandle(); const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Remove `file`. await file.remove(); // Expect one "disappeared" event to happen on `file`. const records = await observer.getRecords(); await assert_records_equal( watched_handle, records, [disappearedEvent(path.relativePathComponents())]); observer.disconnect(); } } }, 'Removing a file through FileSystemFileHandle.remove is reported as an "disappeared" event if in scope'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const path of scope_test.out_of_scope_paths(recursive)) { const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Create and remove `file`. const file = await path.createHandle(); await file.remove(); // Expect the observer to receive no events. const records = await observer.getRecords(); await assert_records_equal(watched_handle, records, []); observer.disconnect(); } } }, 'Events outside the watch scope are not sent to the observer\'s callback'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const src of scope_test.in_scope_paths(recursive)) { for await (const dest of scope_test.in_scope_paths(recursive)) { const file = await src.createHandle(); const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Move `file`. await file.move(dest.parentHandle(), dest.fileName()); // Expect one "moved" event to happen on `file`. const records = await observer.getRecords(); await assert_records_equal( watched_handle, records, [movedEvent( file, dest.relativePathComponents(), src.relativePathComponents())]); observer.disconnect(); } } } }, 'Moving a file through FileSystemFileHandle.move is reported as a "moved" event if destination and source are in scope'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const src of scope_test.out_of_scope_paths(recursive)) { for await (const dest of scope_test.out_of_scope_paths(recursive)) { const file = await src.createHandle(); const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Move `file`. await file.move(dest.parentHandle(), dest.fileName()); // Expect the observer to not receive any events. const records = await observer.getRecords(); await assert_records_equal(watched_handle, records, []); } } } }, 'Moving a file through FileSystemFileHandle.move is not reported if destination and source are not in scope'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const src of scope_test.out_of_scope_paths(recursive)) { for await (const dest of scope_test.in_scope_paths(recursive)) { const file = await src.createHandle(); const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Move `file`. await file.move(dest.parentHandle(), dest.fileName()); // Expect one "appeared" event to happen on `file`. const records = await observer.getRecords(); await assert_records_equal( watched_handle, records, [appearedEvent(file, dest.relativePathComponents())]); } } } }, 'Moving a file through FileSystemFileHandle.move is reported as a "appeared" event if only destination is in scope'); directory_test(async (t, root_dir) => { const dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); const scope_test = new ScopeTest(t, dir); const watched_handle = await scope_test.watched_handle(); for (const recursive of [false, true]) { for await (const src of scope_test.in_scope_paths(recursive)) { for await (const dest of scope_test.out_of_scope_paths(recursive)) { // These both point to the same underlying file entry initially until // move is called on `fileToMove`. `file` is kept so that we have a // handle that still points at the source file entry. const file = await src.createHandle(); const fileToMove = await src.createHandle(); const observer = new CollectingFileSystemObserver(t, root_dir); await observer.observe([watched_handle], {recursive}); // Move `fileToMove`. await fileToMove.move(dest.parentHandle(), dest.fileName()); // Expect one "disappeared" event to happen on `file`. const records = await observer.getRecords(); await assert_records_equal( watched_handle, records, [disappearedEvent(src.relativePathComponents())]); } } } }, 'Moving a file through FileSystemFileHandle.move is reported as a "disappeared" event if only source is in scope'); // Wraps a `CollectingFileSystemObserver` and disconnects the observer after it's // received `num_of_records_to_observe`. class DisconnectingFileSystemObserver { #collectingObserver; #num_of_records_to_observe; #called_disconnect = false; #records_observed_count = 0; constructor(test, root_dir, num_of_records_to_observe) { this.#collectingObserver = new CollectingFileSystemObserver( test, root_dir, this.#callback.bind(this)); this.#num_of_records_to_observe = num_of_records_to_observe; } #callback(records, observer) { this.#records_observed_count += records.length; const called_disconnect = this.#called_disconnect; // Call `disconnect` once after we've received `num_of_records_to_observe`. if (!called_disconnect && this.#records_observed_count >= this.#num_of_records_to_observe) { observer.disconnect(); this.#called_disconnect = true; } return {called_disconnect}; } getRecordsWithCallbackInfo() { return this.#collectingObserver.getRecordsWithCallbackInfo(); } observe(handles) { return this.#collectingObserver.observe(handles); } } directory_test(async (t, root_dir) => { const total_files_to_create = 100; const child_dir = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); // Create a `FileSystemObserver` that will disconnect after its // received half of the total files we're going to create. const observer = new DisconnectingFileSystemObserver( t, root_dir, total_files_to_create / 2); // Observe the child directory and create files in it. await observer.observe([child_dir]); for (let i = 0; i < total_files_to_create; i++) { child_dir.getFileHandle(`file${i}`, {create: true}); } // Wait for `disconnect` to be called. const records_with_disconnect_state = await observer.getRecordsWithCallbackInfo(); // No observations should have been received after disconnected has been // called. assert_false( records_with_disconnect_state.some( ({called_disconnect}) => called_disconnect), 'Received records after disconnect.'); }, 'Observations stop after disconnect()'); directory_test(async (t, root_dir) => { const num_of_child_dirs = 5; const num_files_to_create_per_directory = 100; const total_files_to_create = num_files_to_create_per_directory * num_of_child_dirs; const child_dirs = await createDirectoryHandles( root_dir, getUniqueName(), getUniqueName(), getUniqueName()); // Create a `FileSystemObserver` that will disconnect after its received half // of the total files we're going to create. const observer = new DisconnectingFileSystemObserver( t, root_dir, total_files_to_create / 2); // Observe the child directories and create files in them. await observer.observe(child_dirs); for (let i = 0; i < num_files_to_create_per_directory; i++) { child_dirs.forEach( child_dir => child_dir.getFileHandle(`file${i}`, {create: true})); } // Wait for `disconnect` to be called. const records_with_disconnect_state = await observer.getRecordsWithCallbackInfo(); // No observations should have been received after disconnected has been // called. assert_false( records_with_disconnect_state.some( ({called_disconnect}) => called_disconnect), 'Received records after disconnect.'); }, 'Observations stop for all observed handles after disconnect()');