// Wraps a FileSystemObserver to collect its records until it stops receiving // them. // // To collect records, it sets up a directory to observe and periodically create // files in it. If no new changes occur (outside of these file creations) // between two file changes, then it resolves the promise returned by // getRecords() with the records it collected. class CollectingFileSystemObserver { #observer = new FileSystemObserver(this.#collectRecordsCallback.bind(this)); #notificationObserver = new FileSystemObserver(this.#notificationCallback.bind(this)); #callback; #records_promise_and_resolvers = Promise.withResolvers(); #collected_records = []; #notification_dir_handle; #notification_file_count = 0; #received_changes_since_last_notification = true; constructor(test, root_dir, callback) { test.add_cleanup(() => { this.disconnect(); this.#notificationObserver.disconnect(); }); this.#setupCollectNotification(root_dir); this.#callback = callback ?? (() => {return {}}); } #getCollectNotificationName() { return `notification_file_${this.#notification_file_count}`; } async #setupCollectNotification(root_dir) { this.#notification_dir_handle = await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); await this.#notificationObserver.observe(this.#notification_dir_handle); await this.#createCollectNotification(); } #createCollectNotification() { this.#notification_file_count++; return this.#notification_dir_handle.getFileHandle( this.#getCollectNotificationName(), {create: true}); } #finishCollectingIfReady() { // `records` contains the notification for collecting records. Determine // if we should finish collecting or create the next notification. if (this.#received_changes_since_last_notification) { this.#received_changes_since_last_notification = false; this.#createCollectNotification(); } else { this.#records_promise_and_resolvers.resolve(this.#collected_records); } } #notificationCallback(records) { this.#finishCollectingIfReady(records); } #collectRecordsCallback(records, observer) { this.#collected_records.push({ ...this.#callback(records, observer), records, }); this.#received_changes_since_last_notification = true; } async getRecords() { return (await this.#records_promise_and_resolvers.promise) .map(record => record.records) .flat(); } getRecordsWithCallbackInfo() { return this.#records_promise_and_resolvers.promise; } observe(handles, options) { return Promise.all( handles.map(handle => this.#observer.observe(handle, options))); } disconnect() { this.#observer.disconnect(); } } async function assert_records_equal(root, actual, expected) { assert_equals( actual.length, expected.length, 'Received an unexpected number of events'); for (let i = 0; i < actual.length; i++) { const actual_record = actual[i]; const expected_record = expected[i]; assert_equals( actual_record.type, expected_record.type, 'A record\'s type didn\'t match the expected type'); assert_array_equals( actual_record.relativePathComponents, expected_record.relativePathComponents, 'A record\'s relativePathComponents didn\'t match the expected relativePathComponents'); if (expected_record.relativePathMovedFrom) { assert_array_equals( actual_record.relativePathMovedFrom, expected_record.relativePathMovedFrom, 'A record\'s relativePathMovedFrom didn\'t match the expected relativePathMovedFrom'); } else { assert_equals( actual_record.relativePathMovedFrom, null, 'A record\'s relativePathMovedFrom was set when it shouldn\'t be'); } if (expected_record.changedHandle) { assert_true( await actual_record.changedHandle.isSameEntry( expected_record.changedHandle), 'A record\'s changedHandle didn\'t match the expected changedHandle'); } else { assert_equals( actual_record.changedHandle, null, 'A record\'s changedHandle was set when it shouldn\'t be'); } assert_true( await actual_record.root.isSameEntry(root), 'A record\'s root didn\'t match the expected root'); } } function modifiedEvent(changedHandle, relativePathComponents) { return {type: 'modified', changedHandle, relativePathComponents}; } function appearedEvent(changedHandle, relativePathComponents) { return {type: 'appeared', changedHandle, relativePathComponents}; } function disappearedEvent(relativePathComponents) { return {type: 'disappeared', changedHandle: null, relativePathComponents}; } function movedEvent( changedHandle, relativePathComponents, relativePathMovedFrom) { return { type: 'moved', changedHandle, relativePathComponents, relativePathMovedFrom }; }