fix(tstest): Fix test timing display issue and update TAP protocol documentation
This commit is contained in:
		
							
								
								
									
										454
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										454
									
								
								readme.md
									
									
									
									
									
								
							| @@ -141,9 +141,9 @@ tstest supports different test environments through file naming: | ||||
| | `*.browser.ts` | Browser environment | `test.ui.browser.ts` | | ||||
| | `*.both.ts` | Both Node.js and browser | `test.isomorphic.both.ts` | | ||||
|  | ||||
| ### Writing Tests | ||||
| ### Writing Tests with tapbundle | ||||
|  | ||||
| tstest includes a built-in TAP (Test Anything Protocol) test framework. Import it from the embedded tapbundle: | ||||
| tstest includes tapbundle, a powerful TAP-based test framework. Import it from the embedded tapbundle: | ||||
|  | ||||
| ```typescript | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| @@ -164,100 +164,392 @@ tstest provides multiple exports for different use cases: | ||||
| - `@git.zone/tstest/tapbundle` - Browser-compatible test framework | ||||
| - `@git.zone/tstest/tapbundle_node` - Node.js-specific test utilities | ||||
|  | ||||
| #### Test Features | ||||
| ## tapbundle Test Framework | ||||
|  | ||||
| ### Basic Test Syntax | ||||
|  | ||||
| **Tag-based Test Filtering** | ||||
| ```typescript | ||||
| tap.tags('unit', 'api') | ||||
|   .test('should handle API requests', async () => { | ||||
|     // Test code | ||||
|   }); | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| // Run with: tstest test/ --tags unit,api | ||||
| ``` | ||||
|  | ||||
| **Test Lifecycle Hooks** | ||||
| ```typescript | ||||
| tap.describe('User API Tests', () => { | ||||
|   let testUser; | ||||
|    | ||||
|   tap.beforeEach(async () => { | ||||
|     testUser = await createTestUser(); | ||||
|   }); | ||||
|    | ||||
|   tap.afterEach(async () => { | ||||
|     await deleteTestUser(testUser.id); | ||||
|   }); | ||||
|    | ||||
|   tap.test('should update user profile', async () => { | ||||
|     // Test code using testUser | ||||
|   }); | ||||
| // Basic test | ||||
| tap.test('should perform basic arithmetic', async () => { | ||||
|   expect(2 + 2).toEqual(4); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| **Parallel Test Execution** | ||||
| ```typescript | ||||
| // Files with matching parallel group names run concurrently | ||||
| // test.auth.para__1.ts | ||||
| tap.test('authentication test', async () => { /* ... */ }); | ||||
|  | ||||
| // test.user.para__1.ts   | ||||
| tap.test('user operations test', async () => { /* ... */ }); | ||||
| ``` | ||||
|  | ||||
| **Test Timeouts and Retries** | ||||
| ```typescript | ||||
| tap.timeout(5000) | ||||
|   .retry(3) | ||||
|   .test('flaky network test', async (tools) => { | ||||
|     // This test has 5 seconds to complete and will retry up to 3 times | ||||
|   }); | ||||
| ``` | ||||
|  | ||||
| **Snapshot Testing** | ||||
| ```typescript | ||||
| tap.test('should match snapshot', async (tools) => { | ||||
|   const result = await generateReport(); | ||||
|   await tools.matchSnapshot(result); | ||||
| // Async test with tools | ||||
| tap.test('async operations', async (tools) => { | ||||
|   await tools.delayFor(100); // delay for 100ms | ||||
|   const result = await fetchData(); | ||||
|   expect(result).toBeDefined(); | ||||
| }); | ||||
|  | ||||
| // Start test execution | ||||
| tap.start(); | ||||
| ``` | ||||
|  | ||||
| **Test Fixtures** | ||||
| ```typescript | ||||
| // Define a reusable fixture | ||||
| tap.defineFixture('testUser', async () => ({ | ||||
|   id: 1, | ||||
|   name: 'Test User', | ||||
|   email: 'test@example.com' | ||||
| })); | ||||
| ### Test Modifiers and Chaining | ||||
|  | ||||
| tap.test('user test', async (tools) => { | ||||
|   const user = tools.fixture('testUser'); | ||||
|   expect(user.name).toEqual('Test User'); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| **Skipping and Todo Tests** | ||||
| ```typescript | ||||
| tap.skip.test('work in progress', async () => { | ||||
| // Skip a test | ||||
| tap.skip.test('not ready yet', async () => { | ||||
|   // This test will be skipped | ||||
| }); | ||||
|  | ||||
| tap.todo('implement user deletion', async () => { | ||||
|   // This marks a test as todo | ||||
| // Run only this test (exclusive) | ||||
| tap.only.test('focus on this', async () => { | ||||
|   // Only this test will run | ||||
| }); | ||||
|  | ||||
| // Todo test | ||||
| tap.todo('implement later', async () => { | ||||
|   // Marked as todo | ||||
| }); | ||||
|  | ||||
| // Chaining modifiers | ||||
| tap.timeout(5000) | ||||
|    .retry(3) | ||||
|    .tags('api', 'integration') | ||||
|    .test('complex test', async (tools) => { | ||||
|      // Test with 5s timeout, 3 retries, and tags | ||||
|    }); | ||||
| ``` | ||||
|  | ||||
| ### Test Organization with describe() | ||||
|  | ||||
| ```typescript | ||||
| tap.describe('User Management', () => { | ||||
|   let testDatabase; | ||||
|    | ||||
|   tap.beforeEach(async () => { | ||||
|     testDatabase = await createTestDB(); | ||||
|   }); | ||||
|    | ||||
|   tap.afterEach(async () => { | ||||
|     await testDatabase.cleanup(); | ||||
|   }); | ||||
|    | ||||
|   tap.test('should create user', async () => { | ||||
|     const user = await testDatabase.createUser({ name: 'John' }); | ||||
|     expect(user.id).toBeDefined(); | ||||
|   }); | ||||
|    | ||||
|   tap.describe('User Permissions', () => { | ||||
|     tap.test('should set admin role', async () => { | ||||
|       // Nested describe blocks | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| **Browser Testing** | ||||
| ### Test Tools (Available in Test Function) | ||||
|  | ||||
| Every test function receives a `tools` parameter with utilities: | ||||
|  | ||||
| ```typescript | ||||
| tap.test('using test tools', async (tools) => { | ||||
|   // Delay utilities | ||||
|   await tools.delayFor(1000); // delay for 1000ms | ||||
|   await tools.delayForRandom(100, 500); // random delay between 100-500ms | ||||
|    | ||||
|   // Skip test conditionally | ||||
|   tools.skipIf(process.env.CI === 'true', 'Skipping in CI'); | ||||
|    | ||||
|   // Skip test unconditionally | ||||
|   if (!apiKeyAvailable) { | ||||
|     tools.skip('API key not available'); | ||||
|   } | ||||
|    | ||||
|   // Mark as todo | ||||
|   tools.todo('Needs implementation'); | ||||
|    | ||||
|   // Retry configuration | ||||
|   tools.retry(3); // Set retry count | ||||
|    | ||||
|   // Timeout configuration | ||||
|   tools.timeout(10000); // Set timeout to 10s | ||||
|    | ||||
|   // Context sharing between tests | ||||
|   tools.context.set('userId', 12345); | ||||
|   const userId = tools.context.get('userId'); | ||||
|    | ||||
|   // Deferred promises | ||||
|   const deferred = tools.defer(); | ||||
|   setTimeout(() => deferred.resolve('done'), 100); | ||||
|   await deferred.promise; | ||||
|    | ||||
|   // Colored console output | ||||
|   const coloredString = await tools.coloredString('Success!', 'green'); | ||||
|   console.log(coloredString); | ||||
|    | ||||
|   // Error handling helper | ||||
|   const error = await tools.returnError(async () => { | ||||
|     throw new Error('Expected error'); | ||||
|   }); | ||||
|   expect(error).toBeInstanceOf(Error); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Snapshot Testing | ||||
|  | ||||
| ```typescript | ||||
| tap.test('snapshot test', async (tools) => { | ||||
|   const output = generateComplexOutput(); | ||||
|    | ||||
|   // Compare with saved snapshot | ||||
|   await tools.matchSnapshot(output); | ||||
|    | ||||
|   // Named snapshots for multiple checks in one test | ||||
|   await tools.matchSnapshot(output.header, 'header'); | ||||
|   await tools.matchSnapshot(output.body, 'body'); | ||||
| }); | ||||
|  | ||||
| // Update snapshots with: UPDATE_SNAPSHOTS=true tstest test/ | ||||
| ``` | ||||
|  | ||||
| ### Test Fixtures | ||||
|  | ||||
| ```typescript | ||||
| // Define reusable fixtures | ||||
| tap.defineFixture('testUser', async (data) => ({ | ||||
|   id: Date.now(), | ||||
|   name: data?.name || 'Test User', | ||||
|   email: data?.email || 'test@example.com', | ||||
|   created: new Date() | ||||
| })); | ||||
|  | ||||
| tap.defineFixture('testPost', async (data) => ({ | ||||
|   id: Date.now(), | ||||
|   title: data?.title || 'Test Post', | ||||
|   authorId: data?.authorId || 1 | ||||
| })); | ||||
|  | ||||
| // Use fixtures in tests | ||||
| tap.test('fixture test', async (tools) => { | ||||
|   const user = await tools.fixture('testUser', { name: 'John' }); | ||||
|   const post = await tools.fixture('testPost', { authorId: user.id }); | ||||
|    | ||||
|   expect(post.authorId).toEqual(user.id); | ||||
|    | ||||
|   // Factory pattern for multiple instances | ||||
|   const users = await tools.factory('testUser').createMany(5); | ||||
|   expect(users).toHaveLength(5); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Parallel Test Execution | ||||
|  | ||||
| ```typescript | ||||
| // Parallel tests within a file | ||||
| tap.testParallel('parallel test 1', async () => { | ||||
|   await heavyOperation(); | ||||
| }); | ||||
|  | ||||
| tap.testParallel('parallel test 2', async () => { | ||||
|   await anotherHeavyOperation(); | ||||
| }); | ||||
|  | ||||
| // File naming for parallel groups | ||||
| // test.api.para__1.ts - runs in parallel with other para__1 files | ||||
| // test.db.para__1.ts - runs in parallel with other para__1 files | ||||
| // test.auth.para__2.ts - runs after para__1 group completes | ||||
| ``` | ||||
|  | ||||
| ### Assertions with expect() | ||||
|  | ||||
| tapbundle uses @push.rocks/smartexpect for assertions: | ||||
|  | ||||
| ```typescript | ||||
| // Basic assertions | ||||
| expect(value).toEqual(5); | ||||
| expect(value).not.toEqual(10); | ||||
| expect(obj).toDeepEqual({ a: 1, b: 2 }); | ||||
|  | ||||
| // Type assertions | ||||
| expect('hello').toBeTypeofString(); | ||||
| expect(42).toBeTypeofNumber(); | ||||
| expect(true).toBeTypeofBoolean(); | ||||
| expect([]).toBeArray(); | ||||
| expect({}).toBeTypeOf('object'); | ||||
|  | ||||
| // Comparison assertions | ||||
| expect(5).toBeGreaterThan(3); | ||||
| expect(3).toBeLessThan(5); | ||||
| expect(5).toBeGreaterThanOrEqual(5); | ||||
| expect(5).toBeLessThanOrEqual(5); | ||||
| expect(0.1 + 0.2).toBeCloseTo(0.3, 10); | ||||
|  | ||||
| // Truthiness | ||||
| expect(true).toBeTrue(); | ||||
| expect(false).toBeFalse(); | ||||
| expect('text').toBeTruthy(); | ||||
| expect(0).toBeFalsy(); | ||||
| expect(null).toBeNull(); | ||||
| expect(undefined).toBeUndefined(); | ||||
| expect(null).toBeNullOrUndefined(); | ||||
|  | ||||
| // String assertions | ||||
| expect('hello world').toStartWith('hello'); | ||||
| expect('hello world').toEndWith('world'); | ||||
| expect('hello world').toInclude('lo wo'); | ||||
| expect('hello world').toMatch(/^hello/); | ||||
| expect('option').toBeOneOf(['choice', 'option', 'alternative']); | ||||
|  | ||||
| // Array assertions | ||||
| expect([1, 2, 3]).toContain(2); | ||||
| expect([1, 2, 3]).toContainAll([1, 3]); | ||||
| expect([1, 2, 3]).toExclude(4); | ||||
| expect([1, 2, 3]).toHaveLength(3); | ||||
| expect([]).toBeEmptyArray(); | ||||
| expect([{ id: 1 }]).toContainEqual({ id: 1 }); | ||||
|  | ||||
| // Object assertions | ||||
| expect(obj).toHaveProperty('name'); | ||||
| expect(obj).toHaveProperty('user.email', 'test@example.com'); | ||||
| expect(obj).toHaveDeepProperty(['level1', 'level2']); | ||||
| expect(obj).toMatchObject({ name: 'John' }); | ||||
|  | ||||
| // Function assertions | ||||
| expect(() => { throw new Error('test'); }).toThrow(); | ||||
| expect(() => { throw new Error('test'); }).toThrow(Error); | ||||
| expect(() => { throw new Error('test error'); }).toThrowErrorMatching(/test/); | ||||
| expect(myFunction).not.toThrow(); | ||||
|  | ||||
| // Promise assertions | ||||
| await expect(Promise.resolve('value')).resolves.toEqual('value'); | ||||
| await expect(Promise.reject(new Error('fail'))).rejects.toThrow(); | ||||
|  | ||||
| // Custom assertions | ||||
| expect(7).customAssertion( | ||||
|   value => value % 2 === 1, | ||||
|   'Value is not odd' | ||||
| ); | ||||
| ``` | ||||
|  | ||||
| ### Pre-tasks | ||||
|  | ||||
| Run setup tasks before tests start: | ||||
|  | ||||
| ```typescript | ||||
| tap.preTask('setup database', async () => { | ||||
|   await initializeTestDatabase(); | ||||
|   console.log('Database initialized'); | ||||
| }); | ||||
|  | ||||
| tap.preTask('load environment', async () => { | ||||
|   await loadTestEnvironment(); | ||||
| }); | ||||
|  | ||||
| // Pre-tasks run in order before any tests | ||||
| ``` | ||||
|  | ||||
| ### Tag-based Test Filtering | ||||
|  | ||||
| ```typescript | ||||
| // Tag individual tests | ||||
| tap.tags('unit', 'api') | ||||
|    .test('api unit test', async () => { | ||||
|      // Test code | ||||
|    }); | ||||
|  | ||||
| tap.tags('integration', 'slow') | ||||
|    .test('database integration', async () => { | ||||
|      // Test code | ||||
|    }); | ||||
|  | ||||
| // Run only tests with specific tags | ||||
| // tstest test/ --tags unit,api | ||||
| ``` | ||||
|  | ||||
| ### Context Sharing | ||||
|  | ||||
| Share data between tests: | ||||
|  | ||||
| ```typescript | ||||
| tap.test('first test', async (tools) => { | ||||
|   const sessionId = await createSession(); | ||||
|   tools.context.set('sessionId', sessionId); | ||||
| }); | ||||
|  | ||||
| tap.test('second test', async (tools) => { | ||||
|   const sessionId = tools.context.get('sessionId'); | ||||
|   expect(sessionId).toBeDefined(); | ||||
|    | ||||
|   // Cleanup | ||||
|   tools.context.delete('sessionId'); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Browser Testing with webhelpers | ||||
|  | ||||
| For browser-specific tests: | ||||
|  | ||||
| ```typescript | ||||
| // test.browser.ts | ||||
| import { tap, webhelpers } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| tap.test('DOM manipulation', async () => { | ||||
|   // Create DOM elements from HTML strings | ||||
|   const element = await webhelpers.fixture(webhelpers.html` | ||||
|     <div>Hello World</div> | ||||
|     <div class="test-container"> | ||||
|       <h1>Test Title</h1> | ||||
|       <button id="test-btn">Click Me</button> | ||||
|     </div> | ||||
|   `); | ||||
|   expect(element).toBeInstanceOf(HTMLElement); | ||||
|    | ||||
|   expect(element.querySelector('h1').textContent).toEqual('Test Title'); | ||||
|    | ||||
|   // Simulate interactions | ||||
|   const button = element.querySelector('#test-btn'); | ||||
|   button.click(); | ||||
| }); | ||||
|  | ||||
| tap.test('CSS testing', async () => { | ||||
|   const styles = webhelpers.css` | ||||
|     .test-class { | ||||
|       color: red; | ||||
|       font-size: 16px; | ||||
|     } | ||||
|   `; | ||||
|    | ||||
|   // styles is a string that can be injected into the page | ||||
|   expect(styles).toInclude('color: red'); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Advanced Error Handling | ||||
|  | ||||
| ```typescript | ||||
| tap.test('error handling', async (tools) => { | ||||
|   // Capture errors without failing the test | ||||
|   const error = await tools.returnError(async () => { | ||||
|     await functionThatThrows(); | ||||
|   }); | ||||
|    | ||||
|   expect(error).toBeInstanceOf(Error); | ||||
|   expect(error.message).toEqual('Expected error message'); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Test Wrap | ||||
|  | ||||
| Create wrapped test environments: | ||||
|  | ||||
| ```typescript | ||||
| import { TapWrap } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| const tapWrap = new TapWrap({ | ||||
|   before: async () => { | ||||
|     console.log('Before all tests'); | ||||
|     await globalSetup(); | ||||
|   }, | ||||
|   after: async () => { | ||||
|     console.log('After all tests'); | ||||
|     await globalCleanup(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // Tests registered here will have the wrap lifecycle | ||||
| tapWrap.tap.test('wrapped test', async () => { | ||||
|   // This test runs with the wrap setup/teardown | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| @@ -330,6 +622,20 @@ tstest test/ --quiet | ||||
|  | ||||
| ## Changelog | ||||
|  | ||||
| ### Version 1.9.2 | ||||
| - 🐛 Fixed test timing display issue (removed duplicate timing in output) | ||||
| - 📝 Improved internal protocol design documentation | ||||
| - 🔧 Added protocol v2 utilities for future improvements | ||||
|  | ||||
| ### Version 1.9.1   | ||||
| - 🐛 Fixed log file naming to preserve directory structure | ||||
| - 📁 Log files now prevent collisions: `test__dir__file.log` | ||||
|  | ||||
| ### Version 1.9.0 | ||||
| - 📚 Comprehensive documentation update | ||||
| - 🏗️ Embedded tapbundle for better integration | ||||
| - 🌐 Full browser compatibility | ||||
|  | ||||
| ### Version 1.8.0 | ||||
| - 📦 Embedded tapbundle directly into tstest project | ||||
| - 🌐 Made tapbundle fully browser-compatible | ||||
|   | ||||
		Reference in New Issue
	
	Block a user