FileSystemObserver.js (13122B)
1 'use strict'; 2 3 // This script depends on the following scripts: 4 // resources/test-helpers.js 5 // resources/collecting-file-system-observer.js 6 // resources/change-observer-scope-test.js 7 // script-tests/FileSystemObserver-writable-file-stream.js 8 9 promise_test(async t => { 10 try { 11 const observer = new FileSystemObserver(() => {}); 12 } catch { 13 assert_unreached(); 14 } 15 }, 'Creating a FileSystemObserver from a supported global succeeds'); 16 17 directory_test(async (t, root_dir) => { 18 const observer = new FileSystemObserver(() => {}); 19 try { 20 observer.unobserve(root_dir); 21 } catch { 22 assert_unreached(); 23 } 24 }, 'Calling unobserve() without a corresponding observe() shouldn\'t throw'); 25 26 directory_test(async (t, root_dir) => { 27 const observer = new FileSystemObserver(() => {}); 28 try { 29 observer.unobserve(root_dir); 30 observer.unobserve(root_dir); 31 } catch { 32 assert_unreached(); 33 } 34 }, 'unobserve() is idempotent'); 35 36 promise_test(async t => { 37 const observer = new FileSystemObserver(() => {}); 38 try { 39 observer.disconnect(); 40 } catch { 41 assert_unreached(); 42 } 43 }, 'Calling disconnect() without observing shouldn\'t throw'); 44 45 promise_test(async t => { 46 const observer = new FileSystemObserver(() => {}); 47 try { 48 observer.disconnect(); 49 observer.disconnect(); 50 } catch { 51 assert_unreached(); 52 } 53 }, 'disconnect() is idempotent'); 54 55 directory_test(async (t, root_dir) => { 56 const observer = new FileSystemObserver(() => {}); 57 58 // Create a `FileSystemFileHandle` and delete its underlying file entry. 59 const file = await root_dir.getFileHandle(getUniqueName(), {create: true}); 60 await file.remove(); 61 62 await promise_rejects_dom(t, 'NotFoundError', observer.observe(file)); 63 }, 'observe() fails when file does not exist'); 64 65 directory_test(async (t, root_dir) => { 66 const observer = new FileSystemObserver(() => {}); 67 68 // Create a `FileSystemDirectoryHandle` and delete its underlying file entry. 69 const dir = 70 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 71 await dir.remove(); 72 73 await promise_rejects_dom(t, 'NotFoundError', observer.observe(dir)); 74 }, 'observe() fails when directory does not exist'); 75 76 directory_test(async (t, root_dir) => { 77 const dir = 78 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 79 80 const scope_test = new ScopeTest(t, dir); 81 const watched_handle = await scope_test.watched_handle(); 82 83 for (const recursive of [false, true]) { 84 for await (const path of scope_test.in_scope_paths(recursive)) { 85 const observer = new CollectingFileSystemObserver(t, root_dir); 86 await observer.observe([watched_handle], {recursive}); 87 88 // Create `file`. 89 const file = await path.createHandle(); 90 91 // Expect one "appeared" event to happen on `file`. 92 const records = await observer.getRecords(); 93 await assert_records_equal( 94 watched_handle, records, 95 [appearedEvent(file, path.relativePathComponents())]); 96 97 observer.disconnect(); 98 } 99 } 100 }, 'Creating a file through FileSystemDirectoryHandle.getFileHandle is reported as an "appeared" event if in scope'); 101 102 directory_test(async (t, root_dir) => { 103 const dir = 104 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 105 106 const scope_test = new ScopeTest(t, dir); 107 const watched_handle = await scope_test.watched_handle(); 108 109 for (const recursive of [false, true]) { 110 for await (const path of scope_test.in_scope_paths(recursive)) { 111 const file = await path.createHandle(); 112 113 const observer = new CollectingFileSystemObserver(t, root_dir); 114 await observer.observe([watched_handle], {recursive}); 115 116 // Remove `file`. 117 await file.remove(); 118 119 // Expect one "disappeared" event to happen on `file`. 120 const records = await observer.getRecords(); 121 await assert_records_equal( 122 watched_handle, records, 123 [disappearedEvent(path.relativePathComponents())]); 124 125 observer.disconnect(); 126 } 127 } 128 }, 'Removing a file through FileSystemFileHandle.remove is reported as an "disappeared" event if in scope'); 129 130 directory_test(async (t, root_dir) => { 131 const dir = 132 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 133 134 const scope_test = new ScopeTest(t, dir); 135 const watched_handle = await scope_test.watched_handle(); 136 137 for (const recursive of [false, true]) { 138 for await (const path of scope_test.out_of_scope_paths(recursive)) { 139 const observer = new CollectingFileSystemObserver(t, root_dir); 140 await observer.observe([watched_handle], {recursive}); 141 142 // Create and remove `file`. 143 const file = await path.createHandle(); 144 await file.remove(); 145 146 // Expect the observer to receive no events. 147 const records = await observer.getRecords(); 148 await assert_records_equal(watched_handle, records, []); 149 150 observer.disconnect(); 151 } 152 } 153 }, 'Events outside the watch scope are not sent to the observer\'s callback'); 154 155 directory_test(async (t, root_dir) => { 156 const dir = 157 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 158 159 const scope_test = new ScopeTest(t, dir); 160 const watched_handle = await scope_test.watched_handle(); 161 162 for (const recursive of [false, true]) { 163 for await (const src of scope_test.in_scope_paths(recursive)) { 164 for await (const dest of scope_test.in_scope_paths(recursive)) { 165 const file = await src.createHandle(); 166 167 const observer = new CollectingFileSystemObserver(t, root_dir); 168 await observer.observe([watched_handle], {recursive}); 169 170 // Move `file`. 171 await file.move(dest.parentHandle(), dest.fileName()); 172 173 // Expect one "moved" event to happen on `file`. 174 const records = await observer.getRecords(); 175 await assert_records_equal( 176 watched_handle, records, [movedEvent( 177 file, dest.relativePathComponents(), 178 src.relativePathComponents())]); 179 180 observer.disconnect(); 181 } 182 } 183 } 184 }, 'Moving a file through FileSystemFileHandle.move is reported as a "moved" event if destination and source are in scope'); 185 186 directory_test(async (t, root_dir) => { 187 const dir = 188 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 189 190 const scope_test = new ScopeTest(t, dir); 191 const watched_handle = await scope_test.watched_handle(); 192 193 for (const recursive of [false, true]) { 194 for await (const src of scope_test.out_of_scope_paths(recursive)) { 195 for await (const dest of scope_test.out_of_scope_paths(recursive)) { 196 const file = await src.createHandle(); 197 198 const observer = new CollectingFileSystemObserver(t, root_dir); 199 await observer.observe([watched_handle], {recursive}); 200 201 // Move `file`. 202 await file.move(dest.parentHandle(), dest.fileName()); 203 204 // Expect the observer to not receive any events. 205 const records = await observer.getRecords(); 206 await assert_records_equal(watched_handle, records, []); 207 } 208 } 209 } 210 }, 'Moving a file through FileSystemFileHandle.move is not reported if destination and source are not in scope'); 211 212 directory_test(async (t, root_dir) => { 213 const dir = 214 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 215 216 const scope_test = new ScopeTest(t, dir); 217 const watched_handle = await scope_test.watched_handle(); 218 219 for (const recursive of [false, true]) { 220 for await (const src of scope_test.out_of_scope_paths(recursive)) { 221 for await (const dest of scope_test.in_scope_paths(recursive)) { 222 const file = await src.createHandle(); 223 224 const observer = new CollectingFileSystemObserver(t, root_dir); 225 await observer.observe([watched_handle], {recursive}); 226 227 // Move `file`. 228 await file.move(dest.parentHandle(), dest.fileName()); 229 230 // Expect one "appeared" event to happen on `file`. 231 const records = await observer.getRecords(); 232 await assert_records_equal( 233 watched_handle, records, 234 [appearedEvent(file, dest.relativePathComponents())]); 235 } 236 } 237 } 238 }, 'Moving a file through FileSystemFileHandle.move is reported as a "appeared" event if only destination is in scope'); 239 240 directory_test(async (t, root_dir) => { 241 const dir = 242 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 243 244 const scope_test = new ScopeTest(t, dir); 245 const watched_handle = await scope_test.watched_handle(); 246 247 for (const recursive of [false, true]) { 248 for await (const src of scope_test.in_scope_paths(recursive)) { 249 for await (const dest of scope_test.out_of_scope_paths(recursive)) { 250 // These both point to the same underlying file entry initially until 251 // move is called on `fileToMove`. `file` is kept so that we have a 252 // handle that still points at the source file entry. 253 const file = await src.createHandle(); 254 const fileToMove = await src.createHandle(); 255 256 const observer = new CollectingFileSystemObserver(t, root_dir); 257 await observer.observe([watched_handle], {recursive}); 258 259 // Move `fileToMove`. 260 await fileToMove.move(dest.parentHandle(), dest.fileName()); 261 262 // Expect one "disappeared" event to happen on `file`. 263 const records = await observer.getRecords(); 264 await assert_records_equal( 265 watched_handle, records, 266 [disappearedEvent(src.relativePathComponents())]); 267 } 268 } 269 } 270 }, 'Moving a file through FileSystemFileHandle.move is reported as a "disappeared" event if only source is in scope'); 271 272 // Wraps a `CollectingFileSystemObserver` and disconnects the observer after it's 273 // received `num_of_records_to_observe`. 274 class DisconnectingFileSystemObserver { 275 #collectingObserver; 276 277 #num_of_records_to_observe; 278 279 #called_disconnect = false; 280 #records_observed_count = 0; 281 282 constructor(test, root_dir, num_of_records_to_observe) { 283 this.#collectingObserver = new CollectingFileSystemObserver( 284 test, root_dir, this.#callback.bind(this)); 285 this.#num_of_records_to_observe = num_of_records_to_observe; 286 } 287 288 #callback(records, observer) { 289 this.#records_observed_count += records.length; 290 291 const called_disconnect = this.#called_disconnect; 292 293 // Call `disconnect` once after we've received `num_of_records_to_observe`. 294 if (!called_disconnect && 295 this.#records_observed_count >= this.#num_of_records_to_observe) { 296 observer.disconnect(); 297 this.#called_disconnect = true; 298 } 299 300 return {called_disconnect}; 301 } 302 303 getRecordsWithCallbackInfo() { 304 return this.#collectingObserver.getRecordsWithCallbackInfo(); 305 } 306 307 observe(handles) { 308 return this.#collectingObserver.observe(handles); 309 } 310 } 311 312 313 directory_test(async (t, root_dir) => { 314 const total_files_to_create = 100; 315 316 const child_dir = 317 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 318 319 // Create a `FileSystemObserver` that will disconnect after its 320 // received half of the total files we're going to create. 321 const observer = new DisconnectingFileSystemObserver( 322 t, root_dir, total_files_to_create / 2); 323 324 // Observe the child directory and create files in it. 325 await observer.observe([child_dir]); 326 for (let i = 0; i < total_files_to_create; i++) { 327 child_dir.getFileHandle(`file${i}`, {create: true}); 328 } 329 330 // Wait for `disconnect` to be called. 331 const records_with_disconnect_state = 332 await observer.getRecordsWithCallbackInfo(); 333 334 // No observations should have been received after disconnected has been 335 // called. 336 assert_false( 337 records_with_disconnect_state.some( 338 ({called_disconnect}) => called_disconnect), 339 'Received records after disconnect.'); 340 }, 'Observations stop after disconnect()'); 341 342 directory_test(async (t, root_dir) => { 343 const num_of_child_dirs = 5; 344 const num_files_to_create_per_directory = 100; 345 const total_files_to_create = 346 num_files_to_create_per_directory * num_of_child_dirs; 347 348 const child_dirs = await createDirectoryHandles( 349 root_dir, getUniqueName(), getUniqueName(), getUniqueName()); 350 351 // Create a `FileSystemObserver` that will disconnect after its received half 352 // of the total files we're going to create. 353 const observer = new DisconnectingFileSystemObserver( 354 t, root_dir, total_files_to_create / 2); 355 356 // Observe the child directories and create files in them. 357 await observer.observe(child_dirs); 358 for (let i = 0; i < num_files_to_create_per_directory; i++) { 359 child_dirs.forEach( 360 child_dir => child_dir.getFileHandle(`file${i}`, {create: true})); 361 } 362 363 // Wait for `disconnect` to be called. 364 const records_with_disconnect_state = 365 await observer.getRecordsWithCallbackInfo(); 366 367 // No observations should have been received after disconnected has been 368 // called. 369 assert_false( 370 records_with_disconnect_state.some( 371 ({called_disconnect}) => called_disconnect), 372 'Received records after disconnect.'); 373 }, 'Observations stop for all observed handles after disconnect()');