Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
4714d5e8ad | |||
ff6aae7159 | |||
d05ec21b73 | |||
956a880a4a | |||
ee11b1ac17 | |||
054cbb6b3c |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-12 - 2.5.0 - feat(tstest.classes.runtime.parser)
|
||||||
|
Add support for "all" runtime token and update docs/tests; regenerate lockfile and add local settings
|
||||||
|
|
||||||
|
- Add support for the `all` runtime token (expands to node, chromium, deno, bun) in tstest filename parser (tstest.classes.runtime.parser)
|
||||||
|
- Handle `all` with modifiers (e.g. `*.all.nonci.ts`) and mixed tokens (e.g. `node+all`) so it expands to the full runtime set
|
||||||
|
- Add unit tests covering `all` cases in test/test.runtime.parser.node.ts
|
||||||
|
- Update README (examples and tables) to document `.all.ts` and `.all.nonci.ts` usage and include a universal example
|
||||||
|
- Update ts files' parser comments and constants to include ALL_RUNTIMES
|
||||||
|
- Add deno.lock (dependency lockfile) and a local .claude/settings.local.json for project permissions / local settings
|
||||||
|
|
||||||
|
## 2025-10-11 - 2.4.3 - fix(docs)
|
||||||
|
Update documentation: expand README with multi-runtime architecture, add module READMEs, and add local dev settings
|
||||||
|
|
||||||
|
- Expanded project README: fixed typos, clarified availability header, and added a detailed Multi-Runtime Architecture section (runtimes, naming conventions, migration tool, examples, and runtime-specific notes).
|
||||||
|
- Inserted additional example output and adjusted JSON/example sections to reflect multi-runtime flows and updated totals/durations in examples.
|
||||||
|
- Added dedicated README files for ts_tapbundle, ts_tapbundle_node, and ts_tapbundle_protocol modules with API overviews and usage guides.
|
||||||
|
- Added .claude/settings.local.json to provide local development permissions/settings used by the project tooling.
|
||||||
|
- Minor formatting and documentation cleanup (whitespace, headings, and changelog entries).
|
||||||
|
|
||||||
|
## 2025-10-10 - 2.4.2 - fix(deno)
|
||||||
|
Enable additional Deno permissions for runtime adapters and add local dev settings
|
||||||
|
|
||||||
|
- Add --allow-sys, --allow-import and --node-modules-dir to the default Deno permission set used by the Deno runtime adapter
|
||||||
|
- Include the new permission flags in the fallback permissions array when constructing Deno command args
|
||||||
|
- Add .claude/settings.local.json to capture local development permissions and helper commands
|
||||||
|
|
||||||
## 2025-10-10 - 2.4.1 - fix(runtime/deno)
|
## 2025-10-10 - 2.4.1 - fix(runtime/deno)
|
||||||
Enable Deno runtime tests by adding required permissions and local settings
|
Enable Deno runtime tests by adding required permissions and local settings
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "2.4.1",
|
"version": "2.5.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
333
readme.md
333
readme.md
@@ -1,7 +1,7 @@
|
|||||||
# @git.zone/tstest
|
# @git.zone/tstest
|
||||||
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative across multiple runtimes!
|
||||||
|
|
||||||
## Availabililty and Links
|
## Availability and Links
|
||||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@git.zone/tstest)
|
* [npmjs.org (npm package)](https://www.npmjs.com/package/@git.zone/tstest)
|
||||||
* [code.foss.global (source)](https://code.foss.global/git.zone/tstest)
|
* [code.foss.global (source)](https://code.foss.global/git.zone/tstest)
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
### ✨ Key Features
|
### ✨ Key Features
|
||||||
|
|
||||||
- 🎯 **Smart Test Execution** - Run all tests, single files, or use glob patterns
|
- 🎯 **Smart Test Execution** - Run all tests, single files, or use glob patterns
|
||||||
|
- 🚀 **Multi-Runtime Support** - Run tests in Node.js, Deno, Bun, and Chromium
|
||||||
- 🎨 **Beautiful Output** - Color-coded results with emojis and clean formatting
|
- 🎨 **Beautiful Output** - Color-coded results with emojis and clean formatting
|
||||||
- 📊 **Multiple Output Modes** - Choose from normal, quiet, verbose, or JSON output
|
- 📊 **Multiple Output Modes** - Choose from normal, quiet, verbose, or JSON output
|
||||||
- 🔍 **Automatic Discovery** - Finds all your test files automatically
|
- 🔍 **Automatic Discovery** - Finds all your test files automatically
|
||||||
- 🌐 **Cross-Environment** - Supports Node.js and browser testing
|
|
||||||
- 📝 **Detailed Logging** - Optional file logging for debugging
|
- 📝 **Detailed Logging** - Optional file logging for debugging
|
||||||
- ⚡ **Performance Metrics** - See which tests are slow
|
- ⚡ **Performance Metrics** - See which tests are slow
|
||||||
- 🤖 **CI/CD Ready** - JSON output mode for automation
|
- 🤖 **CI/CD Ready** - JSON output mode for automation
|
||||||
@@ -26,13 +26,10 @@
|
|||||||
- ⏳ **Timeout Control** - Set custom timeouts for tests
|
- ⏳ **Timeout Control** - Set custom timeouts for tests
|
||||||
- 🔁 **Retry Logic** - Automatically retry failing tests
|
- 🔁 **Retry Logic** - Automatically retry failing tests
|
||||||
- 🛠️ **Test Fixtures** - Create reusable test data
|
- 🛠️ **Test Fixtures** - Create reusable test data
|
||||||
- 📦 **Browser-Compatible** - Full browser support with embedded tapbundle
|
|
||||||
- 👀 **Watch Mode** - Automatically re-run tests on file changes
|
- 👀 **Watch Mode** - Automatically re-run tests on file changes
|
||||||
- 📊 **Real-time Progress** - Live test execution progress updates
|
- 📊 **Real-time Progress** - Live test execution progress updates
|
||||||
- 🎨 **Visual Diffs** - Beautiful side-by-side diffs for failed assertions
|
- 🎨 **Visual Diffs** - Beautiful side-by-side diffs for failed assertions
|
||||||
- 🔄 **Event-based Reporting** - Real-time test lifecycle events
|
- 🔄 **Event-based Reporting** - Real-time test lifecycle events
|
||||||
- ⚙️ **Test Configuration** - Flexible test settings with .tstest.json files
|
|
||||||
- 🚀 **Protocol V2** - Enhanced TAP protocol with Unicode delimiters
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -42,6 +39,154 @@ npm install --save-dev @git.zone/tstest
|
|||||||
pnpm add -D @git.zone/tstest
|
pnpm add -D @git.zone/tstest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Multi-Runtime Architecture
|
||||||
|
|
||||||
|
tstest supports running your tests across multiple JavaScript runtimes, allowing you to verify cross-platform compatibility easily.
|
||||||
|
|
||||||
|
### Supported Runtimes
|
||||||
|
|
||||||
|
- **Node.js** - Default runtime, uses tsrun for TypeScript execution
|
||||||
|
- **Chromium** - Browser environment testing with full DOM support
|
||||||
|
- **Deno** - Secure TypeScript/JavaScript runtime with modern features
|
||||||
|
- **Bun** - Ultra-fast all-in-one JavaScript runtime
|
||||||
|
|
||||||
|
### Test File Naming Convention
|
||||||
|
|
||||||
|
Name your test files with runtime specifiers to control where they run:
|
||||||
|
|
||||||
|
| Pattern | Runtimes | Example |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| `*.ts` | Node.js only (default) | `test.api.ts` |
|
||||||
|
| `*.node.ts` | Node.js only | `test.server.node.ts` |
|
||||||
|
| `*.chromium.ts` | Chromium browser | `test.dom.chromium.ts` |
|
||||||
|
| `*.deno.ts` | Deno runtime | `test.http.deno.ts` |
|
||||||
|
| `*.bun.ts` | Bun runtime | `test.fast.bun.ts` |
|
||||||
|
| `*.all.ts` | All runtimes (Node, Chromium, Deno, Bun) | `test.universal.all.ts` |
|
||||||
|
| `*.node+chromium.ts` | Both Node.js and Chromium | `test.isomorphic.node+chromium.ts` |
|
||||||
|
| `*.node+deno.ts` | Both Node.js and Deno | `test.cross.node+deno.ts` |
|
||||||
|
| `*.deno+bun.ts` | Both Deno and Bun | `test.modern.deno+bun.ts` |
|
||||||
|
| `*.chromium.nonci.ts` | Chromium, skip in CI | `test.visual.chromium.nonci.ts` |
|
||||||
|
| `*.all.nonci.ts` | All runtimes, skip in CI | `test.comprehensive.all.nonci.ts` |
|
||||||
|
|
||||||
|
**Multi-Runtime Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test.api.all.ts - runs in all runtimes (Node, Chromium, Deno, Bun)
|
||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('universal HTTP test', async () => {
|
||||||
|
const response = await fetch('https://api.example.com/test');
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test.api.node+deno+bun.ts - runs in specific runtimes
|
||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('cross-runtime HTTP test', async () => {
|
||||||
|
const response = await fetch('https://api.example.com/test');
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime Execution Order
|
||||||
|
|
||||||
|
When multiple runtimes are specified, tests execute in this order:
|
||||||
|
1. Node.js
|
||||||
|
2. Bun
|
||||||
|
3. Deno
|
||||||
|
4. Chromium
|
||||||
|
|
||||||
|
### Legacy Naming (Deprecated)
|
||||||
|
|
||||||
|
The following patterns are still supported but deprecated. Use the migration tool to update:
|
||||||
|
|
||||||
|
| Legacy Pattern | Modern Equivalent | Migration Command |
|
||||||
|
|----------------|-------------------|-------------------|
|
||||||
|
| `*.browser.ts` | `*.chromium.ts` | `tstest migrate` |
|
||||||
|
| `*.both.ts` | `*.node+chromium.ts` | `tstest migrate` |
|
||||||
|
|
||||||
|
When running legacy files, tstest shows a deprecation warning with the suggested new name.
|
||||||
|
|
||||||
|
### Migration Tool
|
||||||
|
|
||||||
|
Migrate your test files from legacy naming to the new convention:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run - see what would change
|
||||||
|
tstest migrate --dry-run
|
||||||
|
|
||||||
|
# Apply migrations (uses git mv to preserve history)
|
||||||
|
tstest migrate --write
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration Features:**
|
||||||
|
- ✅ Uses `git mv` to preserve file history
|
||||||
|
- ✅ Idempotent - safe to run multiple times
|
||||||
|
- ✅ Dry-run by default for safety
|
||||||
|
- ✅ Colored output showing all changes
|
||||||
|
- ✅ Handles modifiers like `.nonci` correctly
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Test File Migration Tool
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
🔍 DRY RUN MODE - No files will be modified
|
||||||
|
|
||||||
|
Found 3 legacy test file(s)
|
||||||
|
|
||||||
|
Would migrate:
|
||||||
|
test.browser.ts
|
||||||
|
→ test.chromium.ts
|
||||||
|
|
||||||
|
Would migrate:
|
||||||
|
test.both.ts
|
||||||
|
→ test.node+chromium.ts
|
||||||
|
|
||||||
|
Would migrate:
|
||||||
|
test.auth.browser.nonci.ts
|
||||||
|
→ test.auth.chromium.nonci.ts
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
Summary:
|
||||||
|
Total legacy files: 3
|
||||||
|
Successfully migrated: 3
|
||||||
|
Errors: 0
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
To apply these changes, run:
|
||||||
|
tstest migrate --write
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime-Specific Permissions
|
||||||
|
|
||||||
|
#### Deno Runtime
|
||||||
|
|
||||||
|
Tests run with these permissions by default:
|
||||||
|
```bash
|
||||||
|
--allow-read
|
||||||
|
--allow-env
|
||||||
|
--allow-net
|
||||||
|
--allow-write
|
||||||
|
--allow-sys
|
||||||
|
--allow-import # Enables npm packages and Node.js built-ins
|
||||||
|
--node-modules-dir # Node.js compatibility mode
|
||||||
|
--sloppy-imports # Allows .js imports to resolve to .ts files
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure custom permissions in your test file or via environment variables.
|
||||||
|
|
||||||
|
#### Bun Runtime
|
||||||
|
|
||||||
|
Bun runs with its native TypeScript support and full access to Node.js APIs.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Test Execution
|
### Basic Test Execution
|
||||||
@@ -92,18 +237,29 @@ tstest "test/unit/*.ts"
|
|||||||
Pattern: test
|
Pattern: test
|
||||||
Found: 4 test file(s)
|
Found: 4 test file(s)
|
||||||
|
|
||||||
▶️ test/test.ts (1/4)
|
━━━ Part 1: Node.js ━━━
|
||||||
Runtime: node.js
|
|
||||||
✅ prepare test (1ms)
|
▶️ test/test.node+deno.ts (1/4)
|
||||||
Summary: 1/1 PASSED
|
Runtime: Node.js
|
||||||
|
✅ HTTP request works (12ms)
|
||||||
|
✅ JSON parsing works (3ms)
|
||||||
|
Summary: 2/2 PASSED in 1.2s
|
||||||
|
|
||||||
|
━━━ Part 2: Deno ━━━
|
||||||
|
|
||||||
|
▶️ test/test.node+deno.ts (1/4)
|
||||||
|
Runtime: Deno
|
||||||
|
✅ HTTP request works (15ms)
|
||||||
|
✅ JSON parsing works (2ms)
|
||||||
|
Summary: 2/2 PASSED in 1.1s
|
||||||
|
|
||||||
📊 Test Summary
|
📊 Test Summary
|
||||||
┌────────────────────────────────┐
|
┌────────────────────────────────┐
|
||||||
│ Total Files: 4 │
|
│ Total Files: 4 │
|
||||||
│ Total Tests: 4 │
|
│ Total Tests: 8 │
|
||||||
│ Passed: 4 │
|
│ Passed: 8 │
|
||||||
│ Failed: 0 │
|
│ Failed: 0 │
|
||||||
│ Duration: 542ms │
|
│ Duration: 2.4s │
|
||||||
└────────────────────────────────┘
|
└────────────────────────────────┘
|
||||||
|
|
||||||
ALL TESTS PASSED! 🎉
|
ALL TESTS PASSED! 🎉
|
||||||
@@ -141,19 +297,7 @@ Perfect for CI/CD pipelines:
|
|||||||
{"event":"summary","summary":{"totalFiles":4,"totalTests":4,"totalPassed":4,"totalFailed":0,"totalDuration":542}}
|
{"event":"summary","summary":{"totalFiles":4,"totalTests":4,"totalPassed":4,"totalFailed":0,"totalDuration":542}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test File Naming Conventions
|
## Writing Tests with tapbundle
|
||||||
|
|
||||||
tstest supports different test environments through file naming:
|
|
||||||
|
|
||||||
| Pattern | Environment | Example |
|
|
||||||
|---------|-------------|---------|
|
|
||||||
| `*.ts` | Node.js (default) | `test.basic.ts` |
|
|
||||||
| `*.node.ts` | Node.js only | `test.api.node.ts` |
|
|
||||||
| `*.chrome.ts` | Chrome browser | `test.dom.chrome.ts` |
|
|
||||||
| `*.browser.ts` | Browser environment | `test.ui.browser.ts` |
|
|
||||||
| `*.both.ts` | Both Node.js and browser | `test.isomorphic.both.ts` |
|
|
||||||
|
|
||||||
### Writing Tests with tapbundle
|
|
||||||
|
|
||||||
tstest includes tapbundle, a powerful TAP-based test framework. Import it from the embedded tapbundle:
|
tstest includes tapbundle, a powerful TAP-based test framework. Import it from the embedded tapbundle:
|
||||||
|
|
||||||
@@ -165,7 +309,7 @@ tap.test('my awesome test', async () => {
|
|||||||
expect(result).toEqual('expected value');
|
expect(result).toEqual('expected value');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Module Exports**
|
**Module Exports**
|
||||||
@@ -196,7 +340,7 @@ tap.test('async operations', async (tools) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start test execution
|
// Start test execution
|
||||||
tap.start();
|
export default tap.start();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Modifiers and Chaining
|
### Test Modifiers and Chaining
|
||||||
@@ -231,20 +375,20 @@ tap.timeout(5000)
|
|||||||
```typescript
|
```typescript
|
||||||
tap.describe('User Management', () => {
|
tap.describe('User Management', () => {
|
||||||
let testDatabase;
|
let testDatabase;
|
||||||
|
|
||||||
tap.beforeEach(async () => {
|
tap.beforeEach(async () => {
|
||||||
testDatabase = await createTestDB();
|
testDatabase = await createTestDB();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.afterEach(async () => {
|
tap.afterEach(async () => {
|
||||||
await testDatabase.cleanup();
|
await testDatabase.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create user', async () => {
|
tap.test('should create user', async () => {
|
||||||
const user = await testDatabase.createUser({ name: 'John' });
|
const user = await testDatabase.createUser({ name: 'John' });
|
||||||
expect(user.id).toBeDefined();
|
expect(user.id).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.describe('User Permissions', () => {
|
tap.describe('User Permissions', () => {
|
||||||
tap.test('should set admin role', async () => {
|
tap.test('should set admin role', async () => {
|
||||||
// Nested describe blocks
|
// Nested describe blocks
|
||||||
@@ -262,37 +406,37 @@ tap.test('using test tools', async (tools) => {
|
|||||||
// Delay utilities
|
// Delay utilities
|
||||||
await tools.delayFor(1000); // delay for 1000ms
|
await tools.delayFor(1000); // delay for 1000ms
|
||||||
await tools.delayForRandom(100, 500); // random delay between 100-500ms
|
await tools.delayForRandom(100, 500); // random delay between 100-500ms
|
||||||
|
|
||||||
// Skip test conditionally
|
// Skip test conditionally
|
||||||
tools.skipIf(process.env.CI === 'true', 'Skipping in CI');
|
tools.skipIf(process.env.CI === 'true', 'Skipping in CI');
|
||||||
|
|
||||||
// Skip test unconditionally
|
// Skip test unconditionally
|
||||||
if (!apiKeyAvailable) {
|
if (!apiKeyAvailable) {
|
||||||
tools.skip('API key not available');
|
tools.skip('API key not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as todo
|
// Mark as todo
|
||||||
tools.todo('Needs implementation');
|
tools.todo('Needs implementation');
|
||||||
|
|
||||||
// Retry configuration
|
// Retry configuration
|
||||||
tools.retry(3); // Set retry count
|
tools.retry(3); // Set retry count
|
||||||
|
|
||||||
// Timeout configuration
|
// Timeout configuration
|
||||||
tools.timeout(10000); // Set timeout to 10s
|
tools.timeout(10000); // Set timeout to 10s
|
||||||
|
|
||||||
// Context sharing between tests
|
// Context sharing between tests
|
||||||
tools.context.set('userId', 12345);
|
tools.context.set('userId', 12345);
|
||||||
const userId = tools.context.get('userId');
|
const userId = tools.context.get('userId');
|
||||||
|
|
||||||
// Deferred promises
|
// Deferred promises
|
||||||
const deferred = tools.defer();
|
const deferred = tools.defer();
|
||||||
setTimeout(() => deferred.resolve('done'), 100);
|
setTimeout(() => deferred.resolve('done'), 100);
|
||||||
await deferred.promise;
|
await deferred.promise;
|
||||||
|
|
||||||
// Colored console output
|
// Colored console output
|
||||||
const coloredString = await tools.coloredString('Success!', 'green');
|
const coloredString = await tools.coloredString('Success!', 'green');
|
||||||
console.log(coloredString);
|
console.log(coloredString);
|
||||||
|
|
||||||
// Error handling helper
|
// Error handling helper
|
||||||
const error = await tools.returnError(async () => {
|
const error = await tools.returnError(async () => {
|
||||||
throw new Error('Expected error');
|
throw new Error('Expected error');
|
||||||
@@ -306,10 +450,10 @@ tap.test('using test tools', async (tools) => {
|
|||||||
```typescript
|
```typescript
|
||||||
tap.test('snapshot test', async (tools) => {
|
tap.test('snapshot test', async (tools) => {
|
||||||
const output = generateComplexOutput();
|
const output = generateComplexOutput();
|
||||||
|
|
||||||
// Compare with saved snapshot
|
// Compare with saved snapshot
|
||||||
await tools.matchSnapshot(output);
|
await tools.matchSnapshot(output);
|
||||||
|
|
||||||
// Named snapshots for multiple checks in one test
|
// Named snapshots for multiple checks in one test
|
||||||
await tools.matchSnapshot(output.header, 'header');
|
await tools.matchSnapshot(output.header, 'header');
|
||||||
await tools.matchSnapshot(output.body, 'body');
|
await tools.matchSnapshot(output.body, 'body');
|
||||||
@@ -339,9 +483,9 @@ tap.defineFixture('testPost', async (data) => ({
|
|||||||
tap.test('fixture test', async (tools) => {
|
tap.test('fixture test', async (tools) => {
|
||||||
const user = await tools.fixture('testUser', { name: 'John' });
|
const user = await tools.fixture('testUser', { name: 'John' });
|
||||||
const post = await tools.fixture('testPost', { authorId: user.id });
|
const post = await tools.fixture('testPost', { authorId: user.id });
|
||||||
|
|
||||||
expect(post.authorId).toEqual(user.id);
|
expect(post.authorId).toEqual(user.id);
|
||||||
|
|
||||||
// Factory pattern for multiple instances
|
// Factory pattern for multiple instances
|
||||||
const users = await tools.factory('testUser').createMany(5);
|
const users = await tools.factory('testUser').createMany(5);
|
||||||
expect(users).toHaveLength(5);
|
expect(users).toHaveLength(5);
|
||||||
@@ -485,7 +629,7 @@ tap.test('first test', async (tools) => {
|
|||||||
tap.test('second test', async (tools) => {
|
tap.test('second test', async (tools) => {
|
||||||
const sessionId = tools.context.get('sessionId');
|
const sessionId = tools.context.get('sessionId');
|
||||||
expect(sessionId).toBeDefined();
|
expect(sessionId).toBeDefined();
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
tools.context.delete('sessionId');
|
tools.context.delete('sessionId');
|
||||||
});
|
});
|
||||||
@@ -506,9 +650,9 @@ tap.test('DOM manipulation', async () => {
|
|||||||
<button id="test-btn">Click Me</button>
|
<button id="test-btn">Click Me</button>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(element.querySelector('h1').textContent).toEqual('Test Title');
|
expect(element.querySelector('h1').textContent).toEqual('Test Title');
|
||||||
|
|
||||||
// Simulate interactions
|
// Simulate interactions
|
||||||
const button = element.querySelector('#test-btn');
|
const button = element.querySelector('#test-btn');
|
||||||
button.click();
|
button.click();
|
||||||
@@ -521,7 +665,7 @@ tap.test('CSS testing', async () => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// styles is a string that can be injected into the page
|
// styles is a string that can be injected into the page
|
||||||
expect(styles).toInclude('color: red');
|
expect(styles).toInclude('color: red');
|
||||||
});
|
});
|
||||||
@@ -535,7 +679,7 @@ tap.test('error handling', async (tools) => {
|
|||||||
const error = await tools.returnError(async () => {
|
const error = await tools.returnError(async () => {
|
||||||
await functionThatThrows();
|
await functionThatThrows();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
expect(error.message).toEqual('Expected error message');
|
expect(error.message).toEqual('Expected error message');
|
||||||
});
|
});
|
||||||
@@ -612,7 +756,7 @@ When assertions fail, tstest shows beautiful side-by-side diffs:
|
|||||||
String Diff:
|
String Diff:
|
||||||
- Expected
|
- Expected
|
||||||
+ Received
|
+ Received
|
||||||
|
|
||||||
- Hello World
|
- Hello World
|
||||||
+ Hello Universe
|
+ Hello Universe
|
||||||
|
|
||||||
@@ -625,73 +769,6 @@ When assertions fail, tstest shows beautiful side-by-side diffs:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Configuration (.tstest.json)
|
|
||||||
|
|
||||||
Configure test behavior with `.tstest.json` files:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timeout": 30000,
|
|
||||||
"retries": 2,
|
|
||||||
"bail": false,
|
|
||||||
"parallel": true,
|
|
||||||
"tags": ["unit", "fast"],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration files are discovered in:
|
|
||||||
1. Test file directory
|
|
||||||
2. Parent directories (up to project root)
|
|
||||||
3. Project root
|
|
||||||
4. Home directory (`~/.tstest.json`)
|
|
||||||
|
|
||||||
Settings cascade and merge, with closer files taking precedence.
|
|
||||||
|
|
||||||
### Event-based Test Reporting
|
|
||||||
|
|
||||||
tstest emits detailed events during test execution for integration with CI/CD tools:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"event":"suite:started","file":"test/api.test.ts","timestamp":"2025-05-26T10:30:00.000Z"}
|
|
||||||
{"event":"test:started","name":"api endpoint validation","timestamp":"2025-05-26T10:30:00.100Z"}
|
|
||||||
{"event":"test:progress","name":"api endpoint validation","message":"Validating response schema"}
|
|
||||||
{"event":"test:completed","name":"api endpoint validation","passed":true,"duration":145}
|
|
||||||
{"event":"suite:completed","file":"test/api.test.ts","passed":true,"total":2,"failed":0}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enhanced TAP Protocol (Protocol V2)
|
|
||||||
|
|
||||||
tstest uses an enhanced TAP protocol with Unicode delimiters for better parsing:
|
|
||||||
|
|
||||||
```
|
|
||||||
⟦TSTEST:EVENT:test:started⟧{"name":"my test","timestamp":"2025-05-26T10:30:00.000Z"}
|
|
||||||
ok 1 my test
|
|
||||||
⟦TSTEST:EVENT:test:completed⟧{"name":"my test","passed":true,"duration":145}
|
|
||||||
```
|
|
||||||
|
|
||||||
This prevents conflicts with test output that might contain TAP-like formatting.
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### Glob Pattern Support
|
|
||||||
|
|
||||||
Run specific test patterns:
|
|
||||||
```bash
|
|
||||||
# Run all unit tests
|
|
||||||
tstest "test/unit/**/*.ts"
|
|
||||||
|
|
||||||
# Run all integration tests
|
|
||||||
tstest "test/integration/*.test.ts"
|
|
||||||
|
|
||||||
# Run multiple patterns
|
|
||||||
tstest "test/**/*.spec.ts" "test/**/*.test.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Always quote glob patterns to prevent shell expansion. Without quotes, the shell will expand the pattern and only pass the first matching file to tstest.
|
|
||||||
|
|
||||||
### Enhanced Test Logging
|
### Enhanced Test Logging
|
||||||
|
|
||||||
The `--logfile` option provides intelligent test logging with automatic organization:
|
The `--logfile` option provides intelligent test logging with automatic organization:
|
||||||
@@ -849,6 +926,18 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### Version 2.4.0
|
||||||
|
- 🚀 **Multi-Runtime Architecture** - Support for Deno, Bun, Node.js, and Chromium
|
||||||
|
- 🔀 **New Naming Convention** - Flexible `.runtime1+runtime2.ts` pattern
|
||||||
|
- 🌐 **Universal Testing** - `.all.ts` pattern runs tests on all supported runtimes
|
||||||
|
- 🔄 **Migration Tool** - Easy migration from legacy naming (`.browser.ts`, `.both.ts`)
|
||||||
|
- 🦕 **Deno Support** - Full Deno runtime with Node.js compatibility
|
||||||
|
- 🐰 **Bun Support** - Ultra-fast Bun runtime integration
|
||||||
|
- ⚡ **Dynamic Port Selection** - Random port allocation (30000-40000) prevents conflicts
|
||||||
|
- 🏗️ **Runtime Adapter Pattern** - Extensible architecture for adding new runtimes
|
||||||
|
- 📝 **Deprecation Warnings** - Helpful migration suggestions for legacy naming
|
||||||
|
- ✅ **Comprehensive Tests** - Full test coverage for parser and migration tool
|
||||||
|
|
||||||
### Version 1.11.0
|
### Version 1.11.0
|
||||||
- 👀 Added Watch Mode with `--watch`/`-w` flag for automatic test re-runs
|
- 👀 Added Watch Mode with `--watch`/`-w` flag for automatic test re-runs
|
||||||
- 📊 Implemented real-time test progress updates with event streaming
|
- 📊 Implemented real-time test progress updates with event streaming
|
||||||
@@ -878,7 +967,7 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
|||||||
- 📝 Improved internal protocol design documentation
|
- 📝 Improved internal protocol design documentation
|
||||||
- 🔧 Added protocol v2 utilities for future improvements
|
- 🔧 Added protocol v2 utilities for future improvements
|
||||||
|
|
||||||
### Version 1.9.1
|
### Version 1.9.1
|
||||||
- 🐛 Fixed log file naming to preserve directory structure
|
- 🐛 Fixed log file naming to preserve directory structure
|
||||||
- 📁 Log files now prevent collisions: `test__dir__file.log`
|
- 📁 Log files now prevent collisions: `test__dir__file.log`
|
||||||
|
|
||||||
@@ -901,7 +990,7 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license.md) file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
@@ -911,9 +1000,9 @@ This project is owned and maintained by Task Venture Capital GmbH. The names and
|
|||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
@@ -164,4 +164,40 @@ tap.test('parseTestFilename - handles full paths', async () => {
|
|||||||
expect(parsed.original).toEqual('test.node+chromium.ts');
|
expect(parsed.original).toEqual('test.node+chromium.ts');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword expands to all runtimes', async () => {
|
||||||
|
const parsed = parseTestFilename('test.all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword with nonci modifier', async () => {
|
||||||
|
const parsed = parseTestFilename('test.all.nonci.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual(['nonci']);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword with complex basename', async () => {
|
||||||
|
const parsed = parseTestFilename('test.some.feature.all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test.some.feature');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword in chain expands to all runtimes', async () => {
|
||||||
|
const parsed = parseTestFilename('test.node+all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '2.4.1',
|
version: '2.5.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,10 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
'--allow-env',
|
'--allow-env',
|
||||||
'--allow-net',
|
'--allow-net',
|
||||||
'--allow-write',
|
'--allow-write',
|
||||||
'--sloppy-imports', // Allow .js imports to resolve to .ts files
|
'--allow-sys', // Allow system info access
|
||||||
|
'--allow-import', // Allow npm/node imports
|
||||||
|
'--node-modules-dir', // Enable Node.js compatibility mode
|
||||||
|
'--sloppy-imports', // Allow .js imports to resolve to .ts files
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
'--allow-env',
|
'--allow-env',
|
||||||
'--allow-net',
|
'--allow-net',
|
||||||
'--allow-write',
|
'--allow-write',
|
||||||
|
'--allow-sys',
|
||||||
|
'--allow-import',
|
||||||
|
'--node-modules-dir',
|
||||||
'--sloppy-imports',
|
'--sloppy-imports',
|
||||||
];
|
];
|
||||||
args.push(...permissions);
|
args.push(...permissions);
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
* - test.chromium.ts
|
* - test.chromium.ts
|
||||||
* - test.node+chromium.ts
|
* - test.node+chromium.ts
|
||||||
* - test.deno+bun.ts
|
* - test.deno+bun.ts
|
||||||
|
* - test.all.ts (runs on all runtimes)
|
||||||
* - test.chromium.nonci.ts
|
* - test.chromium.nonci.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export interface ParserConfig {
|
|||||||
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
||||||
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
||||||
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
||||||
|
const ALL_RUNTIMES: Runtime[] = ['node', 'chromium', 'deno', 'bun'];
|
||||||
|
|
||||||
// Legacy mappings for backwards compatibility
|
// Legacy mappings for backwards compatibility
|
||||||
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
||||||
@@ -102,9 +104,12 @@ export function parseTestFilename(
|
|||||||
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
||||||
const validRuntimes: Runtime[] = [];
|
const validRuntimes: Runtime[] = [];
|
||||||
const invalidRuntimes: string[] = [];
|
const invalidRuntimes: string[] = [];
|
||||||
|
let hasAllKeyword = false;
|
||||||
|
|
||||||
for (const candidate of runtimeCandidates) {
|
for (const candidate of runtimeCandidates) {
|
||||||
if (KNOWN_RUNTIMES.has(candidate)) {
|
if (candidate === 'all') {
|
||||||
|
hasAllKeyword = true;
|
||||||
|
} else if (KNOWN_RUNTIMES.has(candidate)) {
|
||||||
// Dedupe: only add if not already in list
|
// Dedupe: only add if not already in list
|
||||||
if (!validRuntimes.includes(candidate as Runtime)) {
|
if (!validRuntimes.includes(candidate as Runtime)) {
|
||||||
validRuntimes.push(candidate as Runtime);
|
validRuntimes.push(candidate as Runtime);
|
||||||
@@ -114,11 +119,18 @@ export function parseTestFilename(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If 'all' keyword is present, expand to all runtimes
|
||||||
|
if (hasAllKeyword) {
|
||||||
|
runtimes = [...ALL_RUNTIMES];
|
||||||
|
runtimeTokenIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (invalidRuntimes.length > 0) {
|
if (invalidRuntimes.length > 0) {
|
||||||
if (strictUnknownRuntime) {
|
if (strictUnknownRuntime) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
||||||
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}`
|
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}, all`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -138,6 +150,13 @@ export function parseTestFilename(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the 'all' keyword (expands to all runtimes)
|
||||||
|
if (token === 'all') {
|
||||||
|
runtimes = [...ALL_RUNTIMES];
|
||||||
|
runtimeTokenIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a single runtime token
|
// Check if this is a single runtime token
|
||||||
if (KNOWN_RUNTIMES.has(token)) {
|
if (KNOWN_RUNTIMES.has(token)) {
|
||||||
runtimes = [token as Runtime];
|
runtimes = [token as Runtime];
|
||||||
|
389
ts_tapbundle/readme.md
Normal file
389
ts_tapbundle/readme.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# @git.zone/tstest/tapbundle
|
||||||
|
|
||||||
|
> 🧪 Core TAP testing framework with enhanced assertions and lifecycle hooks
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# tapbundle is typically included as part of @git.zone/tstest
|
||||||
|
pnpm install --save-dev @git.zone/tstest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@git.zone/tstest/tapbundle` is the core testing framework module that provides the TAP (Test Anything Protocol) implementation for tstest. It offers a comprehensive API for writing and organizing tests with support for lifecycle hooks, test suites, enhanced assertions with diff generation, and flexible test configuration.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- 🎯 **TAP Protocol Compliant** - Full TAP version 13 support
|
||||||
|
- 🔍 **Enhanced Assertions** - Built on smartexpect with automatic diff generation
|
||||||
|
- 🏗️ **Test Suites** - Organize tests with `describe()` blocks
|
||||||
|
- 🔄 **Lifecycle Hooks** - beforeEach/afterEach at suite and global levels
|
||||||
|
- 🏷️ **Test Tagging** - Filter tests by tags for selective execution
|
||||||
|
- ⚡ **Parallel Testing** - Run tests concurrently with `testParallel()`
|
||||||
|
- 🔁 **Automatic Retries** - Configure retry logic for flaky tests
|
||||||
|
- ⏱️ **Timeout Control** - Set timeouts at global, file, or test level
|
||||||
|
- 🎨 **Fluent API** - Chain test configurations with builder pattern
|
||||||
|
- 📊 **Protocol Events** - Real-time test execution events
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Simple Test File
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('should add numbers correctly', async () => {
|
||||||
|
const result = 2 + 2;
|
||||||
|
expect(result).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Test Suites
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.describe('Calculator', () => {
|
||||||
|
tap.beforeEach(async (tapTools) => {
|
||||||
|
// Setup before each test in this suite
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should add', async () => {
|
||||||
|
expect(2 + 2).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should subtract', async () => {
|
||||||
|
expect(5 - 3).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async (tapTools) => {
|
||||||
|
// Cleanup after each test in this suite
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Main Test Methods
|
||||||
|
|
||||||
|
#### `tap.test(description, testFunction)`
|
||||||
|
|
||||||
|
Define a standard test that runs sequentially.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.test('should validate user input', async () => {
|
||||||
|
// test code
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tap.testParallel(description, testFunction)`
|
||||||
|
|
||||||
|
Define a test that runs in parallel with other parallel tests.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.testParallel('should fetch user data', async () => {
|
||||||
|
// test code
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tap.describe(description, suiteFunction)`
|
||||||
|
|
||||||
|
Create a test suite to group related tests.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.describe('User Authentication', () => {
|
||||||
|
tap.test('should login', async () => { });
|
||||||
|
tap.test('should logout', async () => { });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Modes
|
||||||
|
|
||||||
|
#### Skip Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.skip.test('not ready yet', async () => {
|
||||||
|
// This test will be skipped
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Only Mode
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.only.test('focus on this test', async () => {
|
||||||
|
// Only tests marked with 'only' will run
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Todo Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.todo.test('implement feature X');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fluent Test Builder
|
||||||
|
|
||||||
|
Chain test configurations for expressive test definitions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap
|
||||||
|
.tags('integration', 'database')
|
||||||
|
.priority('high')
|
||||||
|
.retry(3)
|
||||||
|
.timeout(5000)
|
||||||
|
.test('should handle database connection', async () => {
|
||||||
|
// test with configured settings
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle Hooks
|
||||||
|
|
||||||
|
#### Suite-Level Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.describe('Database Tests', () => {
|
||||||
|
tap.beforeEach(async (tapTools) => {
|
||||||
|
// Runs before each test in this suite
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async (tapTools) => {
|
||||||
|
// Runs after each test in this suite
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test 1', async () => { });
|
||||||
|
tap.test('test 2', async () => { });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Global Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.settings({
|
||||||
|
beforeAll: async () => {
|
||||||
|
// Runs once before all tests
|
||||||
|
},
|
||||||
|
afterAll: async () => {
|
||||||
|
// Runs once after all tests
|
||||||
|
},
|
||||||
|
beforeEach: async (testName) => {
|
||||||
|
// Runs before every test
|
||||||
|
},
|
||||||
|
afterEach: async (testName, passed) => {
|
||||||
|
// Runs after every test
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Settings
|
||||||
|
|
||||||
|
Configure test behavior at the file level:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.settings({
|
||||||
|
timeout: 10000, // Default timeout for all tests
|
||||||
|
retries: 2, // Retry failed tests
|
||||||
|
retryDelay: 1000, // Delay between retries
|
||||||
|
bail: false, // Stop on first failure
|
||||||
|
suppressConsole: false, // Hide console output
|
||||||
|
verboseErrors: true, // Show full stack traces
|
||||||
|
showTestDuration: true, // Display test durations
|
||||||
|
maxConcurrency: 4, // Max parallel tests
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enhanced Assertions
|
||||||
|
|
||||||
|
The `expect` function is an enhanced wrapper around [@push.rocks/smartexpect](https://code.foss.global/push.rocks/smartexpect) that automatically generates diffs for failed assertions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('should compare objects', async () => {
|
||||||
|
const actual = { name: 'John', age: 30 };
|
||||||
|
const expected = { name: 'John', age: 31 };
|
||||||
|
|
||||||
|
// Will show a detailed diff of the differences
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Available Assertions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Equality
|
||||||
|
expect(value).toEqual(expected);
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
|
||||||
|
// Truthiness
|
||||||
|
expect(value).toBeTruthy();
|
||||||
|
expect(value).toBeFalsy();
|
||||||
|
|
||||||
|
// Type checks
|
||||||
|
expect(value).toBeType('string');
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
expect(string).toMatch(/pattern/);
|
||||||
|
expect(string).toContain('substring');
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
expect(array).toContain(item);
|
||||||
|
|
||||||
|
// Exceptions
|
||||||
|
expect(fn).toThrow();
|
||||||
|
expect(fn).toThrow('error message');
|
||||||
|
|
||||||
|
// Async
|
||||||
|
await expect(promise).toResolve();
|
||||||
|
await expect(promise).toReject();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Tagging and Filtering
|
||||||
|
|
||||||
|
Tag tests for selective execution:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define tests with tags
|
||||||
|
tap.tags('integration', 'slow').test('complex test', async () => {
|
||||||
|
// test code
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.tags('unit').test('fast test', async () => {
|
||||||
|
// test code
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Filter tests by setting the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TSTEST_FILTER_TAGS=unit tstest test/mytest.node.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### TapTools
|
||||||
|
|
||||||
|
Each test receives a `tapTools` instance with utilities:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.test('should have utilities', async (tapTools) => {
|
||||||
|
// Mark test as skipped
|
||||||
|
tapTools.markAsSkipped('reason');
|
||||||
|
|
||||||
|
// Mark as todo
|
||||||
|
tapTools.todo('not implemented');
|
||||||
|
|
||||||
|
// Configure retries
|
||||||
|
tapTools.retry(3);
|
||||||
|
|
||||||
|
// Log test output
|
||||||
|
tapTools.log('debug message');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Pre-Tasks
|
||||||
|
|
||||||
|
Run setup tasks before any tests execute:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.preTask('setup database', async () => {
|
||||||
|
// Runs before any tests
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('first test', async () => {
|
||||||
|
// Database is ready
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Priority
|
||||||
|
|
||||||
|
Organize tests by priority level:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.priority('high').test('critical test', async () => { });
|
||||||
|
tap.priority('medium').test('normal test', async () => { });
|
||||||
|
tap.priority('low').test('optional test', async () => { });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Suites
|
||||||
|
|
||||||
|
Create deeply nested test organization:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.describe('API', () => {
|
||||||
|
tap.describe('Users', () => {
|
||||||
|
tap.describe('GET /users', () => {
|
||||||
|
tap.test('should return all users', async () => { });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protocol Events
|
||||||
|
|
||||||
|
Access real-time test events for custom tooling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setProtocolEmitter } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
// Get access to protocol emitter for custom event handling
|
||||||
|
// Events: test:started, test:completed, assertion:failed, suite:started, suite:completed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always export `tap.start()`** at the end of test files:
|
||||||
|
```typescript
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use descriptive test names** that explain what is being tested:
|
||||||
|
```typescript
|
||||||
|
tap.test('should return 404 when user does not exist', async () => { });
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Group related tests** with `describe()` blocks:
|
||||||
|
```typescript
|
||||||
|
tap.describe('User validation', () => {
|
||||||
|
// All user validation tests
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Leverage lifecycle hooks** to reduce duplication:
|
||||||
|
```typescript
|
||||||
|
tap.beforeEach(async () => {
|
||||||
|
// Common setup
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Tag tests appropriately** for flexible test execution:
|
||||||
|
```typescript
|
||||||
|
tap.tags('integration', 'database').test('...', async () => { });
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Support
|
||||||
|
|
||||||
|
tapbundle is written in TypeScript and provides full type definitions. The `Tap` class accepts a generic type for shared context:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MyTestContext {
|
||||||
|
db: DatabaseConnection;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tap = new Tap<MyTestContext>();
|
||||||
|
|
||||||
|
tap.test('should use context', async (tapTools) => {
|
||||||
|
// tapTools is typed with MyTestContext
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Legal
|
||||||
|
|
||||||
|
This project is licensed under MIT.
|
||||||
|
|
||||||
|
© 2025 Task Venture Capital GmbH. All rights reserved.
|
367
ts_tapbundle_node/readme.md
Normal file
367
ts_tapbundle_node/readme.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# @git.zone/tstest/tapbundle_node
|
||||||
|
|
||||||
|
> 🔧 Node.js-specific testing utilities for enhanced test capabilities
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# tapbundle_node is included as part of @git.zone/tstest
|
||||||
|
pnpm install --save-dev @git.zone/tstest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@git.zone/tstest/tapbundle_node` provides Node.js-specific utilities for testing. These tools are only available when running tests in Node.js runtime and provide functionality for working with environment variables, shell commands, test databases, storage systems, and HTTPS certificates.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- 🔐 **Environment Variables** - On-demand environment variable loading with qenv
|
||||||
|
- 💻 **Shell Commands** - Execute bash commands during tests
|
||||||
|
- 🔒 **HTTPS Certificates** - Generate self-signed certificates for testing
|
||||||
|
- 🗄️ **MongoDB Testing** - Create ephemeral MongoDB instances
|
||||||
|
- 📦 **S3 Storage Testing** - Create local S3-compatible storage for tests
|
||||||
|
- 📁 **Test File Management** - Download and manage test assets
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
import { tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('should use node-specific tools', async () => {
|
||||||
|
// Use Node.js-specific utilities
|
||||||
|
const result = await tapNodeTools.runCommand('echo "hello"');
|
||||||
|
console.log(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### tapNodeTools
|
||||||
|
|
||||||
|
The main singleton instance providing all Node.js-specific utilities.
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
##### `getQenv()`
|
||||||
|
|
||||||
|
Get the qenv instance for managing environment variables from `.nogit/` directory.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const qenv = await tapNodeTools.getQenv();
|
||||||
|
// qenv will load from .env files in .nogit/ directory
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `getEnvVarOnDemand(envVarName)`
|
||||||
|
|
||||||
|
Request an environment variable. If not available, qenv will prompt for it and store it securely.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.test('should get API key', async () => {
|
||||||
|
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
|
||||||
|
// If GITHUB_API_KEY is not set, qenv will prompt for it
|
||||||
|
// The value is stored in .nogit/.env for future use
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- API keys for integration tests
|
||||||
|
- Database credentials
|
||||||
|
- Service endpoints
|
||||||
|
- Any sensitive configuration needed for testing
|
||||||
|
|
||||||
|
#### Shell Commands
|
||||||
|
|
||||||
|
##### `runCommand(command)`
|
||||||
|
|
||||||
|
Execute a bash command and return the result.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.test('should execute shell commands', async () => {
|
||||||
|
const result = await tapNodeTools.runCommand('ls -la');
|
||||||
|
console.log(result.stdout);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Setup test environment
|
||||||
|
- Execute CLI tools
|
||||||
|
- File system operations
|
||||||
|
- Process management
|
||||||
|
|
||||||
|
#### HTTPS Certificates
|
||||||
|
|
||||||
|
##### `createHttpsCert(commonName?, allowSelfSigned?)`
|
||||||
|
|
||||||
|
Generate a self-signed HTTPS certificate for testing secure connections.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
tap.test('should create HTTPS server', async () => {
|
||||||
|
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
|
||||||
|
|
||||||
|
// Use with Node.js https module
|
||||||
|
const server = https.createServer({ key, cert }, (req, res) => {
|
||||||
|
res.end('Hello Secure World');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(3000);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `commonName` (optional): Certificate common name, default: 'localhost'
|
||||||
|
- `allowSelfSigned` (optional): Allow self-signed certificates by setting `NODE_TLS_REJECT_UNAUTHORIZED=0`, default: true
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `key`: PEM-encoded private key
|
||||||
|
- `cert`: PEM-encoded certificate
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Testing HTTPS servers
|
||||||
|
- Testing secure WebSocket connections
|
||||||
|
- Testing certificate validation logic
|
||||||
|
- Mocking secure external services
|
||||||
|
|
||||||
|
#### Database Testing
|
||||||
|
|
||||||
|
##### `createSmartmongo()`
|
||||||
|
|
||||||
|
Create an ephemeral MongoDB instance for testing. Automatically started and ready to use.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
|
||||||
|
tap.test('should use MongoDB', async () => {
|
||||||
|
const mongoInstance = await tapNodeTools.createSmartmongo();
|
||||||
|
|
||||||
|
// Use the MongoDB instance
|
||||||
|
const db = await mongoInstance.getDatabase('testdb');
|
||||||
|
const collection = await db.getCollection('users');
|
||||||
|
|
||||||
|
await collection.insertOne({ name: 'Alice', age: 30 });
|
||||||
|
const user = await collection.findOne({ name: 'Alice' });
|
||||||
|
|
||||||
|
expect(user.age).toEqual(30);
|
||||||
|
|
||||||
|
// Cleanup (optional - instance will be cleaned up automatically)
|
||||||
|
await mongoInstance.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Ephemeral instance (starts fresh)
|
||||||
|
- Automatic cleanup
|
||||||
|
- Full MongoDB API via [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo)
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Testing database operations
|
||||||
|
- Integration tests with MongoDB
|
||||||
|
- Testing data models
|
||||||
|
- Schema validation tests
|
||||||
|
|
||||||
|
#### Storage Testing
|
||||||
|
|
||||||
|
##### `createSmarts3()`
|
||||||
|
|
||||||
|
Create a local S3-compatible storage instance for testing object storage operations.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
|
||||||
|
tap.test('should use S3 storage', async () => {
|
||||||
|
const s3Instance = await tapNodeTools.createSmarts3();
|
||||||
|
|
||||||
|
// Use the S3 instance (MinIO-compatible API)
|
||||||
|
const bucket = await s3Instance.createBucket('test-bucket');
|
||||||
|
await bucket.putObject('file.txt', Buffer.from('Hello S3'));
|
||||||
|
const file = await bucket.getObject('file.txt');
|
||||||
|
|
||||||
|
expect(file.toString()).toEqual('Hello S3');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await s3Instance.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- Port: 3003 (default)
|
||||||
|
- Clean slate: true (starts fresh each time)
|
||||||
|
- Full S3-compatible API via [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3)
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Testing file uploads/downloads
|
||||||
|
- Testing object storage operations
|
||||||
|
- Testing backup/restore logic
|
||||||
|
- Mocking cloud storage
|
||||||
|
|
||||||
|
### TestFileProvider
|
||||||
|
|
||||||
|
Utility for downloading and managing test assets.
|
||||||
|
|
||||||
|
#### `getDockerAlpineImageAsLocalTarball()`
|
||||||
|
|
||||||
|
Download the Alpine Linux Docker image as a tarball for testing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
|
||||||
|
tap.test('should provide docker image', async () => {
|
||||||
|
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
|
||||||
|
|
||||||
|
// Use the tarball path
|
||||||
|
// Path: ./.nogit/testfiles/alpine.tar
|
||||||
|
|
||||||
|
expect(tarballPath).toMatch(/alpine\.tar$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Downloads from https://code.foss.global/testassets/docker
|
||||||
|
- Caches in `.nogit/testfiles/` directory
|
||||||
|
- Returns local file path
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Testing Docker operations
|
||||||
|
- Testing container deployment
|
||||||
|
- Testing image handling logic
|
||||||
|
|
||||||
|
### Path Utilities
|
||||||
|
|
||||||
|
The module exports useful path constants:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as paths from '@git.zone/tstest/tapbundle_node/paths';
|
||||||
|
|
||||||
|
console.log(paths.cwd); // Current working directory
|
||||||
|
console.log(paths.testFilesDir); // ./.nogit/testfiles/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Patterns and Best Practices
|
||||||
|
|
||||||
|
### Testing with External Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.describe('User Service Integration', () => {
|
||||||
|
let mongoInstance;
|
||||||
|
let db;
|
||||||
|
|
||||||
|
tap.beforeEach(async () => {
|
||||||
|
mongoInstance = await tapNodeTools.createSmartmongo();
|
||||||
|
db = await mongoInstance.getDatabase('testdb');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create user', async () => {
|
||||||
|
const users = await db.getCollection('users');
|
||||||
|
await users.insertOne({ name: 'Bob', email: 'bob@example.com' });
|
||||||
|
|
||||||
|
const user = await users.findOne({ name: 'Bob' });
|
||||||
|
expect(user.email).toEqual('bob@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async () => {
|
||||||
|
await mongoInstance.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing HTTPS Servers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
|
tap.test('should serve over HTTPS', async () => {
|
||||||
|
const { key, cert } = await tapNodeTools.createHttpsCert();
|
||||||
|
|
||||||
|
const server = https.createServer({ key, cert }, (req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('Secure Response');
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server.listen(8443, () => resolve(undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the server
|
||||||
|
const response = await fetch('https://localhost:8443');
|
||||||
|
const text = await response.text();
|
||||||
|
expect(text).toEqual('Secure Response');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Dependent Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('should authenticate with GitHub', async () => {
|
||||||
|
const githubToken = await tapNodeTools.getEnvVarOnDemand('GITHUB_TOKEN');
|
||||||
|
|
||||||
|
// Use the token for API calls
|
||||||
|
const response = await fetch('https://api.github.com/user', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${githubToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Requirements
|
||||||
|
|
||||||
|
⚠️ **Node.js Only**: All utilities in this module require Node.js runtime. They will not work in:
|
||||||
|
- Browser environments
|
||||||
|
- Deno runtime
|
||||||
|
- Bun runtime
|
||||||
|
|
||||||
|
For multi-runtime tests, use these utilities only in `.node.ts` test files.
|
||||||
|
|
||||||
|
## File Naming
|
||||||
|
|
||||||
|
Use Node.js-specific file naming when using these utilities:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/mytest.node.ts ✅ Node.js only
|
||||||
|
test/mytest.node+deno.ts ❌ Will fail in Deno
|
||||||
|
test/mytest.browser+node.ts ⚠️ Browser won't have access to these tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This module uses the following packages:
|
||||||
|
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) - Environment variable management
|
||||||
|
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) - Shell command execution
|
||||||
|
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) - Certificate generation
|
||||||
|
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) - MongoDB testing
|
||||||
|
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) - S3 storage testing
|
||||||
|
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) - File operations
|
||||||
|
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) - HTTP requests
|
||||||
|
|
||||||
|
## Legal
|
||||||
|
|
||||||
|
This project is licensed under MIT.
|
||||||
|
|
||||||
|
© 2025 Task Venture Capital GmbH. All rights reserved.
|
587
ts_tapbundle_protocol/readme.md
Normal file
587
ts_tapbundle_protocol/readme.md
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
# @git.zone/tstest/tapbundle_protocol
|
||||||
|
|
||||||
|
> 📡 Enhanced TAP Protocol V2 implementation for structured test communication
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# tapbundle_protocol is included as part of @git.zone/tstest
|
||||||
|
pnpm install --save-dev @git.zone/tstest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@git.zone/tstest/tapbundle_protocol` implements Protocol V2, an enhanced version of the Test Anything Protocol (TAP) with support for structured metadata, real-time events, error diffs, and isomorphic operation. This protocol enables rich communication between test runners and test consumers while maintaining backward compatibility with standard TAP parsers.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- 📋 **TAP v13 Compliant** - Fully compatible with standard TAP consumers
|
||||||
|
- 🎯 **Enhanced Metadata** - Timing, tags, errors, diffs, and custom data
|
||||||
|
- 🔄 **Real-time Events** - Live test execution updates
|
||||||
|
- 🔍 **Structured Errors** - JSON error blocks with stack traces and diffs
|
||||||
|
- 📸 **Snapshot Support** - Built-in snapshot testing protocol
|
||||||
|
- 🌐 **Isomorphic** - Works in Node.js, browsers, Deno, and Bun
|
||||||
|
- 🏷️ **Protocol Markers** - Structured data using Unicode delimiters
|
||||||
|
|
||||||
|
## Protocol V2 Format
|
||||||
|
|
||||||
|
### Protocol Markers
|
||||||
|
|
||||||
|
Protocol V2 uses special Unicode markers to embed structured data within TAP output:
|
||||||
|
|
||||||
|
- `⟦TSTEST:` - Start marker
|
||||||
|
- `⟧` - End marker
|
||||||
|
|
||||||
|
These markers allow structured data to coexist with standard TAP without breaking compatibility.
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
```tap
|
||||||
|
⟦TSTEST:PROTOCOL:2.0.0⟧
|
||||||
|
TAP version 13
|
||||||
|
1..3
|
||||||
|
ok 1 - should add numbers ⟦TSTEST:time:42⟧
|
||||||
|
not ok 2 - should validate input
|
||||||
|
⟦TSTEST:META:{"time":156,"file":"test.ts","line":42}⟧
|
||||||
|
⟦TSTEST:ERROR⟧
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"message": "Expected 5 to equal 6",
|
||||||
|
"diff": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
⟦TSTEST:/ERROR⟧
|
||||||
|
ok 3 - should handle edge cases # SKIP not implemented ⟦TSTEST:SKIP:not implemented⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### ProtocolEmitter
|
||||||
|
|
||||||
|
Generates Protocol V2 messages. Used by tapbundle to emit test results.
|
||||||
|
|
||||||
|
#### `emitProtocolHeader()`
|
||||||
|
|
||||||
|
Emit the protocol version header.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProtocolEmitter } from '@git.zone/tstest/tapbundle_protocol';
|
||||||
|
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
console.log(emitter.emitProtocolHeader());
|
||||||
|
// Output: ⟦TSTEST:PROTOCOL:2.0.0⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitTapVersion(version?)`
|
||||||
|
|
||||||
|
Emit TAP version line.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log(emitter.emitTapVersion(13));
|
||||||
|
// Output: TAP version 13
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitPlan(plan)`
|
||||||
|
|
||||||
|
Emit test plan.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log(emitter.emitPlan({ start: 1, end: 5 }));
|
||||||
|
// Output: 1..5
|
||||||
|
|
||||||
|
console.log(emitter.emitPlan({ start: 1, end: 0, skipAll: 'Not ready' }));
|
||||||
|
// Output: 1..0 # Skipped: Not ready
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitTest(result)`
|
||||||
|
|
||||||
|
Emit a test result with optional metadata.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lines = emitter.emitTest({
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'should work correctly',
|
||||||
|
metadata: {
|
||||||
|
time: 45,
|
||||||
|
tags: ['unit', 'fast']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.forEach(line => console.log(line));
|
||||||
|
// Output:
|
||||||
|
// ok 1 - should work correctly ⟦TSTEST:time:45⟧
|
||||||
|
// ⟦TSTEST:META:{"tags":["unit","fast"]}⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitComment(comment)`
|
||||||
|
|
||||||
|
Emit a comment line.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log(emitter.emitComment('Setup complete'));
|
||||||
|
// Output: # Setup complete
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitBailout(reason)`
|
||||||
|
|
||||||
|
Emit a bailout (abort all tests).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log(emitter.emitBailout('Database connection failed'));
|
||||||
|
// Output: Bail out! Database connection failed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitError(error)`
|
||||||
|
|
||||||
|
Emit a structured error block.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lines = emitter.emitError({
|
||||||
|
testNumber: 2,
|
||||||
|
error: {
|
||||||
|
message: 'Expected 5 to equal 6',
|
||||||
|
stack: 'Error: ...',
|
||||||
|
actual: 5,
|
||||||
|
expected: 6,
|
||||||
|
diff: '...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.forEach(line => console.log(line));
|
||||||
|
// Output:
|
||||||
|
// ⟦TSTEST:ERROR⟧
|
||||||
|
// {
|
||||||
|
// "testNumber": 2,
|
||||||
|
// "error": { ... }
|
||||||
|
// }
|
||||||
|
// ⟦TSTEST:/ERROR⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitEvent(event)`
|
||||||
|
|
||||||
|
Emit a real-time test event.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
console.log(emitter.emitEvent({
|
||||||
|
eventType: 'test:started',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: {
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'should work'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
// Output: ⟦TSTEST:EVENT:{"eventType":"test:started",...}⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `emitSnapshot(snapshot)`
|
||||||
|
|
||||||
|
Emit snapshot data.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lines = emitter.emitSnapshot({
|
||||||
|
name: 'user-data',
|
||||||
|
content: { name: 'Alice', age: 30 },
|
||||||
|
format: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.forEach(line => console.log(line));
|
||||||
|
// Output:
|
||||||
|
// ⟦TSTEST:SNAPSHOT:user-data⟧
|
||||||
|
// {
|
||||||
|
// "name": "Alice",
|
||||||
|
// "age": 30
|
||||||
|
// }
|
||||||
|
// ⟦TSTEST:/SNAPSHOT⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProtocolParser
|
||||||
|
|
||||||
|
Parses Protocol V2 messages. Used by tstest to consume test results.
|
||||||
|
|
||||||
|
#### `parseLine(line)`
|
||||||
|
|
||||||
|
Parse a single line and return protocol messages.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProtocolParser } from '@git.zone/tstest/tapbundle_protocol';
|
||||||
|
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const messages = parser.parseLine('ok 1 - test passed ⟦TSTEST:time:42⟧');
|
||||||
|
console.log(messages);
|
||||||
|
// Output:
|
||||||
|
// [{
|
||||||
|
// type: 'test',
|
||||||
|
// content: {
|
||||||
|
// ok: true,
|
||||||
|
// testNumber: 1,
|
||||||
|
// description: 'test passed',
|
||||||
|
// metadata: { time: 42 }
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Message Types
|
||||||
|
|
||||||
|
The parser returns different message types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IProtocolMessage {
|
||||||
|
type: 'test' | 'plan' | 'comment' | 'version' | 'bailout' | 'protocol' | 'snapshot' | 'error' | 'event';
|
||||||
|
content: any;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Test result
|
||||||
|
{
|
||||||
|
type: 'test',
|
||||||
|
content: {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'test name',
|
||||||
|
metadata: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan
|
||||||
|
{
|
||||||
|
type: 'plan',
|
||||||
|
content: {
|
||||||
|
start: 1,
|
||||||
|
end: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
content: {
|
||||||
|
eventType: 'test:started',
|
||||||
|
timestamp: 1234567890,
|
||||||
|
data: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
content: {
|
||||||
|
testNumber: 2,
|
||||||
|
error: {
|
||||||
|
message: '...',
|
||||||
|
stack: '...',
|
||||||
|
diff: '...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getProtocolVersion()`
|
||||||
|
|
||||||
|
Get the detected protocol version.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const version = parser.getProtocolVersion();
|
||||||
|
console.log(version); // "2.0.0" or null
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Types
|
||||||
|
|
||||||
|
### ITestResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ITestResult {
|
||||||
|
ok: boolean;
|
||||||
|
testNumber: number;
|
||||||
|
description: string;
|
||||||
|
directive?: {
|
||||||
|
type: 'skip' | 'todo';
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
metadata?: ITestMetadata;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ITestMetadata
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ITestMetadata {
|
||||||
|
// Timing
|
||||||
|
time?: number; // Test duration in milliseconds
|
||||||
|
startTime?: number; // Unix timestamp
|
||||||
|
endTime?: number; // Unix timestamp
|
||||||
|
|
||||||
|
// Status
|
||||||
|
skip?: string; // Skip reason
|
||||||
|
todo?: string; // Todo reason
|
||||||
|
retry?: number; // Current retry attempt
|
||||||
|
maxRetries?: number; // Max retries allowed
|
||||||
|
|
||||||
|
// Error details
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
diff?: string;
|
||||||
|
actual?: any;
|
||||||
|
expected?: any;
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test context
|
||||||
|
file?: string; // Source file path
|
||||||
|
line?: number; // Line number
|
||||||
|
column?: number; // Column number
|
||||||
|
|
||||||
|
// Custom data
|
||||||
|
tags?: string[]; // Test tags
|
||||||
|
custom?: Record<string, any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ITestEvent
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ITestEvent {
|
||||||
|
eventType: EventType;
|
||||||
|
timestamp: number;
|
||||||
|
data: {
|
||||||
|
testNumber?: number;
|
||||||
|
description?: string;
|
||||||
|
suiteName?: string;
|
||||||
|
hookName?: string;
|
||||||
|
progress?: number; // 0-100
|
||||||
|
duration?: number;
|
||||||
|
error?: IEnhancedError;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventType =
|
||||||
|
| 'test:queued'
|
||||||
|
| 'test:started'
|
||||||
|
| 'test:progress'
|
||||||
|
| 'test:completed'
|
||||||
|
| 'suite:started'
|
||||||
|
| 'suite:completed'
|
||||||
|
| 'hook:started'
|
||||||
|
| 'hook:completed'
|
||||||
|
| 'assertion:failed';
|
||||||
|
```
|
||||||
|
|
||||||
|
### IEnhancedError
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IEnhancedError {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
diff?: IDiffResult;
|
||||||
|
actual?: any;
|
||||||
|
expected?: any;
|
||||||
|
code?: string;
|
||||||
|
type?: 'assertion' | 'timeout' | 'uncaught' | 'syntax' | 'runtime';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDiffResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IDiffResult {
|
||||||
|
type: 'string' | 'object' | 'array' | 'primitive';
|
||||||
|
changes: IDiffChange[];
|
||||||
|
context?: number; // Lines of context
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDiffChange {
|
||||||
|
type: 'add' | 'remove' | 'modify';
|
||||||
|
path?: string[]; // For object/array diffs
|
||||||
|
oldValue?: any;
|
||||||
|
newValue?: any;
|
||||||
|
line?: number; // For string diffs
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol Constants
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PROTOCOL_MARKERS, PROTOCOL_VERSION } from '@git.zone/tstest/tapbundle_protocol';
|
||||||
|
|
||||||
|
console.log(PROTOCOL_VERSION); // "2.0.0"
|
||||||
|
console.log(PROTOCOL_MARKERS.START); // "⟦TSTEST:"
|
||||||
|
console.log(PROTOCOL_MARKERS.END); // "⟧"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Markers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const PROTOCOL_MARKERS = {
|
||||||
|
START: '⟦TSTEST:',
|
||||||
|
END: '⟧',
|
||||||
|
META_PREFIX: 'META:',
|
||||||
|
ERROR_PREFIX: 'ERROR',
|
||||||
|
ERROR_END: '/ERROR',
|
||||||
|
SNAPSHOT_PREFIX: 'SNAPSHOT:',
|
||||||
|
SNAPSHOT_END: '/SNAPSHOT',
|
||||||
|
PROTOCOL_PREFIX: 'PROTOCOL:',
|
||||||
|
SKIP_PREFIX: 'SKIP:',
|
||||||
|
TODO_PREFIX: 'TODO:',
|
||||||
|
EVENT_PREFIX: 'EVENT:',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
### Creating a Custom Test Runner
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProtocolEmitter } from '@git.zone/tstest/tapbundle_protocol';
|
||||||
|
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
// Emit protocol header
|
||||||
|
console.log(emitter.emitProtocolHeader());
|
||||||
|
console.log(emitter.emitTapVersion(13));
|
||||||
|
|
||||||
|
// Emit plan
|
||||||
|
console.log(emitter.emitPlan({ start: 1, end: 2 }));
|
||||||
|
|
||||||
|
// Run test 1
|
||||||
|
emitter.emitEvent({
|
||||||
|
eventType: 'test:started',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: { testNumber: 1 }
|
||||||
|
}).split('\n').forEach(line => console.log(line));
|
||||||
|
|
||||||
|
const result1 = emitter.emitTest({
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'first test',
|
||||||
|
metadata: { time: 45 }
|
||||||
|
});
|
||||||
|
result1.forEach(line => console.log(line));
|
||||||
|
|
||||||
|
// Run test 2
|
||||||
|
const result2 = emitter.emitTest({
|
||||||
|
ok: false,
|
||||||
|
testNumber: 2,
|
||||||
|
description: 'second test',
|
||||||
|
metadata: {
|
||||||
|
time: 120,
|
||||||
|
error: {
|
||||||
|
message: 'Assertion failed',
|
||||||
|
actual: 5,
|
||||||
|
expected: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result2.forEach(line => console.log(line));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsing Test Output
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProtocolParser } from '@git.zone/tstest/tapbundle_protocol';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
const messages = parser.parseLine(line);
|
||||||
|
|
||||||
|
messages.forEach(message => {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'test':
|
||||||
|
console.log(`Test ${message.content.testNumber}: ${message.content.ok ? 'PASS' : 'FAIL'}`);
|
||||||
|
break;
|
||||||
|
case 'event':
|
||||||
|
console.log(`Event: ${message.content.eventType}`);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(`Error: ${message.content.error.message}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Test Dashboards
|
||||||
|
|
||||||
|
Real-time events enable building live test dashboards:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
parser.parseLine(line).forEach(message => {
|
||||||
|
if (message.type === 'event') {
|
||||||
|
const event = message.content;
|
||||||
|
|
||||||
|
switch (event.eventType) {
|
||||||
|
case 'test:started':
|
||||||
|
updateUI({ status: 'running', test: event.data.description });
|
||||||
|
break;
|
||||||
|
case 'test:completed':
|
||||||
|
updateUI({ status: 'done', duration: event.data.duration });
|
||||||
|
break;
|
||||||
|
case 'suite:started':
|
||||||
|
createSuiteCard(event.data.suiteName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
Protocol V2 is fully backward compatible with standard TAP parsers:
|
||||||
|
|
||||||
|
- Protocol markers use Unicode characters that TAP parsers ignore
|
||||||
|
- Standard TAP output (ok/not ok, plan, comments) works everywhere
|
||||||
|
- Enhanced features gracefully degrade in standard TAP consumers
|
||||||
|
|
||||||
|
**Standard TAP View:**
|
||||||
|
```tap
|
||||||
|
TAP version 13
|
||||||
|
1..3
|
||||||
|
ok 1 - should add numbers
|
||||||
|
not ok 2 - should validate input
|
||||||
|
ok 3 - should handle edge cases # SKIP not implemented
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protocol V2 View (same output):**
|
||||||
|
```tap
|
||||||
|
⟦TSTEST:PROTOCOL:2.0.0⟧
|
||||||
|
TAP version 13
|
||||||
|
1..3
|
||||||
|
ok 1 - should add numbers ⟦TSTEST:time:42⟧
|
||||||
|
not ok 2 - should validate input
|
||||||
|
⟦TSTEST:META:{"time":156}⟧
|
||||||
|
ok 3 - should handle edge cases # SKIP not implemented ⟦TSTEST:SKIP:not implemented⟧
|
||||||
|
```
|
||||||
|
|
||||||
|
## Isomorphic Design
|
||||||
|
|
||||||
|
This module works in all JavaScript environments:
|
||||||
|
|
||||||
|
- ✅ Node.js
|
||||||
|
- ✅ Browsers (via tapbundle)
|
||||||
|
- ✅ Deno
|
||||||
|
- ✅ Bun
|
||||||
|
- ✅ Web Workers
|
||||||
|
- ✅ Service Workers
|
||||||
|
|
||||||
|
No runtime-specific APIs are used, making it truly portable.
|
||||||
|
|
||||||
|
## Legal
|
||||||
|
|
||||||
|
This project is licensed under MIT.
|
||||||
|
|
||||||
|
© 2025 Task Venture Capital GmbH. All rights reserved.
|
Reference in New Issue
Block a user