| // Flags: --expose-internals |
| import * as common from '../common/index.mjs'; |
| import * as fixtures from '../common/fixtures.mjs'; |
| import tmpdir from '../common/tmpdir.js'; |
| import path from 'node:path'; |
| import assert from 'node:assert'; |
| import process from 'node:process'; |
| import { describe, it, beforeEach, afterEach } from 'node:test'; |
| import { writeFileSync, mkdirSync } from 'node:fs'; |
| import { setTimeout } from 'node:timers/promises'; |
| import { once } from 'node:events'; |
| import { spawn } from 'node:child_process'; |
| import watcher from 'internal/watch_mode/files_watcher'; |
| |
| if (common.isIBMi) |
| common.skip('IBMi does not support `fs.watch()`'); |
| |
| const supportsRecursiveWatching = common.isOSX || common.isWindows; |
| |
| const { FilesWatcher } = watcher; |
| tmpdir.refresh(); |
| |
| describe('watch mode file watcher', () => { |
| let watcher; |
| let changesCount; |
| |
| beforeEach(() => { |
| changesCount = 0; |
| watcher = new FilesWatcher({ debounce: 100 }); |
| watcher.on('changed', () => changesCount++); |
| }); |
| |
| afterEach(() => watcher.clear()); |
| |
| let counter = 0; |
| function writeAndWaitForChanges(watcher, file) { |
| return new Promise((resolve) => { |
| const interval = setInterval(() => writeFileSync(file, `write ${counter++}`), 100); |
| watcher.once('changed', () => { |
| clearInterval(interval); |
| resolve(); |
| }); |
| }); |
| } |
| |
| it('should watch changed files', async () => { |
| const file = tmpdir.resolve('file1'); |
| writeFileSync(file, 'written'); |
| watcher.filterFile(file); |
| await writeAndWaitForChanges(watcher, file); |
| assert.strictEqual(changesCount, 1); |
| }); |
| |
| it('should debounce changes', async () => { |
| const file = tmpdir.resolve('file2'); |
| writeFileSync(file, 'written'); |
| watcher.filterFile(file); |
| await writeAndWaitForChanges(watcher, file); |
| |
| writeFileSync(file, '1'); |
| writeFileSync(file, '2'); |
| writeFileSync(file, '3'); |
| writeFileSync(file, '4'); |
| await setTimeout(200); // debounce * 2 |
| writeFileSync(file, '5'); |
| const changed = once(watcher, 'changed'); |
| writeFileSync(file, 'after'); |
| await changed; |
| // Unfortunately testing that changesCount === 2 is flaky |
| assert.ok(changesCount < 5); |
| }); |
| |
| it('should debounce changes on multiple files', async () => { |
| const files = []; |
| for (let i = 0; i < 10; i++) { |
| const file = tmpdir.resolve(`file-debounced-${i}`); |
| writeFileSync(file, 'written'); |
| watcher.filterFile(file); |
| files.push(file); |
| } |
| |
| files.forEach((file) => writeFileSync(file, '1')); |
| files.forEach((file) => writeFileSync(file, '2')); |
| files.forEach((file) => writeFileSync(file, '3')); |
| files.forEach((file) => writeFileSync(file, '4')); |
| |
| await setTimeout(200); // debounce * 2 |
| files.forEach((file) => writeFileSync(file, '5')); |
| const changed = once(watcher, 'changed'); |
| files.forEach((file) => writeFileSync(file, 'after')); |
| await changed; |
| // Unfortunately testing that changesCount === 2 is flaky |
| assert.ok(changesCount < 5); |
| }); |
| |
| it('should ignore files in watched directory if they are not filtered', |
| { skip: !supportsRecursiveWatching }, async () => { |
| watcher.on('changed', common.mustNotCall()); |
| watcher.watchPath(tmpdir.path); |
| writeFileSync(tmpdir.resolve('file3'), '1'); |
| // Wait for this long to make sure changes are not triggered |
| await setTimeout(1000); |
| }); |
| |
| it('should allow clearing filters', async () => { |
| const file = tmpdir.resolve('file4'); |
| writeFileSync(file, 'written'); |
| watcher.filterFile(file); |
| await writeAndWaitForChanges(watcher, file); |
| |
| writeFileSync(file, '1'); |
| assert.strictEqual(changesCount, 1); |
| |
| watcher.clearFileFilters(); |
| writeFileSync(file, '2'); |
| // Wait for this long to make sure changes are triggered only once |
| await setTimeout(1000); |
| assert.strictEqual(changesCount, 1); |
| }); |
| |
| it('should watch all files in watched path when in "all" mode', |
| { skip: !supportsRecursiveWatching }, async () => { |
| watcher = new FilesWatcher({ debounce: 100, mode: 'all' }); |
| watcher.on('changed', () => changesCount++); |
| |
| const file = tmpdir.resolve('file5'); |
| watcher.watchPath(tmpdir.path); |
| |
| const changed = once(watcher, 'changed'); |
| await setTimeout(common.platformTimeout(100)); // avoid throttling |
| writeFileSync(file, 'changed'); |
| await changed; |
| assert.strictEqual(changesCount, 1); |
| }); |
| |
| it('should ruse existing watcher if it exists', |
| { skip: !supportsRecursiveWatching }, () => { |
| assert.deepStrictEqual(watcher.watchedPaths, []); |
| watcher.watchPath(tmpdir.path); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| watcher.watchPath(tmpdir.path); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| }); |
| |
| it('should ruse existing watcher of a parent directory', |
| { skip: !supportsRecursiveWatching }, () => { |
| assert.deepStrictEqual(watcher.watchedPaths, []); |
| watcher.watchPath(tmpdir.path); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| watcher.watchPath(tmpdir.resolve('subdirectory')); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| }); |
| |
| it('should remove existing watcher if adding a parent directory watcher', |
| { skip: !supportsRecursiveWatching }, () => { |
| assert.deepStrictEqual(watcher.watchedPaths, []); |
| const subdirectory = tmpdir.resolve('subdirectory'); |
| mkdirSync(subdirectory); |
| watcher.watchPath(subdirectory); |
| assert.deepStrictEqual(watcher.watchedPaths, [subdirectory]); |
| watcher.watchPath(tmpdir.path); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| }); |
| |
| it('should clear all watchers when calling clear', |
| { skip: !supportsRecursiveWatching }, () => { |
| assert.deepStrictEqual(watcher.watchedPaths, []); |
| watcher.watchPath(tmpdir.path); |
| assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]); |
| watcher.clear(); |
| assert.deepStrictEqual(watcher.watchedPaths, []); |
| }); |
| |
| it('should watch files from subprocess IPC events', async () => { |
| const file = fixtures.path('watch-mode/ipc.js'); |
| const child = spawn(process.execPath, [file], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], encoding: 'utf8' }); |
| watcher.watchChildProcessModules(child); |
| await once(child, 'exit'); |
| let expected = [file, tmpdir.resolve('file')]; |
| if (supportsRecursiveWatching) { |
| expected = expected.map((file) => path.dirname(file)); |
| } |
| assert.deepStrictEqual(watcher.watchedPaths, expected); |
| }); |
| }); |