feat: enhance EditorFileWatcher and IPC for improved file change handling
- Introduced a grace period in EditorFileWatcher to ignore rapid 'change' events during startup, preventing performance issues in large repositories. - Added a new IPC channel for setting watched file paths, optimizing file monitoring by focusing on currently open tabs. - Updated the editor API to include the new setWatchedFiles method, enhancing performance by avoiding unnecessary project-wide file watching. - Enhanced tests for EditorFileWatcher to validate the new startup behavior and ensure proper event handling.
This commit is contained in:
parent
e1585b4f71
commit
51df8847a9
9 changed files with 171 additions and 30 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
EDITOR_READ_FILE,
|
||||
EDITOR_RENAME_FILE,
|
||||
EDITOR_SEARCH_IN_FILES,
|
||||
EDITOR_SET_WATCHED_FILES,
|
||||
EDITOR_WATCH_DIR,
|
||||
EDITOR_WRITE_FILE,
|
||||
// eslint-disable-next-line boundaries/element-types -- IPC channel constants are shared between main and preload by design
|
||||
|
|
@ -352,6 +353,19 @@ async function handleEditorWatchDir(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update watched file list (open tabs).
|
||||
*/
|
||||
async function handleEditorSetWatchedFiles(
|
||||
_event: IpcMainInvokeEvent,
|
||||
filePaths: string[]
|
||||
): Promise<IpcResult<void>> {
|
||||
return wrapHandler('setWatchedFiles', async () => {
|
||||
if (!activeProjectRoot) throw new Error('Editor not initialized');
|
||||
editorFileWatcher.setWatchedFiles(Array.isArray(filePaths) ? filePaths : []);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Registration
|
||||
// =============================================================================
|
||||
|
|
@ -384,6 +398,7 @@ export function registerEditorHandlers(ipcMain: IpcMain): void {
|
|||
ipcMain.handle(EDITOR_READ_BINARY_PREVIEW, handleEditorReadBinaryPreview);
|
||||
ipcMain.handle(EDITOR_GIT_STATUS, handleEditorGitStatus);
|
||||
ipcMain.handle(EDITOR_WATCH_DIR, handleEditorWatchDir);
|
||||
ipcMain.handle(EDITOR_SET_WATCHED_FILES, handleEditorSetWatchedFiles);
|
||||
}
|
||||
|
||||
export function removeEditorHandlers(ipcMain: IpcMain): void {
|
||||
|
|
@ -402,6 +417,7 @@ export function removeEditorHandlers(ipcMain: IpcMain): void {
|
|||
ipcMain.removeHandler(EDITOR_READ_BINARY_PREVIEW);
|
||||
ipcMain.removeHandler(EDITOR_GIT_STATUS);
|
||||
ipcMain.removeHandler(EDITOR_WATCH_DIR);
|
||||
ipcMain.removeHandler(EDITOR_SET_WATCHED_FILES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,11 +22,8 @@ const log = createLogger('EditorFileWatcher');
|
|||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
/** Directories to ignore (regex for chokidar's `ignored` option) */
|
||||
const IGNORED_PATTERN =
|
||||
/(node_modules|\.git|dist|build|out|coverage|__pycache__|\.cache|\.next|\.turbo|\.parcel-cache|\.vite|\.venv|\.tox|vendor|target|Pods|DerivedData|\.idea|\.vscode|\.DS_Store)/;
|
||||
|
||||
const MAX_DEPTH = 20;
|
||||
const STARTUP_IGNORE_CHANGE_MS = 3000;
|
||||
const MAX_EMITTED_EVENTS_PER_FLUSH = 300;
|
||||
|
||||
// =============================================================================
|
||||
// Service
|
||||
|
|
@ -40,34 +37,69 @@ export class EditorFileWatcher {
|
|||
private onChangeCallback: ((event: EditorFileChangeEvent) => void) | null = null;
|
||||
// Higher debounce = fewer IPC events during large bursts (checkout/build/format).
|
||||
private readonly DEBOUNCE_MS = 350;
|
||||
private ignoreChangeUntilMs = 0;
|
||||
private watchedFilesKey = '';
|
||||
|
||||
/**
|
||||
* Start watching a project directory.
|
||||
* Idempotent: stops any existing watcher first.
|
||||
* Initialize watcher context for a project root.
|
||||
*
|
||||
* Performance: does NOT watch the entire project directory.
|
||||
* Use setWatchedFiles() to watch only open files (tabs).
|
||||
*/
|
||||
start(projectRoot: string, onChange: (event: EditorFileChangeEvent) => void): void {
|
||||
this.stop();
|
||||
this.projectRoot = projectRoot;
|
||||
this.ignoreChangeUntilMs = Date.now() + STARTUP_IGNORE_CHANGE_MS;
|
||||
this.watchedFilesKey = '';
|
||||
|
||||
log.info('Starting file watcher for:', projectRoot);
|
||||
log.info('Starting file watcher (open files only) for:', projectRoot);
|
||||
this.onChangeCallback = onChange;
|
||||
}
|
||||
|
||||
this.watcher = watch(projectRoot, {
|
||||
ignored: IGNORED_PATTERN,
|
||||
/**
|
||||
* Update list of watched file paths (open tabs).
|
||||
* Rebuilds chokidar watcher when the set changes.
|
||||
*/
|
||||
setWatchedFiles(filePaths: string[]): void {
|
||||
if (!this.projectRoot) {
|
||||
throw new Error('Watcher not initialized');
|
||||
}
|
||||
|
||||
const normalized = filePaths
|
||||
.filter((p): p is string => typeof p === 'string' && p.length > 0)
|
||||
.filter((p) => isPathWithinRoot(p, this.projectRoot!));
|
||||
|
||||
normalized.sort();
|
||||
const key = normalized.join('\n');
|
||||
if (key === this.watchedFilesKey) return;
|
||||
this.watchedFilesKey = key;
|
||||
|
||||
// Close existing watcher first (if any)
|
||||
if (this.watcher) {
|
||||
void this.watcher.close();
|
||||
this.watcher = null;
|
||||
}
|
||||
|
||||
if (normalized.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a new watcher for the given file set.
|
||||
// disableGlobbing prevents chokidar from treating file names as patterns.
|
||||
this.watcher = watch(normalized, {
|
||||
ignoreInitial: true,
|
||||
ignorePermissionErrors: true,
|
||||
followSymlinks: false,
|
||||
depth: MAX_DEPTH,
|
||||
});
|
||||
|
||||
this.onChangeCallback = onChange;
|
||||
|
||||
const emitSafe = (type: EditorFileChangeEvent['type'], filePath: string): void => {
|
||||
// SEC-2: validate path is within project root before sending to renderer
|
||||
if (!isPathWithinRoot(filePath, projectRoot)) {
|
||||
if (type === 'change' && Date.now() < this.ignoreChangeUntilMs) {
|
||||
return;
|
||||
}
|
||||
if (!isPathWithinRoot(filePath, this.projectRoot!)) {
|
||||
log.warn('Watcher event outside project root, ignoring:', filePath);
|
||||
return;
|
||||
}
|
||||
// Aggregate rapid events — only the last event type per path is kept
|
||||
this.pendingEvents.set(filePath, type);
|
||||
this.scheduleFlush();
|
||||
};
|
||||
|
|
@ -91,6 +123,8 @@ export class EditorFileWatcher {
|
|||
}
|
||||
this.pendingEvents.clear();
|
||||
this.onChangeCallback = null;
|
||||
this.ignoreChangeUntilMs = 0;
|
||||
this.watchedFilesKey = '';
|
||||
if (this.watcher) {
|
||||
log.info('Stopping file watcher');
|
||||
void this.watcher.close();
|
||||
|
|
@ -110,9 +144,28 @@ export class EditorFileWatcher {
|
|||
const events = new Map(this.pendingEvents);
|
||||
this.pendingEvents.clear();
|
||||
if (!this.onChangeCallback) return;
|
||||
for (const [filePath, type] of events) {
|
||||
this.onChangeCallback({ type, path: filePath });
|
||||
// Cap emitted events per flush to protect renderer from floods.
|
||||
// Prefer create/delete events over change events.
|
||||
let emitted = 0;
|
||||
|
||||
if (events.size > MAX_EMITTED_EVENTS_PER_FLUSH) {
|
||||
log.warn(
|
||||
`Watcher burst: ${events.size} events pending, capping to ${MAX_EMITTED_EVENTS_PER_FLUSH}`
|
||||
);
|
||||
}
|
||||
|
||||
const emit = (type: EditorFileChangeEvent['type']): void => {
|
||||
for (const [filePath, t] of events) {
|
||||
if (t !== type) continue;
|
||||
this.onChangeCallback?.({ type: t, path: filePath });
|
||||
emitted++;
|
||||
if (emitted >= MAX_EMITTED_EVENTS_PER_FLUSH) return;
|
||||
}
|
||||
};
|
||||
|
||||
emit('delete');
|
||||
if (emitted < MAX_EMITTED_EVENTS_PER_FLUSH) emit('create');
|
||||
if (emitted < MAX_EMITTED_EVENTS_PER_FLUSH) emit('change');
|
||||
}, this.DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -458,6 +458,9 @@ export const EDITOR_GIT_STATUS = 'editor:gitStatus';
|
|||
/** Enable/disable file watcher for current project */
|
||||
export const EDITOR_WATCH_DIR = 'editor:watchDir';
|
||||
|
||||
/** Update list of watched file paths (open tabs) */
|
||||
export const EDITOR_SET_WATCHED_FILES = 'editor:setWatchedFiles';
|
||||
|
||||
/** Read binary file as base64 for inline preview */
|
||||
export const EDITOR_READ_BINARY_PREVIEW = 'editor:readBinaryPreview';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
EDITOR_READ_FILE,
|
||||
EDITOR_RENAME_FILE,
|
||||
EDITOR_SEARCH_IN_FILES,
|
||||
EDITOR_SET_WATCHED_FILES,
|
||||
EDITOR_WATCH_DIR,
|
||||
EDITOR_WRITE_FILE,
|
||||
HTTP_SERVER_GET_STATUS,
|
||||
|
|
@ -1032,6 +1033,8 @@ const electronAPI: ElectronAPI = {
|
|||
invokeIpcWithResult<BinaryPreviewResult>(EDITOR_READ_BINARY_PREVIEW, filePath),
|
||||
gitStatus: () => invokeIpcWithResult<GitStatusResult>(EDITOR_GIT_STATUS),
|
||||
watchDir: (enable: boolean) => invokeIpcWithResult<void>(EDITOR_WATCH_DIR, enable),
|
||||
setWatchedFiles: (filePaths: string[]) =>
|
||||
invokeIpcWithResult<void>(EDITOR_SET_WATCHED_FILES, filePaths),
|
||||
onEditorChange: (callback: (event: EditorFileChangeEvent) => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: EditorFileChangeEvent): void =>
|
||||
callback(data);
|
||||
|
|
|
|||
|
|
@ -988,6 +988,9 @@ export class HttpAPIClient implements ElectronAPI {
|
|||
watchDir: async () => {
|
||||
throw new Error('Editor not available in browser mode');
|
||||
},
|
||||
setWatchedFiles: async () => {
|
||||
throw new Error('Editor not available in browser mode');
|
||||
},
|
||||
onEditorChange: () => {
|
||||
return () => {};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -65,6 +65,28 @@ let watcherEventCounts: Record<EditorFileChangeEvent['type'], number> = {
|
|||
delete: 0,
|
||||
};
|
||||
|
||||
let watchedFilesSyncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let lastWatchedFilesKey = '';
|
||||
|
||||
function scheduleSyncWatchedFiles(get: () => AppState): void {
|
||||
if (!api.editor) return;
|
||||
const state = get();
|
||||
if (!state.editorWatcherEnabled) return;
|
||||
if (!state.editorProjectPath) return;
|
||||
|
||||
const filePaths = state.editorOpenTabs.map((t) => t.filePath).filter(Boolean);
|
||||
filePaths.sort();
|
||||
const key = filePaths.join('\n');
|
||||
if (key === lastWatchedFilesKey) return;
|
||||
lastWatchedFilesKey = key;
|
||||
|
||||
if (watchedFilesSyncTimer) clearTimeout(watchedFilesSyncTimer);
|
||||
watchedFilesSyncTimer = setTimeout(() => {
|
||||
watchedFilesSyncTimer = null;
|
||||
void api.editor.setWatchedFiles(filePaths);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open request sequence for editor initialization.
|
||||
* Cancels stale async work (notably React 18 StrictMode dev effect mount/unmount).
|
||||
|
|
@ -308,7 +330,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
|
||||
scheduleIdleWork(() => {
|
||||
if (editorOpenSeq !== openSeq || get().editorProjectPath !== projectPath) return;
|
||||
if (watcherDesired) void get().toggleWatcher(true);
|
||||
// TODO: temporarily disabled file watcher — re-enable when stabilized
|
||||
if (watcherDesired) void get().toggleWatcher(false);
|
||||
// Defer initial git status a bit more — it can be expensive on large repos.
|
||||
setTimeout(() => {
|
||||
if (editorOpenSeq !== openSeq || get().editorProjectPath !== projectPath) return;
|
||||
|
|
@ -465,6 +488,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
editorOpenTabs: newTabs,
|
||||
editorActiveTabId: tab.id,
|
||||
});
|
||||
|
||||
scheduleSyncWatchedFiles(get);
|
||||
},
|
||||
|
||||
closeEditorTab: (tabId: string) => {
|
||||
|
|
@ -505,6 +530,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
editorModifiedFiles: restModified,
|
||||
editorSaveError: restErrors,
|
||||
});
|
||||
|
||||
scheduleSyncWatchedFiles(get);
|
||||
},
|
||||
|
||||
closeOtherEditorTabs: (keepTabId: string) => {
|
||||
|
|
@ -809,6 +836,9 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
};
|
||||
});
|
||||
|
||||
// Keep open-files-only watcher in sync with remapped tab paths
|
||||
scheduleSyncWatchedFiles(get);
|
||||
|
||||
// Remap bridge state for each affected tab
|
||||
const { editorOpenTabs } = get();
|
||||
for (const tab of editorOpenTabs) {
|
||||
|
|
@ -907,6 +937,9 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
};
|
||||
});
|
||||
|
||||
// Keep open-files-only watcher in sync with remapped tab paths
|
||||
scheduleSyncWatchedFiles(get);
|
||||
|
||||
// Remap bridge state
|
||||
const { editorOpenTabs } = get();
|
||||
for (const tab of editorOpenTabs) {
|
||||
|
|
@ -992,6 +1025,12 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
} catch {
|
||||
// localStorage may not be available
|
||||
}
|
||||
if (enable) {
|
||||
scheduleSyncWatchedFiles(get);
|
||||
} else {
|
||||
// Ensure main process stops watching files promptly.
|
||||
lastWatchedFilesKey = '';
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Failed to toggle watcher:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,11 @@ export interface EditorAPI {
|
|||
readBinaryPreview: (filePath: string) => Promise<BinaryPreviewResult>;
|
||||
gitStatus: () => Promise<GitStatusResult>;
|
||||
watchDir: (enable: boolean) => Promise<void>;
|
||||
/**
|
||||
* Provide the list of currently-open file paths (tabs) to watch.
|
||||
* Intended as a performance optimization: avoids watching the whole project tree.
|
||||
*/
|
||||
setWatchedFiles: (filePaths: string[]) => Promise<void>;
|
||||
/** Subscribe to file change events (main → renderer). Returns cleanup function. */
|
||||
onEditorChange: (callback: (event: EditorFileChangeEvent) => void) => () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ vi.mock('@preload/constants/ipcChannels', () => ({
|
|||
EDITOR_RENAME_FILE: 'editor:renameFile',
|
||||
EDITOR_SEARCH_IN_FILES: 'editor:searchInFiles',
|
||||
EDITOR_LIST_FILES: 'editor:listFiles',
|
||||
EDITOR_READ_BINARY_PREVIEW: 'editor:readBinaryPreview',
|
||||
EDITOR_GIT_STATUS: 'editor:gitStatus',
|
||||
EDITOR_WATCH_DIR: 'editor:watchDir',
|
||||
EDITOR_SET_WATCHED_FILES: 'editor:setWatchedFiles',
|
||||
EDITOR_CHANGE: 'editor:change',
|
||||
}));
|
||||
|
||||
|
|
@ -146,8 +148,8 @@ describe('Editor IPC handlers', () => {
|
|||
});
|
||||
|
||||
describe('registration', () => {
|
||||
it('registers all 14 editor channels', () => {
|
||||
expect(mockIpc.handle).toHaveBeenCalledTimes(14);
|
||||
it('registers all 16 editor channels', () => {
|
||||
expect(mockIpc.handle).toHaveBeenCalledTimes(16);
|
||||
expect(mockIpc._handlers.has('editor:open')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:close')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:readDir')).toBe(true);
|
||||
|
|
@ -160,13 +162,15 @@ describe('Editor IPC handlers', () => {
|
|||
expect(mockIpc._handlers.has('editor:renameFile')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:searchInFiles')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:listFiles')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:readBinaryPreview')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:gitStatus')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:watchDir')).toBe(true);
|
||||
expect(mockIpc._handlers.has('editor:setWatchedFiles')).toBe(true);
|
||||
});
|
||||
|
||||
it('removeEditorHandlers clears all channels', () => {
|
||||
removeEditorHandlers(mockIpc as unknown as IpcMain);
|
||||
expect(mockIpc.removeHandler).toHaveBeenCalledTimes(14);
|
||||
expect(mockIpc.removeHandler).toHaveBeenCalledTimes(16);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { EditorFileWatcher } from '../../../../src/main/services/editor/EditorFi
|
|||
describe('EditorFileWatcher', () => {
|
||||
let watcher: EditorFileWatcher;
|
||||
const FLUSH_DEBOUNCE_MS = 350;
|
||||
const STARTUP_IGNORE_CHANGE_MS = 3000;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
|
@ -55,22 +56,26 @@ describe('EditorFileWatcher', () => {
|
|||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('creates chokidar watcher with correct options', () => {
|
||||
it('creates chokidar watcher with correct options (open files only)', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
|
||||
expect(watch).toHaveBeenCalledWith('/Users/test/project', {
|
||||
ignored: expect.any(RegExp),
|
||||
// start() does not create a watcher until we provide watched files
|
||||
expect(watch).not.toHaveBeenCalled();
|
||||
|
||||
watcher.setWatchedFiles(['/Users/test/project/src/index.ts']);
|
||||
|
||||
expect(watch).toHaveBeenCalledWith(['/Users/test/project/src/index.ts'], {
|
||||
ignoreInitial: true,
|
||||
ignorePermissionErrors: true,
|
||||
followSymlinks: false,
|
||||
depth: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('registers change, add, unlink, and error handlers', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/src/index.ts']);
|
||||
|
||||
const registeredEvents = mockOn.mock.calls.map((c) => c[0]);
|
||||
expect(registeredEvents).toContain('change');
|
||||
|
|
@ -82,9 +87,12 @@ describe('EditorFileWatcher', () => {
|
|||
it('emits normalized events through onChange callback', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/src/index.ts']);
|
||||
|
||||
// Simulate chokidar 'change' event
|
||||
const changeHandler = mockOn.mock.calls.find((c) => c[0] === 'change')?.[1];
|
||||
// Startup grace period ignores 'change' events for first 3s
|
||||
vi.advanceTimersByTime(STARTUP_IGNORE_CHANGE_MS);
|
||||
changeHandler?.('/Users/test/project/src/index.ts');
|
||||
vi.advanceTimersByTime(FLUSH_DEBOUNCE_MS);
|
||||
|
||||
|
|
@ -97,6 +105,7 @@ describe('EditorFileWatcher', () => {
|
|||
it('emits create event for add', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/new-file.ts']);
|
||||
|
||||
const addHandler = mockOn.mock.calls.find((c) => c[0] === 'add')?.[1];
|
||||
addHandler?.('/Users/test/project/new-file.ts');
|
||||
|
|
@ -111,6 +120,7 @@ describe('EditorFileWatcher', () => {
|
|||
it('emits delete event for unlink', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/old-file.ts']);
|
||||
|
||||
const unlinkHandler = mockOn.mock.calls.find((c) => c[0] === 'unlink')?.[1];
|
||||
unlinkHandler?.('/Users/test/project/old-file.ts');
|
||||
|
|
@ -123,12 +133,12 @@ describe('EditorFileWatcher', () => {
|
|||
});
|
||||
|
||||
it('ignores events outside project root (SEC-2)', () => {
|
||||
vi.mocked(isPathWithinRoot).mockReturnValueOnce(false);
|
||||
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/src/index.ts']);
|
||||
|
||||
const changeHandler = mockOn.mock.calls.find((c) => c[0] === 'change')?.[1];
|
||||
vi.advanceTimersByTime(STARTUP_IGNORE_CHANGE_MS);
|
||||
changeHandler?.('/etc/passwd');
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
|
@ -137,10 +147,11 @@ describe('EditorFileWatcher', () => {
|
|||
it('stops previous watcher on re-start (idempotent)', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project1', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project1/a.ts']);
|
||||
watcher.start('/Users/test/project2', onChange);
|
||||
|
||||
expect(mockClose).toHaveBeenCalledTimes(1);
|
||||
expect(watch).toHaveBeenCalledTimes(2);
|
||||
expect(watch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -148,6 +159,7 @@ describe('EditorFileWatcher', () => {
|
|||
it('closes the watcher', () => {
|
||||
const onChange = vi.fn();
|
||||
watcher.start('/Users/test/project', onChange);
|
||||
watcher.setWatchedFiles(['/Users/test/project/a.ts']);
|
||||
|
||||
watcher.stop();
|
||||
|
||||
|
|
@ -166,13 +178,16 @@ describe('EditorFileWatcher', () => {
|
|||
expect(watcher.isWatching()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true after start', () => {
|
||||
it('returns true after setWatchedFiles', () => {
|
||||
watcher.start('/Users/test/project', vi.fn());
|
||||
expect(watcher.isWatching()).toBe(false);
|
||||
watcher.setWatchedFiles(['/Users/test/project/a.ts']);
|
||||
expect(watcher.isWatching()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false after stop', () => {
|
||||
watcher.start('/Users/test/project', vi.fn());
|
||||
watcher.setWatchedFiles(['/Users/test/project/a.ts']);
|
||||
watcher.stop();
|
||||
expect(watcher.isWatching()).toBe(false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue