import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as smartwatch from '../ts/index.js'; import * as smartfile from '@push.rocks/smartfile'; import * as smartrx from '@push.rocks/smartrx'; import * as fs from 'fs'; import * as path from 'path'; // Skip in CI if (process.env.CI) { process.exit(0); } const TEST_DIR = './test/assets'; // Helper to delay const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // Helper to wait for an event with timeout async function waitForEvent( observable: smartrx.rxjs.Observable, timeoutMs: number = 5000 ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { subscription.unsubscribe(); reject(new Error(`Timeout waiting for event after ${timeoutMs}ms`)); }, timeoutMs); const subscription = observable.subscribe((value) => { clearTimeout(timeout); subscription.unsubscribe(); resolve(value); }); }); } // Helper to wait for a specific file's event (filters by filename) async function waitForFileEvent( observable: smartrx.rxjs.Observable, expectedFile: string, timeoutMs: number = 5000 ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { subscription.unsubscribe(); reject(new Error(`Timeout waiting for event on ${expectedFile} after ${timeoutMs}ms`)); }, timeoutMs); const subscription = observable.subscribe((value) => { const [filePath] = value; if (filePath.includes(expectedFile)) { clearTimeout(timeout); subscription.unsubscribe(); resolve(value); } // Otherwise keep waiting for the right file }); }); } let testSmartwatch: smartwatch.Smartwatch; // =========================================== // BASIC TESTS // =========================================== tap.test('should create a new instance', async () => { testSmartwatch = new smartwatch.Smartwatch([]); expect(testSmartwatch).toBeInstanceOf(smartwatch.Smartwatch); }); tap.test('should add paths and start watching', async () => { testSmartwatch.add([`${TEST_DIR}/**/*.txt`]); await testSmartwatch.start(); expect(testSmartwatch.status).toEqual('watching'); }); tap.test('should detect ADD event for new files', async () => { const addObservable = await testSmartwatch.getObservableFor('add'); const eventPromise = waitForEvent(addObservable); // Create a new file const testFile = path.join(TEST_DIR, 'add-test.txt'); await smartfile.memory.toFs('test content', testFile); const [filePath] = await eventPromise; expect(filePath).toInclude('add-test.txt'); // Cleanup - wait for atomic delay to complete (100ms debounce + 100ms atomic) await fs.promises.unlink(testFile); await delay(250); }); tap.test('should detect CHANGE event for modified files', async () => { // First create the file const testFile = path.join(TEST_DIR, 'change-test.txt'); await smartfile.memory.toFs('initial content', testFile); // Wait for add event to complete await delay(200); const changeObservable = await testSmartwatch.getObservableFor('change'); const eventPromise = waitForEvent(changeObservable); // Modify the file await smartfile.memory.toFs('modified content', testFile); const [filePath] = await eventPromise; expect(filePath).toInclude('change-test.txt'); // Cleanup - wait for atomic delay to complete await fs.promises.unlink(testFile); await delay(250); }); tap.test('should detect UNLINK event for deleted files', async () => { // First create the file const testFile = path.join(TEST_DIR, 'unlink-test.txt'); await smartfile.memory.toFs('to be deleted', testFile); // Wait for add event to complete await delay(200); const unlinkObservable = await testSmartwatch.getObservableFor('unlink'); // Use file-specific wait to handle any pending unlinks from other tests const eventPromise = waitForFileEvent(unlinkObservable, 'unlink-test.txt'); // Delete the file await fs.promises.unlink(testFile); const [filePath] = await eventPromise; expect(filePath).toInclude('unlink-test.txt'); }); tap.test('should stop the watch process', async () => { await testSmartwatch.stop(); expect(testSmartwatch.status).toEqual('idle'); }); tap.test('cleanup: remove any remaining test files', async () => { const files = await fs.promises.readdir(TEST_DIR); for (const file of files) { if (file.startsWith('add-') || file.startsWith('change-') || file.startsWith('unlink-')) { try { await fs.promises.unlink(path.join(TEST_DIR, file)); } catch { // Ignore } } } }); export default tap.start();