feat(cli): Enhance test discovery with support for single file and glob pattern execution using improved CLI argument detection
This commit is contained in:
		| @@ -1,5 +1,14 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-05-15 - 1.1.0 - feat(cli) | ||||||
|  | Enhance test discovery with support for single file and glob pattern execution using improved CLI argument detection | ||||||
|  |  | ||||||
|  | - Detect execution mode (file, glob, directory) based on CLI input in ts/index.ts | ||||||
|  | - Refactor TestDirectory to load test files using SmartFile for single file and glob patterns | ||||||
|  | - Update TsTest to pass execution mode and adjust test discovery accordingly | ||||||
|  | - Bump dependency versions for typedserver, tsbundle, tapbundle, and others | ||||||
|  | - Add .claude/settings.local.json for updated permissions configuration | ||||||
|  |  | ||||||
| ## 2025-01-23 - 1.0.96 - fix(TsTest) | ## 2025-01-23 - 1.0.96 - fix(TsTest) | ||||||
| Fixed improper type-check for promise-like testModule defaults | Fixed improper type-check for promise-like testModule defaults | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
									
									
									
									
								
							| @@ -20,24 +20,24 @@ | |||||||
|     "buildDocs": "tsdoc" |     "buildDocs": "tsdoc" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@git.zone/tsbuild": "^2.2.0", |     "@git.zone/tsbuild": "^2.5.1", | ||||||
|     "@types/node": "^22.10.9" |     "@types/node": "^22.15.18" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@api.global/typedserver": "^3.0.53", |     "@api.global/typedserver": "^3.0.74", | ||||||
|     "@git.zone/tsbundle": "^2.1.0", |     "@git.zone/tsbundle": "^2.2.5", | ||||||
|     "@git.zone/tsrun": "^1.3.3", |     "@git.zone/tsrun": "^1.3.3", | ||||||
|     "@push.rocks/consolecolor": "^2.0.2", |     "@push.rocks/consolecolor": "^2.0.2", | ||||||
|     "@push.rocks/smartbrowser": "^2.0.8", |     "@push.rocks/smartbrowser": "^2.0.8", | ||||||
|     "@push.rocks/smartdelay": "^3.0.5", |     "@push.rocks/smartdelay": "^3.0.5", | ||||||
|     "@push.rocks/smartfile": "^11.1.5", |     "@push.rocks/smartfile": "^11.2.0", | ||||||
|     "@push.rocks/smartlog": "^3.0.7", |     "@push.rocks/smartlog": "^3.0.9", | ||||||
|     "@push.rocks/smartpromise": "^4.2.0", |     "@push.rocks/smartpromise": "^4.2.3", | ||||||
|     "@push.rocks/smartshell": "^3.2.2", |     "@push.rocks/smartshell": "^3.2.3", | ||||||
|     "@push.rocks/tapbundle": "^5.5.6", |     "@push.rocks/tapbundle": "^6.0.3", | ||||||
|     "@types/ws": "^8.5.14", |     "@types/ws": "^8.18.1", | ||||||
|     "figures": "^6.1.0", |     "figures": "^6.1.0", | ||||||
|     "ws": "^8.18.0" |     "ws": "^8.18.2" | ||||||
|   }, |   }, | ||||||
|   "files": [ |   "files": [ | ||||||
|     "ts/**/*", |     "ts/**/*", | ||||||
| @@ -53,5 +53,6 @@ | |||||||
|   ], |   ], | ||||||
|   "browserslist": [ |   "browserslist": [ | ||||||
|     "last 1 chrome versions" |     "last 1 chrome versions" | ||||||
|   ] |   ], | ||||||
|  |   "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										2698
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2698
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										51
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | # Plan for adding single file and glob pattern execution support to tstest | ||||||
|  |  | ||||||
|  | !! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !! | ||||||
|  |  | ||||||
|  | ## Goal - ✅ COMPLETED | ||||||
|  | - ✅ Make `tstest test/test.abc.ts` run the specified file directly | ||||||
|  | - ✅ Support glob patterns like `tstest test/*.spec.ts` or `tstest test/**/*.test.ts` | ||||||
|  | - ✅ Maintain backward compatibility with directory argument | ||||||
|  |  | ||||||
|  | ## Current behavior - UPDATED | ||||||
|  | - ✅ tstest now supports three modes: directory, single file, and glob patterns | ||||||
|  | - ✅ Directory mode now searches recursively using `**/test*.ts` pattern | ||||||
|  | - ✅ Single file mode runs a specific test file | ||||||
|  | - ✅ Glob mode runs files matching the pattern | ||||||
|  |  | ||||||
|  | ## Completed changes | ||||||
|  |  | ||||||
|  | ### 1. ✅ Update cli argument handling in index.ts | ||||||
|  | - ✅ Detect argument type: file path, glob pattern, or directory | ||||||
|  | - ✅ Check if argument contains glob characters (*, **, ?, [], etc.) | ||||||
|  | - ✅ Pass appropriate mode to TsTest constructor | ||||||
|  | - ✅ Added TestExecutionMode enum | ||||||
|  |  | ||||||
|  | ### 2. ✅ Modify TsTest constructor and class | ||||||
|  | - ✅ Add support for three modes: directory, file, glob | ||||||
|  | - ✅ Update constructor to accept pattern/path and mode | ||||||
|  | - ✅ Added executionMode property to track the mode | ||||||
|  |  | ||||||
|  | ### 3. ✅ Update TestDirectory class | ||||||
|  | - ✅ Used `listFileTree` for glob pattern support | ||||||
|  | - ✅ Used `SmartFile.fromFilePath` for single file loading | ||||||
|  | - ✅ Refactored to support all three modes in `_init` method | ||||||
|  | - ✅ Return appropriate file array based on mode | ||||||
|  | - ✅ Changed default directory behavior to recursive search | ||||||
|  |   - ✅ When directory argument: use `**/test*.ts` pattern for recursive search | ||||||
|  |   - ✅ This ensures subdirectories are included in test discovery | ||||||
|  |  | ||||||
|  | ### 4. ✅ Test the implementation | ||||||
|  | - ✅ Created test file `test/test.single.ts` for single file functionality | ||||||
|  | - ✅ Created test file `test/test.glob.ts` for glob pattern functionality | ||||||
|  | - ✅ Created test in subdirectory `test/subdir/test.sub.ts` for recursive search | ||||||
|  | - ✅ Tested with existing test files for backward compatibility | ||||||
|  | - ✅ Tested glob patterns: `test/test.*.ts` works correctly | ||||||
|  | - ✅ Verified that default behavior now includes subdirectories | ||||||
|  |  | ||||||
|  | ## Implementation completed | ||||||
|  | 1. ✅ CLI argument type detection implemented | ||||||
|  | 2. ✅ TsTest class supports all three modes | ||||||
|  | 3. ✅ TestDirectory handles files, globs, and directories | ||||||
|  | 4. ✅ Default pattern changed from `test*.ts` to `**/test*.ts` for recursive search | ||||||
|  | 5. ✅ Comprehensive tests added and all modes verified | ||||||
							
								
								
									
										8
									
								
								test/subdir/test.sub.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/subdir/test.sub.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
|  |  | ||||||
|  | tap.test('subdirectory test execution', async () => { | ||||||
|  |   console.log('This test verifies subdirectory test discovery works'); | ||||||
|  |   expect(true).toBeTrue(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										8
									
								
								test/test.glob.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/test.glob.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
|  |  | ||||||
|  | tap.test('glob pattern test execution', async () => { | ||||||
|  |   console.log('This test verifies glob pattern execution works'); | ||||||
|  |   expect(true).toBeTrue(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										8
									
								
								test/test.single.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/test.single.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
|  |  | ||||||
|  | tap.test('single file test execution', async () => { | ||||||
|  |   console.log('This test verifies single file execution works'); | ||||||
|  |   expect(true).toBeTrue(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@git.zone/tstest', |   name: '@git.zone/tstest', | ||||||
|   version: '1.0.96', |   version: '1.1.0', | ||||||
|   description: 'a test utility to run tests that match test/**/*.ts' |   description: 'a test utility to run tests that match test/**/*.ts' | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								ts/index.ts
									
									
									
									
									
								
							| @@ -1,10 +1,29 @@ | |||||||
| import { TsTest } from './tstest.classes.tstest.js'; | import { TsTest } from './tstest.classes.tstest.js'; | ||||||
|  |  | ||||||
|  | export enum TestExecutionMode { | ||||||
|  |   DIRECTORY = 'directory', | ||||||
|  |   FILE = 'file', | ||||||
|  |   GLOB = 'glob' | ||||||
|  | } | ||||||
|  |  | ||||||
| export const runCli = async () => { | export const runCli = async () => { | ||||||
|   if (!process.argv[2]) { |   if (!process.argv[2]) { | ||||||
|     console.error('You must specify a test directory as argument. Please try again.'); |     console.error('You must specify a test directory/file/pattern as argument. Please try again.'); | ||||||
|     process.exit(1); |     process.exit(1); | ||||||
|   } |   } | ||||||
|   const tsTestInstance = new TsTest(process.cwd(), process.argv[2]); |    | ||||||
|  |   const testPath = process.argv[2]; | ||||||
|  |   let executionMode: TestExecutionMode; | ||||||
|  |    | ||||||
|  |   // Detect execution mode based on the argument | ||||||
|  |   if (testPath.includes('*') || testPath.includes('?') || testPath.includes('[') || testPath.includes('{')) { | ||||||
|  |     executionMode = TestExecutionMode.GLOB; | ||||||
|  |   } else if (testPath.endsWith('.ts')) { | ||||||
|  |     executionMode = TestExecutionMode.FILE; | ||||||
|  |   } else { | ||||||
|  |     executionMode = TestExecutionMode.DIRECTORY; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode); | ||||||
|   await tsTestInstance.run(); |   await tsTestInstance.run(); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as plugins from './tstest.plugins.js'; | import * as plugins from './tstest.plugins.js'; | ||||||
| import * as paths from './tstest.paths.js'; | import * as paths from './tstest.paths.js'; | ||||||
| import { SmartFile } from '@push.rocks/smartfile'; | import { SmartFile } from '@push.rocks/smartfile'; | ||||||
|  | import { TestExecutionMode } from './index.js'; | ||||||
|  |  | ||||||
| // tap related stuff | // tap related stuff | ||||||
| import { TapCombinator } from './tstest.classes.tap.combinator.js'; | import { TapCombinator } from './tstest.classes.tap.combinator.js'; | ||||||
| @@ -14,14 +15,14 @@ export class TestDirectory { | |||||||
|   cwd: string; |   cwd: string; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * the relative location of the test dir |    * the test path or pattern | ||||||
|    */ |    */ | ||||||
|   relativePath: string; |   testPath: string; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * the absolute path of the test dir |    * the execution mode | ||||||
|    */ |    */ | ||||||
|   absolutePath: string; |   executionMode: TestExecutionMode; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * an array of Smartfiles |    * an array of Smartfiles | ||||||
| @@ -30,27 +31,71 @@ export class TestDirectory { | |||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * the constructor for TestDirectory |    * the constructor for TestDirectory | ||||||
|    * tell it the path |    * @param cwdArg - the current working directory | ||||||
|    * @param pathToTestDirectory |    * @param testPathArg - the test path/pattern | ||||||
|  |    * @param executionModeArg - the execution mode | ||||||
|    */ |    */ | ||||||
|   constructor(cwdArg: string, relativePathToTestDirectory: string) { |   constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) { | ||||||
|     this.cwd = cwdArg; |     this.cwd = cwdArg; | ||||||
|     this.relativePath = relativePathToTestDirectory; |     this.testPath = testPathArg; | ||||||
|  |     this.executionMode = executionModeArg; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _init() { |   private async _init() { | ||||||
|     this.testfileArray = await plugins.smartfile.fs.fileTreeToObject( |     switch (this.executionMode) { | ||||||
|       plugins.path.join(this.cwd, this.relativePath), |       case TestExecutionMode.FILE: | ||||||
|       'test*.ts' |         // Single file mode | ||||||
|  |         const filePath = plugins.path.isAbsolute(this.testPath)  | ||||||
|  |           ? this.testPath  | ||||||
|  |           : plugins.path.join(this.cwd, this.testPath); | ||||||
|  |          | ||||||
|  |         if (await plugins.smartfile.fs.fileExists(filePath)) { | ||||||
|  |           this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)]; | ||||||
|  |         } else { | ||||||
|  |           throw new Error(`Test file not found: ${filePath}`); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |          | ||||||
|  |       case TestExecutionMode.GLOB: | ||||||
|  |         // Glob pattern mode - use listFileTree which supports glob patterns | ||||||
|  |         const globPattern = this.testPath; | ||||||
|  |         const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern); | ||||||
|  |          | ||||||
|  |         this.testfileArray = await Promise.all( | ||||||
|  |           matchedFiles.map(async (filePath) => { | ||||||
|  |             const absolutePath = plugins.path.isAbsolute(filePath)  | ||||||
|  |               ? filePath  | ||||||
|  |               : plugins.path.join(this.cwd, filePath); | ||||||
|  |             return await plugins.smartfile.SmartFile.fromFilePath(absolutePath); | ||||||
|  |           }) | ||||||
|         ); |         ); | ||||||
|  |         break; | ||||||
|  |          | ||||||
|  |       case TestExecutionMode.DIRECTORY: | ||||||
|  |         // Directory mode - now recursive with ** pattern | ||||||
|  |         const dirPath = plugins.path.join(this.cwd, this.testPath); | ||||||
|  |         const testPattern = '**/test*.ts'; | ||||||
|  |          | ||||||
|  |         const testFiles = await plugins.smartfile.fs.listFileTree(dirPath, testPattern); | ||||||
|  |          | ||||||
|  |         this.testfileArray = await Promise.all( | ||||||
|  |           testFiles.map(async (filePath) => { | ||||||
|  |             const absolutePath = plugins.path.isAbsolute(filePath) | ||||||
|  |               ? filePath | ||||||
|  |               : plugins.path.join(dirPath, filePath); | ||||||
|  |             return await plugins.smartfile.SmartFile.fromFilePath(absolutePath); | ||||||
|  |           }) | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getTestFilePathArray() { |   async getTestFilePathArray() { | ||||||
|     await this._init(); |     await this._init(); | ||||||
|     const testFilePaths: string[] = []; |     const testFilePaths: string[] = []; | ||||||
|     for (const testFile of this.testfileArray) { |     for (const testFile of this.testfileArray) { | ||||||
|       const filePath = plugins.path.join(this.relativePath, testFile.path); |       // Use the path directly from the SmartFile | ||||||
|       testFilePaths.push(filePath); |       testFilePaths.push(testFile.path); | ||||||
|     } |     } | ||||||
|     return testFilePaths; |     return testFilePaths; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -7,9 +7,11 @@ import { coloredString as cs } from '@push.rocks/consolecolor'; | |||||||
| import { TestDirectory } from './tstest.classes.testdirectory.js'; | import { TestDirectory } from './tstest.classes.testdirectory.js'; | ||||||
| import { TapCombinator } from './tstest.classes.tap.combinator.js'; | import { TapCombinator } from './tstest.classes.tap.combinator.js'; | ||||||
| import { TapParser } from './tstest.classes.tap.parser.js'; | import { TapParser } from './tstest.classes.tap.parser.js'; | ||||||
|  | import { TestExecutionMode } from './index.js'; | ||||||
|  |  | ||||||
| export class TsTest { | export class TsTest { | ||||||
|   public testDir: TestDirectory; |   public testDir: TestDirectory; | ||||||
|  |   public executionMode: TestExecutionMode; | ||||||
|  |  | ||||||
|   public smartshellInstance = new plugins.smartshell.Smartshell({ |   public smartshellInstance = new plugins.smartshell.Smartshell({ | ||||||
|     executor: 'bash', |     executor: 'bash', | ||||||
| @@ -20,8 +22,9 @@ export class TsTest { | |||||||
|  |  | ||||||
|   public tsbundleInstance = new plugins.tsbundle.TsBundle(); |   public tsbundleInstance = new plugins.tsbundle.TsBundle(); | ||||||
|  |  | ||||||
|   constructor(cwdArg: string, relativePathToTestDirectory: string) { |   constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) { | ||||||
|     this.testDir = new TestDirectory(cwdArg, relativePathToTestDirectory); |     this.executionMode = executionModeArg; | ||||||
|  |     this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async run() { |   async run() { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user