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