feat(watch mode): Add watch mode support with CLI options and enhanced documentation

This commit is contained in:
2025-05-26 04:37:38 +00:00
parent 7aaeed0dc6
commit 82757c4abc
12 changed files with 336 additions and 9 deletions

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tstest',
version: '2.1.0',
version: '2.2.0',
description: 'a test utility to run tests that match test/**/*.ts'
}

View File

@ -16,6 +16,8 @@ export const runCli = async () => {
let startFromFile: number | null = null;
let stopAtFile: number | null = null;
let timeoutSeconds: number | null = null;
let watchMode: boolean = false;
let watchIgnorePatterns: string[] = [];
// Parse options
for (let i = 0; i < args.length; i++) {
@ -84,6 +86,18 @@ export const runCli = async () => {
process.exit(1);
}
break;
case '--watch':
case '-w':
watchMode = true;
break;
case '--watch-ignore':
if (i + 1 < args.length) {
watchIgnorePatterns = args[++i].split(',');
} else {
console.error('Error: --watch-ignore requires a comma-separated list of patterns');
process.exit(1);
}
break;
default:
if (!arg.startsWith('-')) {
testPath = arg;
@ -110,6 +124,8 @@ export const runCli = async () => {
console.error(' --startFrom <n> Start running from test file number n');
console.error(' --stopAt <n> Stop running at test file number n');
console.error(' --timeout <s> Timeout test files after s seconds');
console.error(' --watch, -w Watch for file changes and re-run tests');
console.error(' --watch-ignore Patterns to ignore in watch mode (comma-separated)');
process.exit(1);
}
@ -125,7 +141,12 @@ export const runCli = async () => {
}
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile, timeoutSeconds);
await tsTestInstance.run();
if (watchMode) {
await tsTestInstance.runWatch(watchIgnorePatterns);
} else {
await tsTestInstance.run();
}
};
// Execute CLI when this file is run directly

View File

@ -101,6 +101,77 @@ export class TsTest {
tapCombinator.evaluate();
}
public async runWatch(ignorePatterns: string[] = []) {
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
console.clear();
this.logger.watchModeStart();
// Initial run
await this.run();
// Set up file watcher
const fileChanges = new Map<string, NodeJS.Timeout>();
const debounceTime = 300; // 300ms debounce
const runTestsAfterChange = async () => {
console.clear();
const changedFiles = Array.from(fileChanges.keys());
fileChanges.clear();
this.logger.watchModeRerun(changedFiles);
await this.run();
this.logger.watchModeWaiting();
};
// Start watching before subscribing to events
await smartchokInstance.start();
// Subscribe to file change events
const changeObservable = await smartchokInstance.getObservableFor('change');
const addObservable = await smartchokInstance.getObservableFor('add');
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
const handleFileChange = (changedPath: string) => {
// Skip if path matches ignore patterns
if (ignorePatterns.some(pattern => changedPath.includes(pattern))) {
return;
}
// Clear existing timeout for this file if any
if (fileChanges.has(changedPath)) {
clearTimeout(fileChanges.get(changedPath));
}
// Set new timeout for this file
const timeout = setTimeout(() => {
fileChanges.delete(changedPath);
if (fileChanges.size === 0) {
runTestsAfterChange();
}
}, debounceTime);
fileChanges.set(changedPath, timeout);
};
// Subscribe to all relevant events
changeObservable.subscribe(([path]) => handleFileChange(path));
addObservable.subscribe(([path]) => handleFileChange(path));
unlinkObservable.subscribe(([path]) => handleFileChange(path));
this.logger.watchModeWaiting();
// Handle Ctrl+C to exit gracefully
process.on('SIGINT', async () => {
this.logger.watchModeStop();
await smartchokInstance.stop();
process.exit(0);
});
// Keep the process running
await new Promise(() => {}); // This promise never resolves
}
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
// Check if this file should be skipped based on range
if (this.startFromFile !== null && fileIndex < this.startFromFile) {

View File

@ -520,4 +520,47 @@ export class TsTestLogger {
return diff;
}
// Watch mode methods
watchModeStart() {
if (this.options.json) {
this.logJson({ event: 'watchModeStart' });
return;
}
this.log(this.format('\n👀 Watch Mode', 'cyan'));
this.log(this.format(' Running tests in watch mode...', 'dim'));
this.log(this.format(' Press Ctrl+C to exit\n', 'dim'));
}
watchModeWaiting() {
if (this.options.json) {
this.logJson({ event: 'watchModeWaiting' });
return;
}
this.log(this.format('\n Waiting for file changes...', 'dim'));
}
watchModeRerun(changedFiles: string[]) {
if (this.options.json) {
this.logJson({ event: 'watchModeRerun', changedFiles });
return;
}
this.log(this.format('\n🔄 File changes detected:', 'cyan'));
changedFiles.forEach(file => {
this.log(this.format(`${file}`, 'yellow'));
});
this.log(this.format('\n Re-running tests...\n', 'dim'));
}
watchModeStop() {
if (this.options.json) {
this.logJson({ event: 'watchModeStop' });
return;
}
this.log(this.format('\n\n👋 Stopping watch mode...', 'cyan'));
}
}

View File

@ -13,6 +13,7 @@ export {
// @push.rocks scope
import * as consolecolor from '@push.rocks/consolecolor';
import * as smartbrowser from '@push.rocks/smartbrowser';
import * as smartchok from '@push.rocks/smartchok';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile';
import * as smartlog from '@push.rocks/smartlog';
@ -23,6 +24,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
export {
consolecolor,
smartbrowser,
smartchok,
smartdelay,
smartfile,
smartlog,