feat(cli): Improve test runner configuration: update test scripts, reorganize test directories, update dependencies and add local settings for command permissions.
This commit is contained in:
		| @@ -1,5 +1,14 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-15 - 1.5.0 - feat(cli) | ||||
| Improve test runner configuration: update test scripts, reorganize test directories, update dependencies and add local settings for command permissions. | ||||
|  | ||||
| - Updated package.json scripts to use pnpm and separate commands for tapbundle and tstest. | ||||
| - Reorganized tests into dedicated directories (test/tapbundle and test/tstest) and removed deprecated test files. | ||||
| - Refactored import paths and bumped dependency versions in tapbundle, tstest, and associated node utilities. | ||||
| - Added .claude/settings.local.json to configure local permissions for bash and web fetch commands. | ||||
| - Introduced ts/tspublish.json to define publish order. | ||||
|  | ||||
| ## 2025-05-15 - 1.4.0 - feat(logging) | ||||
| Display failed test console logs in default mode | ||||
|  | ||||
|   | ||||
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @@ -12,11 +12,12 @@ | ||||
|     "tstest": "./cli.js" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "test": "(npm run cleanUp && npm run prepareTest && npm run tstest)", | ||||
|     "prepareTest": "git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/sandbox-npmts && cd .nogit/sandbox-npmts && npm install", | ||||
|     "tstest": "cd .nogit/sandbox-npmts && node ../../cli.ts.js test/ --web", | ||||
|     "cleanUp": "rm -rf .nogit/sandbox-npmts", | ||||
|     "build": "(tsbuild --web --allowimplicitany  --skiplibcheck)", | ||||
|     "test": "pnpm run build && pnpm run test:tapbundle && pnpm run test:tstest", | ||||
|     "test:tapbundle": "tsx ./cli.child.ts test/tapbundle/**/*.ts", | ||||
|     "test:tapbundle:verbose": "tsx ./cli.child.ts test/tapbundle/**/*.ts --verbose", | ||||
|     "test:tstest": "tsx ./cli.child.ts test/tstest/**/*.ts", | ||||
|     "test:tstest:verbose": "tsx ./cli.child.ts test/tstest/**/*.ts --verbose", | ||||
|     "build": "(tsbuild tsfolders)", | ||||
|     "buildDocs": "tsdoc" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
| @@ -28,13 +29,22 @@ | ||||
|     "@git.zone/tsbundle": "^2.2.5", | ||||
|     "@git.zone/tsrun": "^1.3.3", | ||||
|     "@push.rocks/consolecolor": "^2.0.2", | ||||
|     "@push.rocks/qenv": "^6.1.0", | ||||
|     "@push.rocks/smartbrowser": "^2.0.8", | ||||
|     "@push.rocks/smartcrypto": "^2.0.4", | ||||
|     "@push.rocks/smartdelay": "^3.0.5", | ||||
|     "@push.rocks/smartenv": "^5.0.12", | ||||
|     "@push.rocks/smartexpect": "^2.4.2", | ||||
|     "@push.rocks/smartfile": "^11.2.0", | ||||
|     "@push.rocks/smartlog": "^3.0.9", | ||||
|     "@push.rocks/smartjson": "^5.0.20", | ||||
|     "@push.rocks/smartlog": "^3.1.1", | ||||
|     "@push.rocks/smartmongo": "^2.0.12", | ||||
|     "@push.rocks/smartpath": "^5.0.18", | ||||
|     "@push.rocks/smartpromise": "^4.2.3", | ||||
|     "@push.rocks/smartrequest": "^2.1.0", | ||||
|     "@push.rocks/smarts3": "^2.2.5", | ||||
|     "@push.rocks/smartshell": "^3.2.3", | ||||
|     "@push.rocks/tapbundle": "^6.0.3", | ||||
|     "@push.rocks/smarttime": "^4.1.1", | ||||
|     "@types/ws": "^8.18.1", | ||||
|     "figures": "^6.1.0", | ||||
|     "ws": "^8.18.2" | ||||
|   | ||||
							
								
								
									
										1302
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1302
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,62 @@ | ||||
| # Architecture Overview | ||||
|  | ||||
| ## Project Structure | ||||
|  | ||||
| This project integrates tstest with tapbundle through a modular architecture: | ||||
|  | ||||
| 1. **tstest** (`/ts/`) - The test runner that discovers and executes test files | ||||
| 2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests | ||||
| 3. **tapbundle_node** (`/ts_tapbundle_node/`) - Node.js-specific testing utilities | ||||
|  | ||||
| ## How Components Work Together | ||||
|  | ||||
| ### Test Execution Flow | ||||
|  | ||||
| 1. **CLI Entry Point** (`cli.js` <20> `cli.ts.js` <20> `cli.child.ts`) | ||||
|    - The CLI uses tsx to run TypeScript files directly | ||||
|    - Accepts glob patterns to find test files | ||||
|    - Supports options like `--verbose`, `--quiet`, `--web` | ||||
|  | ||||
| 2. **Test Discovery** | ||||
|    - tstest scans for test files matching the provided pattern | ||||
|    - Defaults to `test/**/*.ts` when no pattern is specified | ||||
|    - Supports both file and directory modes | ||||
|  | ||||
| 3. **Test Runner** | ||||
|    - Each test file imports `tap` and `expect` from tapbundle | ||||
|    - Tests are written using `tap.test()` with async functions | ||||
|    - Browser tests are compiled with esbuild and run in Chromium via Puppeteer | ||||
|  | ||||
| ### Key Integration Points | ||||
|  | ||||
| 1. **Import Structure** | ||||
|    - Test files import from local tapbundle: `import { tap, expect } from '../../ts_tapbundle/index.js'` | ||||
|    - Node-specific tests also import from tapbundle_node: `import { tapNodeTools } from '../../ts_tapbundle_node/index.js'` | ||||
|  | ||||
| 2. **WebHelpers** | ||||
|    - Browser tests can use webhelpers for DOM manipulation | ||||
|    - `webhelpers.html` - Template literal for creating HTML strings | ||||
|    - `webhelpers.fixture` - Creates DOM elements from HTML strings | ||||
|    - Automatically detects browser environment and only enables in browser context | ||||
|  | ||||
| 3. **Build System** | ||||
|    - Uses `tsbuild tsfolders` to compile TypeScript | ||||
|    - Maintains separate output directories: `/dist_ts/`, `/dist_ts_tapbundle/`, `/dist_ts_tapbundle_node/` | ||||
|    - Compilation order is resolved automatically based on dependencies | ||||
|  | ||||
| ### Test Scripts | ||||
|  | ||||
| The package.json defines several test scripts: | ||||
| - `test` - Builds and runs all tests (tapbundle and tstest) | ||||
| - `test:tapbundle` - Runs tapbundle framework tests | ||||
| - `test:tstest` - Runs tstest's own tests | ||||
| - Both support `:verbose` variants for detailed output | ||||
|  | ||||
| ### Environment Detection | ||||
|  | ||||
| The framework automatically detects the runtime environment: | ||||
| - Node.js tests run directly via tsx | ||||
| - Browser tests are compiled and served via a local server | ||||
| - WebHelpers are only enabled in browser environment | ||||
|  | ||||
| This architecture allows for seamless testing across both Node.js and browser environments while maintaining a clean separation of concerns. | ||||
							
								
								
									
										48
									
								
								test/tapbundle/test.browser.nonci.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								test/tapbundle/test.browser.nonci.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import { tap, expect, webhelpers } from '../../ts_tapbundle/index.js'; | ||||
|  | ||||
| tap.preTask('custompretask', async () => { | ||||
|   console.log('this is a pretask'); | ||||
| }); | ||||
|  | ||||
| tap.test('should have access to webhelpers', async () => { | ||||
|   const myElement = await webhelpers.fixture(webhelpers.html`<div></div>`); | ||||
|   expect(myElement).toBeInstanceOf(HTMLElement); | ||||
|   console.log(myElement); | ||||
| }); | ||||
|  | ||||
| const test1 = tap.test('my first test -> expect true to be true', async () => { | ||||
|   return expect(true).toBeTrue(); | ||||
| }); | ||||
|  | ||||
| const test2 = tap.test('my second test', async (tools) => { | ||||
|   await tools.delayFor(50); | ||||
| }); | ||||
|  | ||||
| const test3 = tap.test( | ||||
|   'my third test -> test2 should take longer than test1 and endure at least 1000ms', | ||||
|   async () => { | ||||
|     expect( | ||||
|       (await test1.testPromise).hrtMeasurement.milliSeconds < | ||||
|         (await test2).hrtMeasurement.milliSeconds, | ||||
|     ).toBeTrue(); | ||||
|     expect((await test2.testPromise).hrtMeasurement.milliSeconds > 10).toBeTrue(); | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| const test4 = tap.skip.test('my 4th test -> should fail', async (tools) => { | ||||
|   tools.allowFailure(); | ||||
|   expect(false).toBeTrue(); | ||||
| }); | ||||
|  | ||||
| const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => { | ||||
|   tools.timeout(1000); | ||||
|   await tools.delayFor(500); | ||||
| }); | ||||
|  | ||||
| const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => { | ||||
|   tools.allowFailure(); | ||||
|   tools.timeout(1000); | ||||
|   await tools.delayFor(100); | ||||
| }); | ||||
|  | ||||
| await tap.start(); | ||||
							
								
								
									
										28
									
								
								test/tapbundle/test.node.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/tapbundle/test.node.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { tap, expect } from '../../ts_tapbundle/index.js'; | ||||
|  | ||||
| import { tapNodeTools } from '../../ts_tapbundle_node/index.js'; | ||||
|  | ||||
| tap.test('should execure a command', async () => { | ||||
|   const result = await tapNodeTools.runCommand('ls -la'); | ||||
|   expect(result.exitCode).toEqual(0); | ||||
| }); | ||||
|  | ||||
| tap.test('should create a https cert', async () => { | ||||
|   const { key, cert } = await tapNodeTools.createHttpsCert('localhost'); | ||||
|   console.log(key); | ||||
|   console.log(cert); | ||||
|   expect(key).toInclude('-----BEGIN RSA PRIVATE KEY-----'); | ||||
|   expect(cert).toInclude('-----BEGIN CERTIFICATE-----'); | ||||
| }); | ||||
|  | ||||
| tap.test('should create a smartmongo instance', async () => { | ||||
|   const smartmongo = await tapNodeTools.createSmartmongo(); | ||||
|   await smartmongo.stop(); | ||||
| }); | ||||
|  | ||||
| tap.test('should create a smarts3 instance', async () => { | ||||
|   const smarts3 = await tapNodeTools.createSmarts3(); | ||||
|   await smarts3.stop(); | ||||
| }); | ||||
|  | ||||
| tap.start(); | ||||
							
								
								
									
										5
									
								
								test/tapbundle/test.tapwrap.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/tapbundle/test.tapwrap.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import { tap, expect, TapWrap } from '../../ts_tapbundle/index.js'; | ||||
|  | ||||
| tap.test('should run a test', async () => {}); | ||||
|  | ||||
| tap.start(); | ||||
							
								
								
									
										49
									
								
								test/tapbundle/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								test/tapbundle/test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { tap, expect } from '../../ts_tapbundle/index.js'; | ||||
|  | ||||
| tap.preTask('hi there', async () => { | ||||
|   console.log('this is a pretask'); | ||||
| }); | ||||
|  | ||||
| const test1 = tap.test('my first test -> expect true to be true', async () => { | ||||
|   return expect(true).toBeTrue(); | ||||
| }); | ||||
|  | ||||
| const test2 = tap.test('my second test', async (tools) => { | ||||
|   await tools.delayFor(1000); | ||||
| }); | ||||
|  | ||||
| const test3 = tap.test( | ||||
|   'my third test -> test2 should take longer than test1 and endure at least 1000ms', | ||||
|   async () => { | ||||
|     expect( | ||||
|       (await test1.testPromise).hrtMeasurement.milliSeconds < | ||||
|         (await test2).hrtMeasurement.milliSeconds, | ||||
|     ).toBeTrue(); | ||||
|     expect((await test2.testPromise).hrtMeasurement.milliSeconds > 1000).toBeTrue(); | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| const test4 = tap.test('my 4th test -> should fail', async (tools) => { | ||||
|   tools.allowFailure(); | ||||
|   expect(false).toBeFalse(); | ||||
|   return 'hello'; | ||||
| }); | ||||
|  | ||||
| const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => { | ||||
|   const test4Result = await test4.testResultPromise; | ||||
|   tools.timeout(1000); | ||||
|   await tools.delayFor(500); | ||||
| }); | ||||
|  | ||||
| const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => { | ||||
|   tools.allowFailure(); | ||||
|   tools.timeout(1000); | ||||
|   await tools.delayFor(2000); | ||||
| }); | ||||
|  | ||||
| const test7 = tap.test('my 7th test -> should print a colored string', async (tools) => { | ||||
|   const cs = await tools.coloredString('hello', 'red', 'cyan'); | ||||
|   console.log(cs); | ||||
| }); | ||||
|  | ||||
| tap.start(); | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import * as tstest from '../ts/index.js'; | ||||
|  | ||||
| tap.test('prepare test', async () => {}); | ||||
|  | ||||
| tap.start(); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('subdirectory test execution', async () => { | ||||
|   console.log('This test verifies subdirectory test discovery works'); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('Test with console output', async () => { | ||||
|   console.log('Log message 1 from test'); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('This test should fail', async () => { | ||||
|   console.log('This test will fail on purpose'); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('Test that will fail with console logs', async () => { | ||||
|   console.log('Starting the test...'); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('glob pattern test execution', async () => { | ||||
|   console.log('This test verifies glob pattern execution works'); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| 
 | ||||
| tap.test('single file test execution', async () => { | ||||
|   console.log('This test verifies single file execution works'); | ||||
							
								
								
									
										6
									
								
								test/tstest/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								test/tstest/test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| import { expect, tap } from '../../ts_tapbundle/index.js'; | ||||
| import * as tstest from '../../ts/index.js'; | ||||
|  | ||||
| tap.test('prepare test', async () => {}); | ||||
|  | ||||
| tap.start(); | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@git.zone/tstest', | ||||
|   version: '1.4.0', | ||||
|   version: '1.5.0', | ||||
|   description: 'a test utility to run tests that match test/**/*.ts' | ||||
| } | ||||
|   | ||||
							
								
								
									
										3
									
								
								ts/tspublish.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts/tspublish.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "order": 2 | ||||
| } | ||||
| @@ -18,7 +18,7 @@ import * as smartfile from '@push.rocks/smartfile'; | ||||
| import * as smartlog from '@push.rocks/smartlog'; | ||||
| import * as smartpromise from '@push.rocks/smartpromise'; | ||||
| import * as smartshell from '@push.rocks/smartshell'; | ||||
| import * as tapbundle from '@push.rocks/tapbundle'; | ||||
| import * as tapbundle from '../dist_ts_tapbundle/index.js'; | ||||
|  | ||||
| export { | ||||
|   consolecolor, | ||||
|   | ||||
							
								
								
									
										8
									
								
								ts_tapbundle/00_commitinfo_data.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts_tapbundle/00_commitinfo_data.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /** | ||||
|  * autocreated commitinfo by @push.rocks/commitinfo | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/tapbundle', | ||||
|   version: '6.0.3', | ||||
|   description: 'A comprehensive testing automation library that provides a wide range of utilities and tools for TAP (Test Anything Protocol) based testing, especially suitable for projects using tapbuffer.' | ||||
| } | ||||
							
								
								
									
										7
									
								
								ts_tapbundle/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ts_tapbundle/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export { tap } from './tapbundle.classes.tap.js'; | ||||
| export { TapWrap } from './tapbundle.classes.tapwrap.js'; | ||||
| export { webhelpers } from './webhelpers.js'; | ||||
|  | ||||
| import { expect } from '@push.rocks/smartexpect'; | ||||
|  | ||||
| export { expect }; | ||||
							
								
								
									
										21
									
								
								ts_tapbundle/tapbundle.classes.pretask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ts_tapbundle/tapbundle.classes.pretask.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
| import { TapTools } from './tapbundle.classes.taptools.js'; | ||||
|  | ||||
| export interface IPreTaskFunction { | ||||
|   (tapTools?: TapTools): Promise<any>; | ||||
| } | ||||
|  | ||||
| export class PreTask { | ||||
|   public description: string; | ||||
|   public preTaskFunction: IPreTaskFunction; | ||||
|  | ||||
|   constructor(descriptionArg: string, preTaskFunctionArg: IPreTaskFunction) { | ||||
|     this.description = descriptionArg; | ||||
|     this.preTaskFunction = preTaskFunctionArg; | ||||
|   } | ||||
|  | ||||
|   public async run() { | ||||
|     console.log(`::__PRETASK: ${this.description}`); | ||||
|     await this.preTaskFunction(new TapTools(null)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										173
									
								
								ts_tapbundle/tapbundle.classes.tap.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								ts_tapbundle/tapbundle.classes.tap.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
|  | ||||
| import { type IPreTaskFunction, PreTask } from './tapbundle.classes.pretask.js'; | ||||
| import { TapTest, type ITestFunction } from './tapbundle.classes.taptest.js'; | ||||
| export class Tap<T> { | ||||
|   /** | ||||
|    * skips a test | ||||
|    * tests marked with tap.skip.test() are never executed | ||||
|    */ | ||||
|   public skip = { | ||||
|     test: (descriptionArg: string, functionArg: ITestFunction<T>) => { | ||||
|       console.log(`skipped test: ${descriptionArg}`); | ||||
|     }, | ||||
|     testParallel: (descriptionArg: string, functionArg: ITestFunction<T>) => { | ||||
|       console.log(`skipped test: ${descriptionArg}`); | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * only executes tests marked as ONLY | ||||
|    */ | ||||
|   public only = { | ||||
|     test: (descriptionArg: string, testFunctionArg: ITestFunction<T>) => { | ||||
|       this.test(descriptionArg, testFunctionArg, 'only'); | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   private _tapPreTasks: PreTask[] = []; | ||||
|   private _tapTests: TapTest<any>[] = []; | ||||
|   private _tapTestsOnly: TapTest<any>[] = []; | ||||
|  | ||||
|   /** | ||||
|    * Normal test function, will run one by one | ||||
|    * @param testDescription - A description of what the test does | ||||
|    * @param testFunction - A Function that returns a Promise and resolves or rejects | ||||
|    */ | ||||
|   public test( | ||||
|     testDescription: string, | ||||
|     testFunction: ITestFunction<T>, | ||||
|     modeArg: 'normal' | 'only' | 'skip' = 'normal', | ||||
|   ): TapTest<T> { | ||||
|     const localTest = new TapTest<T>({ | ||||
|       description: testDescription, | ||||
|       testFunction, | ||||
|       parallel: false, | ||||
|     }); | ||||
|     if (modeArg === 'normal') { | ||||
|       this._tapTests.push(localTest); | ||||
|     } else if (modeArg === 'only') { | ||||
|       this._tapTestsOnly.push(localTest); | ||||
|     } | ||||
|     return localTest; | ||||
|   } | ||||
|  | ||||
|   public preTask(descriptionArg: string, functionArg: IPreTaskFunction) { | ||||
|     this._tapPreTasks.push(new PreTask(descriptionArg, functionArg)); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A parallel test that will not be waited for before the next starts. | ||||
|    * @param testDescription - A description of what the test does | ||||
|    * @param testFunction - A Function that returns a Promise and resolves or rejects | ||||
|    */ | ||||
|   public testParallel(testDescription: string, testFunction: ITestFunction<T>) { | ||||
|     this._tapTests.push( | ||||
|       new TapTest({ | ||||
|         description: testDescription, | ||||
|         testFunction, | ||||
|         parallel: true, | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * starts the test evaluation | ||||
|    */ | ||||
|   public async start(optionsArg?: { throwOnError: boolean }) { | ||||
|     // lets set the tapbundle promise | ||||
|     const smartenvInstance = new plugins.smartenv.Smartenv(); | ||||
|     smartenvInstance.isBrowser | ||||
|       ? ((globalThis as any).tapbundleDeferred = plugins.smartpromise.defer()) | ||||
|       : null; | ||||
|  | ||||
|     // lets continue with running the tests | ||||
|     const promiseArray: Array<Promise<any>> = []; | ||||
|  | ||||
|     // safeguard against empty test array | ||||
|     if (this._tapTests.length === 0) { | ||||
|       console.log('no tests specified. Ending here!'); | ||||
|       // TODO: throw proper error | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // determine which tests to run | ||||
|     let concerningTests: TapTest[]; | ||||
|     if (this._tapTestsOnly.length > 0) { | ||||
|       concerningTests = this._tapTestsOnly; | ||||
|     } else { | ||||
|       concerningTests = this._tapTests; | ||||
|     } | ||||
|  | ||||
|     // lets run the pretasks | ||||
|     for (const preTask of this._tapPreTasks) { | ||||
|       await preTask.run(); | ||||
|     } | ||||
|  | ||||
|     console.log(`1..${concerningTests.length}`); | ||||
|     for (let testKey = 0; testKey < concerningTests.length; testKey++) { | ||||
|       const currentTest = concerningTests[testKey]; | ||||
|       const testPromise = currentTest.run(testKey); | ||||
|       if (currentTest.parallel) { | ||||
|         promiseArray.push(testPromise); | ||||
|       } else { | ||||
|         await testPromise; | ||||
|       } | ||||
|     } | ||||
|     await Promise.all(promiseArray); | ||||
|  | ||||
|     // when tests have been run and all promises are fullfilled | ||||
|     const failReasons: string[] = []; | ||||
|     const executionNotes: string[] = []; | ||||
|     // collect failed tests | ||||
|     for (const tapTest of concerningTests) { | ||||
|       if (tapTest.status !== 'success') { | ||||
|         failReasons.push( | ||||
|           `Test ${tapTest.testKey + 1} failed with status ${tapTest.status}:\n` + | ||||
|             `|| ${tapTest.description}\n` + | ||||
|             `|| for more information please take a look the logs above`, | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // render fail Reasons | ||||
|     for (const failReason of failReasons) { | ||||
|       console.log(failReason); | ||||
|     } | ||||
|  | ||||
|     if (optionsArg && optionsArg.throwOnError && failReasons.length > 0) { | ||||
|       if (!smartenvInstance.isBrowser) process.exit(1); | ||||
|     } | ||||
|     if (smartenvInstance.isBrowser) { | ||||
|       (globalThis as any).tapbundleDeferred.resolve(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async stopForcefully(codeArg = 0, directArg = false) { | ||||
|     console.log(`tap stopping forcefully! Code: ${codeArg} / Direct: ${directArg}`); | ||||
|     if (directArg) { | ||||
|       process.exit(codeArg); | ||||
|     } else { | ||||
|       setTimeout(() => { | ||||
|         process.exit(codeArg); | ||||
|       }, 10); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * handle errors | ||||
|    */ | ||||
|   public threw(err: Error) { | ||||
|     console.log(err); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Explicitly fail the current test with a custom message | ||||
|    * @param message - The failure message to display | ||||
|    */ | ||||
|   public fail(message: string = 'Test failed'): never { | ||||
|     throw new Error(message); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export let tap = new Tap(); | ||||
							
								
								
									
										87
									
								
								ts_tapbundle/tapbundle.classes.taptest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								ts_tapbundle/tapbundle.classes.taptest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
| import { tapCreator } from './tapbundle.tapcreator.js'; | ||||
| import { TapTools } from './tapbundle.classes.taptools.js'; | ||||
|  | ||||
| // imported interfaces | ||||
| import { Deferred } from '@push.rocks/smartpromise'; | ||||
| import { HrtMeasurement } from '@push.rocks/smarttime'; | ||||
|  | ||||
| // interfaces | ||||
| export type TTestStatus = 'success' | 'error' | 'pending' | 'errorAfterSuccess' | 'timeout'; | ||||
|  | ||||
| export interface ITestFunction<T> { | ||||
|   (tapTools?: TapTools): Promise<T>; | ||||
| } | ||||
|  | ||||
| export class TapTest<T = unknown> { | ||||
|   public description: string; | ||||
|   public failureAllowed: boolean; | ||||
|   public hrtMeasurement: HrtMeasurement; | ||||
|   public parallel: boolean; | ||||
|   public status: TTestStatus; | ||||
|   public tapTools: TapTools; | ||||
|   public testFunction: ITestFunction<T>; | ||||
|   public testKey: number; // the testKey the position in the test qeue. Set upon calling .run() | ||||
|   private testDeferred: Deferred<TapTest<T>> = plugins.smartpromise.defer(); | ||||
|   public testPromise: Promise<TapTest<T>> = this.testDeferred.promise; | ||||
|   private testResultDeferred: Deferred<T> = plugins.smartpromise.defer(); | ||||
|   public testResultPromise: Promise<T> = this.testResultDeferred.promise; | ||||
|   /** | ||||
|    * constructor | ||||
|    */ | ||||
|   constructor(optionsArg: { | ||||
|     description: string; | ||||
|     testFunction: ITestFunction<T>; | ||||
|     parallel: boolean; | ||||
|   }) { | ||||
|     this.description = optionsArg.description; | ||||
|     this.hrtMeasurement = new HrtMeasurement(); | ||||
|     this.parallel = optionsArg.parallel; | ||||
|     this.status = 'pending'; | ||||
|     this.tapTools = new TapTools(this); | ||||
|     this.testFunction = optionsArg.testFunction; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * run the test | ||||
|    */ | ||||
|   public async run(testKeyArg: number) { | ||||
|     this.hrtMeasurement.start(); | ||||
|     this.testKey = testKeyArg; | ||||
|     const testNumber = testKeyArg + 1; | ||||
|     try { | ||||
|       const testReturnValue = await this.testFunction(this.tapTools); | ||||
|       if (this.status === 'timeout') { | ||||
|         throw new Error('Test succeeded, but timed out...'); | ||||
|       } | ||||
|       this.hrtMeasurement.stop(); | ||||
|       console.log( | ||||
|         `ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`, | ||||
|       ); | ||||
|       this.status = 'success'; | ||||
|       this.testDeferred.resolve(this); | ||||
|       this.testResultDeferred.resolve(testReturnValue); | ||||
|     } catch (err: any) { | ||||
|       this.hrtMeasurement.stop(); | ||||
|       console.log( | ||||
|         `not ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`, | ||||
|       ); | ||||
|       this.testDeferred.resolve(this); | ||||
|       this.testResultDeferred.resolve(err); | ||||
|  | ||||
|       // if the test has already succeeded before | ||||
|       if (this.status === 'success') { | ||||
|         this.status = 'errorAfterSuccess'; | ||||
|         console.log('!!! ALERT !!!: weird behaviour, since test has been already successfull'); | ||||
|       } else { | ||||
|         this.status = 'error'; | ||||
|       } | ||||
|  | ||||
|       // if the test is allowed to fail | ||||
|       if (this.failureAllowed) { | ||||
|         console.log(`please note: failure allowed!`); | ||||
|       } | ||||
|       console.log(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										68
									
								
								ts_tapbundle/tapbundle.classes.taptools.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								ts_tapbundle/tapbundle.classes.taptools.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
| import { TapTest } from './tapbundle.classes.taptest.js'; | ||||
|  | ||||
| export interface IPromiseFunc { | ||||
|   (): Promise<any>; | ||||
| } | ||||
|  | ||||
| export class TapTools { | ||||
|   /** | ||||
|    * the referenced TapTest | ||||
|    */ | ||||
|   private _tapTest: TapTest; | ||||
|  | ||||
|   constructor(TapTestArg: TapTest<any>) { | ||||
|     this._tapTest = TapTestArg; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * allow failure | ||||
|    */ | ||||
|   public allowFailure() { | ||||
|     this._tapTest.failureAllowed = true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * async/await delay method | ||||
|    */ | ||||
|   public async delayFor(timeMilliArg: number) { | ||||
|     await plugins.smartdelay.delayFor(timeMilliArg); | ||||
|   } | ||||
|  | ||||
|   public async delayForRandom(timeMilliMinArg: number, timeMilliMaxArg: number) { | ||||
|     await plugins.smartdelay.delayForRandom(timeMilliMinArg, timeMilliMaxArg); | ||||
|   } | ||||
|  | ||||
|   public async coloredString(...args: Parameters<typeof plugins.consolecolor.coloredString>) { | ||||
|     return plugins.consolecolor.coloredString(...args); | ||||
|   } | ||||
|  | ||||
|   public async timeout(timeMilliArg: number) { | ||||
|     const timeout = new plugins.smartdelay.Timeout(timeMilliArg); | ||||
|     timeout.makeUnrefed(); | ||||
|     await timeout.promise; | ||||
|     if (this._tapTest.status === 'pending') { | ||||
|       this._tapTest.status = 'timeout'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async returnError(throwingFuncArg: IPromiseFunc) { | ||||
|     let funcErr: Error; | ||||
|     try { | ||||
|       await throwingFuncArg(); | ||||
|     } catch (err: any) { | ||||
|       funcErr = err; | ||||
|     } | ||||
|     return funcErr; | ||||
|   } | ||||
|  | ||||
|   public defer() { | ||||
|     return plugins.smartpromise.defer(); | ||||
|   } | ||||
|  | ||||
|   public cumulativeDefer() { | ||||
|     return plugins.smartpromise.cumulativeDefer(); | ||||
|   } | ||||
|  | ||||
|   public smartjson = plugins.smartjson; | ||||
| } | ||||
							
								
								
									
										13
									
								
								ts_tapbundle/tapbundle.classes.tapwrap.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								ts_tapbundle/tapbundle.classes.tapwrap.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
|  | ||||
| export interface ITapWrapOptions { | ||||
|   before: () => Promise<any>; | ||||
|   after: () => {}; | ||||
| } | ||||
|  | ||||
| export class TapWrap { | ||||
|   public options: ITapWrapOptions; | ||||
|   constructor(optionsArg: ITapWrapOptions) { | ||||
|     this.options = optionsArg; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								ts_tapbundle/tapbundle.plugins.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ts_tapbundle/tapbundle.plugins.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| // pushrocks | ||||
| import * as consolecolor from '@push.rocks/consolecolor'; | ||||
| import * as smartdelay from '@push.rocks/smartdelay'; | ||||
| import * as smartenv from '@push.rocks/smartenv'; | ||||
| import * as smartexpect from '@push.rocks/smartexpect'; | ||||
| import * as smartjson from '@push.rocks/smartjson'; | ||||
| import * as smartpromise from '@push.rocks/smartpromise'; | ||||
|  | ||||
| export { consolecolor, smartdelay, smartenv, smartexpect, smartjson, smartpromise }; | ||||
							
								
								
									
										7
									
								
								ts_tapbundle/tapbundle.tapcreator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ts_tapbundle/tapbundle.tapcreator.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
|  | ||||
| export class TapCreator { | ||||
|   // TODO: | ||||
| } | ||||
|  | ||||
| export let tapCreator = new TapCreator(); | ||||
							
								
								
									
										3
									
								
								ts_tapbundle/tspublish.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts_tapbundle/tspublish.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "order": 1 | ||||
| } | ||||
							
								
								
									
										40
									
								
								ts_tapbundle/webhelpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								ts_tapbundle/webhelpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
| import { tap } from './tapbundle.classes.tap.js'; | ||||
|  | ||||
| class WebHelpers { | ||||
|   html: any; | ||||
|   fixture: any; | ||||
|  | ||||
|   constructor() { | ||||
|     const smartenv = new plugins.smartenv.Smartenv(); | ||||
|      | ||||
|     // Initialize HTML template tag function | ||||
|     this.html = (strings: TemplateStringsArray, ...values: any[]) => { | ||||
|       let result = ''; | ||||
|       for (let i = 0; i < strings.length; i++) { | ||||
|         result += strings[i]; | ||||
|         if (i < values.length) { | ||||
|           result += values[i]; | ||||
|         } | ||||
|       } | ||||
|       return result; | ||||
|     }; | ||||
|  | ||||
|     // Initialize fixture function based on environment | ||||
|     if (smartenv.isBrowser) { | ||||
|       this.fixture = async (htmlString: string): Promise<HTMLElement> => { | ||||
|         const container = document.createElement('div'); | ||||
|         container.innerHTML = htmlString.trim(); | ||||
|         const element = container.firstChild as HTMLElement; | ||||
|         return element; | ||||
|       }; | ||||
|     } else { | ||||
|       // Node.js environment - provide a stub or alternative implementation | ||||
|       this.fixture = async (htmlString: string): Promise<any> => { | ||||
|         throw new Error('WebHelpers.fixture is only available in browser environment'); | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const webhelpers = new WebHelpers(); | ||||
							
								
								
									
										98
									
								
								ts_tapbundle_node/classes.tapnodetools.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								ts_tapbundle_node/classes.tapnodetools.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| import { TestFileProvider } from './classes.testfileprovider.js'; | ||||
| import * as plugins from './plugins.js'; | ||||
|  | ||||
| class TapNodeTools { | ||||
|   private smartshellInstance: plugins.smartshell.Smartshell; | ||||
|   public testFileProvider = new TestFileProvider(); | ||||
|  | ||||
|   constructor() {} | ||||
|  | ||||
|   private qenv: plugins.qenv.Qenv; | ||||
|   public async getQenv(): Promise<plugins.qenv.Qenv> { | ||||
|     this.qenv = this.qenv || new plugins.qenv.Qenv('./', '.nogit/'); | ||||
|     return this.qenv; | ||||
|   } | ||||
|   public async getEnvVarOnDemand(envVarNameArg: string): Promise<string> { | ||||
|     const qenv = await this.getQenv(); | ||||
|     return qenv.getEnvVarOnDemand(envVarNameArg); | ||||
|   } | ||||
|  | ||||
|   public async runCommand(commandArg: string): Promise<any> { | ||||
|     if (!this.smartshellInstance) { | ||||
|       this.smartshellInstance = new plugins.smartshell.Smartshell({ | ||||
|         executor: 'bash', | ||||
|       }); | ||||
|     } | ||||
|     const result = await this.smartshellInstance.exec(commandArg); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   public async createHttpsCert( | ||||
|     commonName: string = 'localhost', | ||||
|     allowSelfSigned: boolean = true | ||||
|   ): Promise<{ key: string; cert: string }> { | ||||
|     if (allowSelfSigned) { | ||||
|       // set node to allow self-signed certificates | ||||
|       process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; | ||||
|     } | ||||
|  | ||||
|     // Generate a key pair | ||||
|     const keys = plugins.smartcrypto.nodeForge.pki.rsa.generateKeyPair(2048); | ||||
|  | ||||
|     // Create a self-signed certificate | ||||
|     const cert = plugins.smartcrypto.nodeForge.pki.createCertificate(); | ||||
|     cert.publicKey = keys.publicKey; | ||||
|     cert.serialNumber = '01'; | ||||
|     cert.validity.notBefore = new Date(); | ||||
|     cert.validity.notAfter = new Date(); | ||||
|     cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); | ||||
|  | ||||
|     const attrs = [ | ||||
|       { name: 'commonName', value: commonName }, | ||||
|       { name: 'countryName', value: 'US' }, | ||||
|       { shortName: 'ST', value: 'California' }, | ||||
|       { name: 'localityName', value: 'San Francisco' }, | ||||
|       { name: 'organizationName', value: 'My Company' }, | ||||
|       { shortName: 'OU', value: 'Dev' }, | ||||
|     ]; | ||||
|     cert.setSubject(attrs); | ||||
|     cert.setIssuer(attrs); | ||||
|  | ||||
|     // Sign the certificate with its own private key (self-signed) | ||||
|     cert.sign(keys.privateKey, plugins.smartcrypto.nodeForge.md.sha256.create()); | ||||
|  | ||||
|     // PEM encode the private key and certificate | ||||
|     const pemKey = plugins.smartcrypto.nodeForge.pki.privateKeyToPem(keys.privateKey); | ||||
|     const pemCert = plugins.smartcrypto.nodeForge.pki.certificateToPem(cert); | ||||
|  | ||||
|     return { | ||||
|       key: pemKey, | ||||
|       cert: pemCert, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * create and return a smartmongo instance | ||||
|    */ | ||||
|   public async createSmartmongo() {  | ||||
|     const smartmongoMod = await import('@push.rocks/smartmongo'); | ||||
|     const smartmongoInstance = new smartmongoMod.SmartMongo(); | ||||
|     await smartmongoInstance.start(); | ||||
|     return smartmongoInstance; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * create and return a smarts3 instance | ||||
|    */ | ||||
|   public async createSmarts3() { | ||||
|     const smarts3Mod = await import('@push.rocks/smarts3'); | ||||
|     const smarts3Instance = new smarts3Mod.Smarts3({ | ||||
|       port: 3003, | ||||
|       cleanSlate: true, | ||||
|     }); | ||||
|     await smarts3Instance.start(); | ||||
|     return smarts3Instance; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const tapNodeTools = new TapNodeTools(); | ||||
							
								
								
									
										17
									
								
								ts_tapbundle_node/classes.testfileprovider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ts_tapbundle_node/classes.testfileprovider.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
| import * as paths from './paths.js'; | ||||
|  | ||||
| export const fileUrls = { | ||||
|   dockerAlpineImage: 'https://code.foss.global/testassets/docker/raw/branch/main/alpine.tar', | ||||
| } | ||||
|  | ||||
| export class TestFileProvider { | ||||
|   public async getDockerAlpineImageAsLocalTarball(): Promise<string> { | ||||
|     const filePath = plugins.path.join(paths.testFilesDir, 'alpine.tar') | ||||
|     // fetch the docker alpine image | ||||
|     const response = await plugins.smartrequest.getBinary(fileUrls.dockerAlpineImage); | ||||
|     await plugins.smartfile.fs.ensureDir(paths.testFilesDir); | ||||
|     await plugins.smartfile.memory.toFs(response.body, filePath); | ||||
|     return filePath; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										2
									
								
								ts_tapbundle_node/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_tapbundle_node/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export * from './classes.tapnodetools.js'; | ||||
|  | ||||
							
								
								
									
										4
									
								
								ts_tapbundle_node/paths.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ts_tapbundle_node/paths.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
|  | ||||
| export const cwd = process.cwd(); | ||||
| export const testFilesDir = plugins.path.join(cwd, './.nogit/testfiles/'); | ||||
							
								
								
									
										16
									
								
								ts_tapbundle_node/plugins.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								ts_tapbundle_node/plugins.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| // node native | ||||
| import * as crypto from 'crypto'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
|  | ||||
| export { crypto,fs, path, }; | ||||
|  | ||||
| // @push.rocks scope | ||||
| import * as qenv from '@push.rocks/qenv'; | ||||
| import * as smartcrypto from '@push.rocks/smartcrypto'; | ||||
| import * as smartfile from '@push.rocks/smartfile'; | ||||
| import * as smartpath from '@push.rocks/smartpath'; | ||||
| import * as smartrequest from '@push.rocks/smartrequest'; | ||||
| import * as smartshell from '@push.rocks/smartshell'; | ||||
|  | ||||
| export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, }; | ||||
		Reference in New Issue
	
	Block a user