1// Flags: --expose-internals 2import * as common from '../common/index.mjs'; 3import * as fixtures from '../common/fixtures.mjs'; 4import tmpdir from '../common/tmpdir.js'; 5import path from 'node:path'; 6import assert from 'node:assert'; 7import process from 'node:process'; 8import { describe, it, beforeEach, afterEach } from 'node:test'; 9import { writeFileSync, mkdirSync } from 'node:fs'; 10import { setTimeout } from 'node:timers/promises'; 11import { once } from 'node:events'; 12import { spawn } from 'node:child_process'; 13import watcher from 'internal/watch_mode/files_watcher'; 14 15if (common.isIBMi) 16 common.skip('IBMi does not support `fs.watch()`'); 17 18const supportsRecursiveWatching = common.isOSX || common.isWindows; 19 20const { FilesWatcher } = watcher; 21tmpdir.refresh(); 22 23describe('watch mode file watcher', () => { 24 let watcher; 25 let changesCount; 26 27 beforeEach(() => { 28 changesCount = 0; 29 watcher = new FilesWatcher({ throttle: 100 }); 30 watcher.on('changed', () => changesCount++); 31 }); 32 33 afterEach(() => watcher.clear()); 34 35 let counter = 0; 36 function writeAndWaitForChanges(watcher, file) { 37 return new Promise((resolve) => { 38 const interval = setInterval(() => writeFileSync(file, `write ${counter++}`), 100); 39 watcher.once('changed', () => { 40 clearInterval(interval); 41 resolve(); 42 }); 43 }); 44 } 45 46 it('should watch changed files', async () => { 47 const file = path.join(tmpdir.path, 'file1'); 48 writeFileSync(file, 'written'); 49 watcher.filterFile(file); 50 await writeAndWaitForChanges(watcher, file); 51 assert.strictEqual(changesCount, 1); 52 }); 53 54 it('should throttle changes', async () => { 55 const file = path.join(tmpdir.path, 'file2'); 56 writeFileSync(file, 'written'); 57 watcher.filterFile(file); 58 await writeAndWaitForChanges(watcher, file); 59 60 writeFileSync(file, '1'); 61 writeFileSync(file, '2'); 62 writeFileSync(file, '3'); 63 writeFileSync(file, '4'); 64 await setTimeout(200); // throttle * 2 65 writeFileSync(file, '5'); 66 const changed = once(watcher, 'changed'); 67 writeFileSync(file, 'after'); 68 await changed; 69 // Unfortunately testing that changesCount === 2 is flaky 70 assert.ok(changesCount < 5); 71 }); 72 73 it('should ignore files in watched directory if they are not filtered', 74 { skip: !supportsRecursiveWatching }, async () => { 75 watcher.on('changed', common.mustNotCall()); 76 watcher.watchPath(tmpdir.path); 77 writeFileSync(path.join(tmpdir.path, 'file3'), '1'); 78 // Wait for this long to make sure changes are not triggered 79 await setTimeout(1000); 80 }); 81 82 it('should allow clearing filters', async () => { 83 const file = path.join(tmpdir.path, 'file4'); 84 writeFileSync(file, 'written'); 85 watcher.filterFile(file); 86 await writeAndWaitForChanges(watcher, file); 87 88 writeFileSync(file, '1'); 89 90 await setTimeout(200); // avoid throttling 91 watcher.clearFileFilters(); 92 writeFileSync(file, '2'); 93 // Wait for this long to make sure changes are triggered only once 94 await setTimeout(1000); 95 assert.strictEqual(changesCount, 1); 96 }); 97 98 it('should watch all files in watched path when in "all" mode', 99 { skip: !supportsRecursiveWatching }, async () => { 100 watcher = new FilesWatcher({ throttle: 100, mode: 'all' }); 101 watcher.on('changed', () => changesCount++); 102 103 const file = path.join(tmpdir.path, 'file5'); 104 watcher.watchPath(tmpdir.path); 105 106 const changed = once(watcher, 'changed'); 107 await setTimeout(common.platformTimeout(100)); // avoid throttling 108 writeFileSync(file, 'changed'); 109 await changed; 110 assert.strictEqual(changesCount, 1); 111 }); 112 113 it('should ruse existing watcher if it exists', 114 { skip: !supportsRecursiveWatching }, () => { 115 assert.deepStrictEqual(watcher.watchedPaths, []); 116 watcher.watchPath(tmpdir.path); 117 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 118 watcher.watchPath(tmpdir.path); 119 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 120 }); 121 122 it('should ruse existing watcher of a parent directory', 123 { skip: !supportsRecursiveWatching }, () => { 124 assert.deepStrictEqual(watcher.watchedPaths, []); 125 watcher.watchPath(tmpdir.path); 126 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 127 watcher.watchPath(path.join(tmpdir.path, 'subdirectory')); 128 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 129 }); 130 131 it('should remove existing watcher if adding a parent directory watcher', 132 { skip: !supportsRecursiveWatching }, () => { 133 assert.deepStrictEqual(watcher.watchedPaths, []); 134 const subdirectory = path.join(tmpdir.path, 'subdirectory'); 135 mkdirSync(subdirectory); 136 watcher.watchPath(subdirectory); 137 assert.deepStrictEqual(watcher.watchedPaths, [subdirectory]); 138 watcher.watchPath(tmpdir.path); 139 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 140 }); 141 142 it('should clear all watchers when calling clear', 143 { skip: !supportsRecursiveWatching }, () => { 144 assert.deepStrictEqual(watcher.watchedPaths, []); 145 watcher.watchPath(tmpdir.path); 146 assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); 147 watcher.clear(); 148 assert.deepStrictEqual(watcher.watchedPaths, []); 149 }); 150 151 it('should watch files from subprocess IPC events', async () => { 152 const file = fixtures.path('watch-mode/ipc.js'); 153 const child = spawn(process.execPath, [file], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], encoding: 'utf8' }); 154 watcher.watchChildProcessModules(child); 155 await once(child, 'exit'); 156 let expected = [file, path.join(tmpdir.path, 'file')]; 157 if (supportsRecursiveWatching) { 158 expected = expected.map((file) => path.dirname(file)); 159 } 160 assert.deepStrictEqual(watcher.watchedPaths, expected); 161 }); 162}); 163