Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9016206ce | |||
| 8edbbd4850 | |||
| 97c91fc010 | |||
| ca08bb2e3c | |||
| 8fd114334f | |||
| c630a171b5 | |||
| 1a4eb5b6d9 | |||
| 41d7c1ce49 | |||
| 3ab5550cb8 | |||
| ee7b387534 | |||
| 7e67b64a6e | |||
| 1ce730d4f2 | |||
| 9357d6e7ef | |||
| 973ce771d2 | |||
| 8441881d92 | |||
| 16ca3b6374 | |||
| b94089652e | |||
| ef6f21fc9c | |||
| 592a4f33c0 | |||
| 1ea3b37d18 | |||
| 062c6e384b | |||
| b1f2eceb75 | |||
| de53b3f00c | |||
| b3f8a28766 | |||
| 86db2491a3 | |||
| b9fd8c7b02 | |||
| d6842326ad | |||
| 175b4463fa | |||
| 6a8417e400 | |||
| 4714d5e8ad | |||
| ff6aae7159 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -22,5 +22,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"deno.enable": false
|
||||
}
|
||||
|
||||
119
changelog.md
119
changelog.md
@@ -1,5 +1,124 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-11-21 - 3.1.1 - fix(tapbundle)
|
||||
Pass TapTools to suite lifecycle hooks (beforeAll/afterAll) and update @push.rocks/smarts3 to ^3.0.0
|
||||
|
||||
- Replace usage of a Deferred promise with a TapTools instance when invoking suite.beforeAll and suite.afterAll
|
||||
- Add import for TapTools in ts_tapbundle/tapbundle.classes.tap.ts
|
||||
- Bump dependency @push.rocks/smarts3 from ^2.2.7 to ^3.0.0 in package.json
|
||||
|
||||
## 2025-11-20 - 3.1.0 - feat(tapbundle)
|
||||
Add global postTask (teardown) and suite lifecycle hooks (beforeAll/afterAll) to tapbundle
|
||||
|
||||
- Introduce PostTask class (ts_tapbundle/tapbundle.classes.posttask.ts) and tap.postTask() API for global teardown.
|
||||
- Integrate postTask execution into Tap.start() so postTasks run after all tests and before the global afterAll hook.
|
||||
- Add suite-level beforeAll and afterAll support and ensure afterAll runs after child suites and their tests (changes in ts_tapbundle/tapbundle.classes.tap.ts).
|
||||
- Add lifecycle tests (test/tapbundle/test.new-lifecycle.ts) verifying execution order, including parallel tests.
|
||||
- Update documentation (readme.hints.md) describing Phase 1 API improvements and usage notes.
|
||||
- This is additive and backward-compatible (no breaking changes).
|
||||
|
||||
## 2025-11-20 - 3.0.1 - fix(@push.rocks/smarts3)
|
||||
Bump @push.rocks/smarts3 dependency to ^2.2.7
|
||||
|
||||
- Update package.json: @push.rocks/smarts3 upgraded from ^2.2.6 to ^2.2.7
|
||||
|
||||
## 2025-11-19 - 3.0.0 - BREAKING CHANGE(tapbundle_serverside)
|
||||
Rename Node-specific tapbundle module to tapbundle_serverside and migrate server-side utilities
|
||||
|
||||
- Change public export in package.json from ./tapbundle_node to ./tapbundle_serverside — consumers must update imports to @git.zone/tstest/tapbundle_serverside
|
||||
- Move and re-create Node-only implementation files under ts_tapbundle_serverside (plugins, paths, classes.tapnodetools, classes.testfileprovider, index, tspublish.json) and remove legacy ts_tapbundle_node sources
|
||||
- Update internal imports and tests to reference the new tapbundle_serverside path (e.g. test/tapbundle/test.node.ts updated)
|
||||
- Update documentation (readme.md and readme.hints.md) to describe the new tapbundle_serverside export and its server-side utilities
|
||||
- Ensure build outputs and publish metadata reflect the new module directory (tspublish.json order preserved)
|
||||
|
||||
## 2025-11-19 - 2.8.3 - fix(dependencies)
|
||||
Update dependency versions
|
||||
|
||||
- Bump devDependency @git.zone/tsbuild to ^3.1.0
|
||||
- Upgrade @git.zone/tsrun to ^2.0.0 (major)
|
||||
- Upgrade @push.rocks/smartenv to ^6.0.0 (major)
|
||||
- Upgrade @push.rocks/smartrequest to ^5.0.1 (major/feature in dependency)
|
||||
- Patch updates: @api.global/typedserver → ^3.0.80, @git.zone/tsbundle → ^2.5.2, @push.rocks/smartmongo → ^2.0.14
|
||||
|
||||
## 2025-11-17 - 2.8.2 - fix(logging)
|
||||
Include runtime identifier in per-test logfile name and sanitize runtime string
|
||||
|
||||
- Append a sanitized runtime identifier to the per-test log filename (format: <safeFilename>__<safeRuntime>.log) so runs for different runtimes don't clash
|
||||
- Sanitize runtime names by lowercasing and removing non-alphanumeric characters to produce filesystem-safe filenames
|
||||
|
||||
## 2025-11-17 - 2.8.1 - fix(config)
|
||||
Remove Bun config file and set deno.json useDefineForClassFields to false for compatibility
|
||||
|
||||
- Removed bunfig.toml (Bun-specific TypeScript decorator configuration) — stops shipping a project-local Bun transpiler config.
|
||||
- Updated deno.json: set compilerOptions.useDefineForClassFields = false to keep legacy class field semantics and avoid runtime/emit incompatibilities in Deno.
|
||||
|
||||
## 2025-11-17 - 2.8.0 - feat(runtime-adapters)
|
||||
Enable TypeScript decorator support for Deno and Bun runtimes and add decorator tests
|
||||
|
||||
- Add bunfig.toml to enable experimentalDecorators for Bun runtime
|
||||
- Add deno.json to enable experimentalDecorators and set target/lib for Deno
|
||||
- Update Bun runtime adapter to note bunfig.toml discovery so Bun runs with decorator support
|
||||
- Update Deno runtime adapter to auto-detect deno.json / deno.jsonc and pass configPath in default options
|
||||
- Add integration tests for decorators (test/decorator.all.ts) to verify decorator support across runtimes
|
||||
|
||||
## 2025-10-26 - 2.7.0 - feat(tapbundle_protocol)
|
||||
Add package export for tapbundle_protocol to expose protocol utilities
|
||||
|
||||
- Add './tapbundle_protocol' export in package.json pointing to './dist_ts_tapbundle_protocol/index.js'.
|
||||
- Allows consumers to import protocol utilities (ProtocolEmitter, ProtocolParser, types) via '@git.zone/tstest/tapbundle_protocol'.
|
||||
- Non-breaking: only extends package exports surface.
|
||||
|
||||
## 2025-10-17 - 2.6.2 - fix(@push.rocks/smartrequest)
|
||||
Bump @push.rocks/smartrequest from ^4.3.1 to ^4.3.2
|
||||
|
||||
- Update dependency @push.rocks/smartrequest from ^4.3.1 to ^4.3.2
|
||||
|
||||
## 2025-10-17 - 2.6.1 - fix(runtime-adapters)
|
||||
Silence shell version checks for Bun and Deno; add local Claude settings
|
||||
|
||||
- Replace smartshell.exec with execSilent in ts/tstest.classes.runtime.bun.ts to suppress output when checking Bun availability
|
||||
- Replace smartshell.exec with execSilent in ts/tstest.classes.runtime.deno.ts to suppress output when checking Deno availability
|
||||
- Add .claude/settings.local.json to record local Claude agent permissions/config used for development
|
||||
|
||||
## 2025-10-17 - 2.6.0 - feat(runtime-adapters)
|
||||
Add runtime environment availability check and logger output; normalize runtime version strings
|
||||
|
||||
- Introduce checkEnvironment() in TsTest and invoke it at the start of run() to detect available runtimes before executing tests.
|
||||
- Add environmentCheck(availability) to TsTestLogger to print a human-friendly environment summary (with JSON and quiet-mode handling).
|
||||
- Normalize reported runtime version strings from adapters: prefix Deno and Bun versions with 'v' and simplify Chromium version text.
|
||||
- Display runtime availability information to the user before moving previous logs or running tests.
|
||||
- Includes addition of local .claude/settings.local.json (local dev/tooling settings).
|
||||
|
||||
## 2025-10-17 - 2.5.2 - fix(runtime.node)
|
||||
Improve Node runtime adapter to use tsrun.spawnPath, strengthen tsrun detection, and improve process lifecycle and loader handling; update tsrun dependency.
|
||||
|
||||
- Use tsrun.spawnPath to spawn Node test processes and pass structured spawn options (cwd, env, args, stdio).
|
||||
- Detect tsrun availability via plugins.tsrun and require spawnPath; provide a clearer error message when tsrun is missing or outdated.
|
||||
- Pass --web via spawn args and set TSTEST_FILTER_TAGS on the spawned process env instead of mutating the parent process.env.
|
||||
- When a 00init.ts exists, create a temporary loader that imports both 00init.ts and the test file, run the loader via tsrun.spawnPath, and clean up the loader after execution.
|
||||
- Use tsrunProcess.terminate()/kill for timeouts to ensure proper process termination and improve cleanup handling.
|
||||
- Export tsrun from ts/tstest.plugins.ts so runtime code can access tsrun APIs via the plugins object.
|
||||
- Bump dependency @git.zone/tsrun from ^1.3.4 to ^1.6.2 in package.json.
|
||||
|
||||
## 2025-10-16 - 2.5.1 - fix(deps)
|
||||
Bump dependencies and add local tooling settings
|
||||
|
||||
- Bumped @api.global/typedserver from ^3.0.78 to ^3.0.79
|
||||
- Bumped @git.zone/tsrun from ^1.3.3 to ^1.3.4
|
||||
- Bumped @push.rocks/smartjson from ^5.0.20 to ^5.2.0
|
||||
- Bumped @push.rocks/smartlog from ^3.1.9 to ^3.1.10
|
||||
- Add local settings configuration file for developer tooling
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
13
deno.json
Normal file
13
deno.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM"
|
||||
],
|
||||
"target": "ES2022"
|
||||
},
|
||||
"nodeModulesDir": true,
|
||||
"version": "3.1.1"
|
||||
}
|
||||
25
package.json
25
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@git.zone/tstest",
|
||||
"version": "2.4.3",
|
||||
"version": "3.1.1",
|
||||
"private": false,
|
||||
"description": "a test utility to run tests that match test/**/*.ts",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./tapbundle": "./dist_ts_tapbundle/index.js",
|
||||
"./tapbundle_node": "./dist_ts_tapbundle_node/index.js"
|
||||
"./tapbundle_serverside": "./dist_ts_tapbundle_serverside/index.js",
|
||||
"./tapbundle_protocol": "./dist_ts_tapbundle_protocol/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
@@ -24,30 +25,30 @@
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsbuild": "^3.1.0",
|
||||
"@types/node": "^22.15.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedserver": "^3.0.78",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@api.global/typedserver": "^3.0.80",
|
||||
"@git.zone/tsbundle": "^2.5.2",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@push.rocks/consolecolor": "^2.0.3",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartbrowser": "^2.0.8",
|
||||
"@push.rocks/smartchok": "^1.1.1",
|
||||
"@push.rocks/smartcrypto": "^2.0.4",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartenv": "^5.0.13",
|
||||
"@push.rocks/smartenv": "^6.0.0",
|
||||
"@push.rocks/smartexpect": "^2.5.0",
|
||||
"@push.rocks/smartfile": "^11.2.7",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.9",
|
||||
"@push.rocks/smartmongo": "^2.0.12",
|
||||
"@push.rocks/smartjson": "^5.2.0",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartmongo": "^2.0.14",
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^4.3.1",
|
||||
"@push.rocks/smarts3": "^2.2.6",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smarts3": "^3.0.0",
|
||||
"@push.rocks/smartshell": "^3.3.0",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
||||
4781
pnpm-lock.yaml
generated
4781
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
131
readme.hints.md
131
readme.hints.md
@@ -6,7 +6,7 @@ This project integrates tstest with tapbundle through a modular architecture:
|
||||
|
||||
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
|
||||
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
|
||||
3. **tapbundle_node** (`/ts_tapbundle_node/`) - Node.js-specific testing utilities
|
||||
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
|
||||
|
||||
## How Components Work Together
|
||||
|
||||
@@ -31,7 +31,7 @@ This project integrates tstest with tapbundle through a modular architecture:
|
||||
|
||||
1. **Import Structure**
|
||||
- Test files import from local tapbundle: `import { tap, expect } from '../../ts_tapbundle/index.js'`
|
||||
- Node-specific tests also import from tapbundle_node: `import { tapNodeTools } from '../../ts_tapbundle_node/index.js'`
|
||||
- Server-side tests also import from tapbundle_serverside for Node.js-only utilities: `import { tapNodeTools } from '../../ts_tapbundle_serverside/index.js'`
|
||||
|
||||
2. **WebHelpers**
|
||||
- Browser tests can use webhelpers for DOM manipulation
|
||||
@@ -41,7 +41,7 @@ This project integrates tstest with tapbundle through a modular architecture:
|
||||
|
||||
3. **Build System**
|
||||
- Uses `tsbuild tsfolders` to compile TypeScript (invoked by `pnpm build`)
|
||||
- Maintains separate output directories: `/dist_ts/`, `/dist_ts_tapbundle/`, `/dist_ts_tapbundle_node/`, `/dist_ts_tapbundle_protocol/`
|
||||
- Maintains separate output directories: `/dist_ts/`, `/dist_ts_tapbundle/`, `/dist_ts_tapbundle_serverside/`, `/dist_ts_tapbundle_protocol/`
|
||||
- Compilation order is resolved automatically based on dependencies in tspublish.json files
|
||||
- Protocol imports use compiled dist directories:
|
||||
```typescript
|
||||
@@ -244,6 +244,131 @@ tstest test/specific.ts -w
|
||||
- Ignores changes matching the ignore patterns
|
||||
- Shows "Waiting for file changes..." between runs
|
||||
|
||||
## Phase 1 API Improvements (v3.1.0)
|
||||
|
||||
### New Features Implemented
|
||||
|
||||
#### 1. tap.postTask() - Global Teardown (COMPLETED)
|
||||
|
||||
Added symmetric teardown method to complement `tap.preTask()`:
|
||||
|
||||
**Implementation:**
|
||||
- Created `PostTask` class in `ts_tapbundle/tapbundle.classes.posttask.ts`
|
||||
- Mirrors PreTask structure with description and function
|
||||
- Integrated into Tap class execution flow
|
||||
- Runs after all tests complete but before global `afterAll` hook
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
tap.postTask('cleanup database', async () => {
|
||||
await cleanupDatabase();
|
||||
});
|
||||
```
|
||||
|
||||
**Execution Order:**
|
||||
1. preTask hooks
|
||||
2. Global beforeAll
|
||||
3. Tests (with suite hooks)
|
||||
4. **postTask hooks** ← NEW
|
||||
5. Global afterAll
|
||||
|
||||
#### 2. Suite-Level beforeAll/afterAll (COMPLETED)
|
||||
|
||||
Added once-per-suite lifecycle hooks:
|
||||
|
||||
**Implementation:**
|
||||
- Extended `ITestSuite` interface with `beforeAll` and `afterAll` properties
|
||||
- Added `tap.beforeAll()` and `tap.afterAll()` methods
|
||||
- Integrated into `_runSuite()` execution flow
|
||||
- Properly handles nested suites
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
tap.describe('Database Tests', () => {
|
||||
tap.beforeAll(async () => {
|
||||
await initializeDatabaseConnection(); // Runs once
|
||||
});
|
||||
|
||||
tap.test('test 1', async () => {});
|
||||
tap.test('test 2', async () => {});
|
||||
|
||||
tap.afterAll(async () => {
|
||||
await closeDatabaseConnection(); // Runs once
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Execution Order per Suite:**
|
||||
1. Suite beforeAll ← NEW
|
||||
2. Suite beforeEach
|
||||
3. Test
|
||||
4. Suite afterEach
|
||||
5. (Repeat 2-4 for each test)
|
||||
6. Child suites (recursive)
|
||||
7. Suite afterAll ← NEW
|
||||
|
||||
#### 3. tap.parallel() Fluent Entry Point (COMPLETED)
|
||||
|
||||
Added fluent API for parallel test creation:
|
||||
|
||||
**Implementation:**
|
||||
- Updated `TestBuilder` class with `_parallel` flag
|
||||
- Builder constructor accepts optional parallel parameter
|
||||
- Added `tap.parallel()` method returning configured builder
|
||||
- Fixed `testParallel()` to return TapTest<T> (was void)
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// Simple parallel test
|
||||
tap.parallel().test('fetch data', async () => {});
|
||||
|
||||
// With full configuration
|
||||
tap
|
||||
.parallel()
|
||||
.tags('api', 'integration')
|
||||
.retry(2)
|
||||
.timeout(5000)
|
||||
.test('configured parallel test', async () => {});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Consistent with other fluent builders (tags, priority, etc.)
|
||||
- More discoverable than separate `testParallel()` method
|
||||
- Allows chaining parallel with other configurations
|
||||
- `testParallel()` kept for backward compatibility
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
**tapbundle/readme.md:**
|
||||
- Added suite-level beforeAll/afterAll documentation
|
||||
- Documented postTask with execution order notes
|
||||
- Added parallel() fluent API examples
|
||||
- Expanded TapTools documentation with all methods
|
||||
- Added "Additional Tap Methods" section for fail(), getSettings(), etc.
|
||||
- Documented all previously undocumented methods
|
||||
|
||||
### Tests
|
||||
|
||||
**test/tapbundle/test.new-lifecycle.ts:**
|
||||
- Tests postTask execution order
|
||||
- Verifies suite-level beforeAll/afterAll
|
||||
- Tests nested suite lifecycle
|
||||
- Validates parallel() fluent API
|
||||
- Confirms all execution order requirements
|
||||
|
||||
**Test Results:** All 9 tests passing ✅
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
None - all changes are additive and backward compatible.
|
||||
|
||||
### Migration Guide
|
||||
|
||||
No migration needed. New features are opt-in:
|
||||
- Continue using existing patterns
|
||||
- Adopt new features incrementally
|
||||
- `testParallel()` still works (recommended: switch to `parallel().test()`)
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
### tap.skip.test(), tap.todo(), and tap.only.test() (Fixed)
|
||||
|
||||
167
readme.md
167
readme.md
@@ -61,15 +61,29 @@ Name your test files with runtime specifiers to control where they run:
|
||||
| `*.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.node+deno+bun.ts - runs in Node.js, Deno, and Bun
|
||||
// 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 () => {
|
||||
@@ -304,7 +318,155 @@ tstest provides multiple exports for different use cases:
|
||||
|
||||
- `@git.zone/tstest` - Main CLI and test runner functionality
|
||||
- `@git.zone/tstest/tapbundle` - Browser-compatible test framework
|
||||
- `@git.zone/tstest/tapbundle_node` - Node.js-specific test utilities
|
||||
- `@git.zone/tstest/tapbundle_serverside` - Server-side testing utilities for Node.js-only tests (*.node.ts files)
|
||||
- Execute shell commands during tests
|
||||
- Manage environment variables on-demand with secure storage
|
||||
- Generate self-signed HTTPS certificates for testing secure connections
|
||||
- Create ephemeral MongoDB instances for database testing
|
||||
- Create local S3-compatible storage for object storage testing
|
||||
- Download and manage test assets (e.g., Docker images)
|
||||
- `@git.zone/tstest/tapbundle_protocol` - Protocol V2 emitter and parser for TAP extensions
|
||||
|
||||
### When to Use tapbundle_serverside
|
||||
|
||||
Use `@git.zone/tstest/tapbundle_serverside` when your tests:
|
||||
|
||||
- Run exclusively on Node.js server-side (*.node.ts test files)
|
||||
- Need to execute shell commands or interact with the file system
|
||||
- Require environment variable management with secure on-demand prompts
|
||||
- Test HTTPS servers and need self-signed certificates
|
||||
- Interact with databases (MongoDB) and need ephemeral test instances
|
||||
- Work with object storage (S3-compatible) and need local testing
|
||||
- Require test assets like Docker images or other downloadable files
|
||||
|
||||
**Important:** tapbundle_serverside utilities are NOT available in:
|
||||
- Browser environments
|
||||
- Deno runtime
|
||||
- Bun runtime
|
||||
|
||||
For cross-runtime tests, only import tapbundle_serverside in `.node.ts` files where you need server-side specific functionality.
|
||||
|
||||
## tapbundle Protocol V2
|
||||
|
||||
tstest includes an enhanced TAP protocol (Protocol V2) that extends standard TAP 13 with additional metadata while maintaining backwards compatibility.
|
||||
|
||||
### Overview
|
||||
|
||||
Protocol V2 adds structured metadata to TAP output using Unicode markers (`⟦TSTEST:...⟧`) that standard TAP parsers safely ignore. This allows for:
|
||||
|
||||
- **Timing information** - Test execution duration in milliseconds
|
||||
- **Structured errors** - Stack traces, diffs, and detailed error data
|
||||
- **Test events** - Real-time progress and lifecycle events
|
||||
- **Snapshots** - Snapshot testing data exchange
|
||||
- **Custom metadata** - Tags, retry counts, file locations
|
||||
|
||||
### Using the Protocol
|
||||
|
||||
```typescript
|
||||
import {
|
||||
ProtocolEmitter,
|
||||
ProtocolParser,
|
||||
PROTOCOL_MARKERS,
|
||||
PROTOCOL_VERSION
|
||||
} from '@git.zone/tstest/tapbundle_protocol';
|
||||
|
||||
// Create an emitter
|
||||
const emitter = new ProtocolEmitter();
|
||||
|
||||
// Emit protocol header
|
||||
console.log(emitter.emitProtocolHeader());
|
||||
// Output: ⟦TSTEST:PROTOCOL:2.0.0⟧
|
||||
|
||||
// Emit TAP version
|
||||
console.log(emitter.emitTapVersion(13));
|
||||
// Output: TAP version 13
|
||||
|
||||
// Emit a test result with metadata
|
||||
const testResult = {
|
||||
ok: true,
|
||||
testNumber: 1,
|
||||
description: 'user authentication works',
|
||||
metadata: {
|
||||
time: 123,
|
||||
tags: ['auth', 'unit']
|
||||
}
|
||||
};
|
||||
console.log(emitter.emitTest(testResult).join('\n'));
|
||||
// Output: ok 1 - user authentication works ⟦TSTEST:time:123⟧
|
||||
// ⟦TSTEST:META:{"tags":["auth","unit"]}⟧
|
||||
```
|
||||
|
||||
### Protocol Markers
|
||||
|
||||
```typescript
|
||||
PROTOCOL_MARKERS = {
|
||||
START: '⟦TSTEST:',
|
||||
END: '⟧',
|
||||
META_PREFIX: 'META:',
|
||||
ERROR_PREFIX: 'ERROR',
|
||||
SNAPSHOT_PREFIX: 'SNAPSHOT:',
|
||||
SKIP_PREFIX: 'SKIP:',
|
||||
TODO_PREFIX: 'TODO:',
|
||||
EVENT_PREFIX: 'EVENT:'
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
#### Creating Custom Test Runners
|
||||
|
||||
```typescript
|
||||
import { ProtocolEmitter } from '@git.zone/tstest/tapbundle_protocol';
|
||||
|
||||
const emitter = new ProtocolEmitter();
|
||||
|
||||
// Emit header and version
|
||||
console.log(emitter.emitProtocolHeader());
|
||||
console.log(emitter.emitTapVersion(13));
|
||||
console.log(emitter.emitPlan({ start: 1, end: 2 }));
|
||||
|
||||
// Run your tests and emit results
|
||||
const startTime = Date.now();
|
||||
// ... run test ...
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(emitter.emitTest({
|
||||
ok: true,
|
||||
testNumber: 1,
|
||||
description: 'my custom test',
|
||||
metadata: { time: duration }
|
||||
}).join('\n'));
|
||||
```
|
||||
|
||||
#### Parsing tapbundle Output
|
||||
|
||||
```typescript
|
||||
import { ProtocolParser } from '@git.zone/tstest/tapbundle_protocol';
|
||||
|
||||
const parser = new ProtocolParser();
|
||||
|
||||
// Parse TAP output line by line
|
||||
parser.parseLine('⟦TSTEST:PROTOCOL:2.0.0⟧');
|
||||
parser.parseLine('TAP version 13');
|
||||
parser.parseLine('1..1');
|
||||
parser.parseLine('ok 1 - test name ⟦TSTEST:time:123⟧');
|
||||
|
||||
// Get parsed results
|
||||
const results = parser.getResults();
|
||||
console.log(results);
|
||||
```
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
Protocol V2 is fully backwards compatible with standard TAP 13. The Unicode markers are treated as comments by standard TAP parsers, so Protocol V2 output can be consumed by any TAP-compliant tool:
|
||||
|
||||
```
|
||||
⟦TSTEST:PROTOCOL:2.0.0⟧ ← Ignored by standard TAP parsers
|
||||
TAP version 13 ← Standard TAP
|
||||
1..2 ← Standard TAP
|
||||
ok 1 - test ⟦TSTEST:time:45⟧ ← TAP parsers see: "ok 1 - test"
|
||||
ok 2 - another test ← Standard TAP
|
||||
```
|
||||
|
||||
## tapbundle Test Framework
|
||||
|
||||
@@ -915,6 +1077,7 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
||||
### 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
|
||||
|
||||
91
test/decorator.all.ts
Normal file
91
test/decorator.all.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { tap, expect } from '../ts_tapbundle/index.js';
|
||||
|
||||
/**
|
||||
* Simple class decorator for testing decorator support across runtimes
|
||||
*/
|
||||
function testDecorator(target: any) {
|
||||
target.decoratorApplied = true;
|
||||
target.decoratorData = 'Decorator was applied successfully';
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method decorator for testing
|
||||
*/
|
||||
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value;
|
||||
descriptor.value = function (...args: any[]) {
|
||||
const result = originalMethod.apply(this, args);
|
||||
return `[logged] ${result}`;
|
||||
};
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter decorator for testing
|
||||
*/
|
||||
function validateParam(target: any, propertyKey: string, parameterIndex: number) {
|
||||
// Mark that parameter validation decorator was applied
|
||||
if (!target.decoratedParams) {
|
||||
target.decoratedParams = {};
|
||||
}
|
||||
if (!target.decoratedParams[propertyKey]) {
|
||||
target.decoratedParams[propertyKey] = [];
|
||||
}
|
||||
target.decoratedParams[propertyKey].push(parameterIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test class with decorators
|
||||
*/
|
||||
@testDecorator
|
||||
class TestClass {
|
||||
public name: string = 'test';
|
||||
|
||||
@logMethod
|
||||
public greet(message: string): string {
|
||||
return `Hello, ${message}!`;
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return 42;
|
||||
}
|
||||
|
||||
public testParams(@validateParam value: string): string {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
tap.test('Class decorator should be applied', async () => {
|
||||
expect((TestClass as any).decoratorApplied).toEqual(true);
|
||||
expect((TestClass as any).decoratorData).toEqual('Decorator was applied successfully');
|
||||
});
|
||||
|
||||
tap.test('Method decorator should modify method behavior', async () => {
|
||||
const instance = new TestClass();
|
||||
const result = instance.greet('World');
|
||||
expect(result).toEqual('[logged] Hello, World!');
|
||||
});
|
||||
|
||||
tap.test('Regular methods should work normally', async () => {
|
||||
const instance = new TestClass();
|
||||
expect(instance.getValue()).toEqual(42);
|
||||
expect(instance.name).toEqual('test');
|
||||
});
|
||||
|
||||
tap.test('Parameter decorator should be applied', async () => {
|
||||
const decoratedParams = (TestClass.prototype as any).decoratedParams;
|
||||
expect(decoratedParams).toBeDefined();
|
||||
expect(decoratedParams.testParams).toBeDefined();
|
||||
expect(decoratedParams.testParams).toContain(0);
|
||||
});
|
||||
|
||||
tap.test('Decorator metadata preservation', async () => {
|
||||
const instance = new TestClass();
|
||||
expect(instance instanceof TestClass).toEqual(true);
|
||||
expect(instance.constructor.name).toEqual('TestClass');
|
||||
expect(instance.testParams('hello')).toEqual('hello');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
170
test/tapbundle/test.new-lifecycle.ts
Normal file
170
test/tapbundle/test.new-lifecycle.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
// Global state for testing new lifecycle features
|
||||
const executionOrder: string[] = [];
|
||||
let postTaskRan = false;
|
||||
|
||||
// Test preTask and postTask
|
||||
tap.preTask('setup environment', async () => {
|
||||
executionOrder.push('preTask');
|
||||
console.log('🔧 PreTask: Setting up environment');
|
||||
});
|
||||
|
||||
tap.postTask('cleanup environment', async () => {
|
||||
postTaskRan = true;
|
||||
executionOrder.push('postTask');
|
||||
console.log('🧹 PostTask: Cleaning up environment');
|
||||
});
|
||||
|
||||
// Test suite-level beforeAll and afterAll
|
||||
tap.describe('Suite with beforeAll/afterAll', () => {
|
||||
tap.beforeAll(async () => {
|
||||
executionOrder.push('suite-beforeAll');
|
||||
console.log('🔰 Suite beforeAll executed');
|
||||
});
|
||||
|
||||
tap.afterAll(async () => {
|
||||
executionOrder.push('suite-afterAll');
|
||||
console.log('🏁 Suite afterAll executed');
|
||||
});
|
||||
|
||||
tap.beforeEach(async () => {
|
||||
executionOrder.push('suite-beforeEach');
|
||||
});
|
||||
|
||||
tap.afterEach(async () => {
|
||||
executionOrder.push('suite-afterEach');
|
||||
});
|
||||
|
||||
tap.test('first test in suite', async () => {
|
||||
executionOrder.push('test-1');
|
||||
expect(executionOrder).toContain('preTask');
|
||||
expect(executionOrder).toContain('suite-beforeAll');
|
||||
console.log('✓ Test 1 executed');
|
||||
});
|
||||
|
||||
tap.test('second test in suite', async () => {
|
||||
executionOrder.push('test-2');
|
||||
expect(executionOrder).toContain('suite-beforeAll');
|
||||
console.log('✓ Test 2 executed');
|
||||
});
|
||||
});
|
||||
|
||||
// Test nested suites with beforeAll/afterAll
|
||||
tap.describe('Parent Suite', () => {
|
||||
tap.beforeAll(async () => {
|
||||
executionOrder.push('parent-beforeAll');
|
||||
console.log('🔰 Parent beforeAll executed');
|
||||
});
|
||||
|
||||
tap.afterAll(async () => {
|
||||
executionOrder.push('parent-afterAll');
|
||||
console.log('🏁 Parent afterAll executed');
|
||||
});
|
||||
|
||||
tap.test('test in parent', async () => {
|
||||
executionOrder.push('parent-test');
|
||||
expect(executionOrder).toContain('parent-beforeAll');
|
||||
});
|
||||
|
||||
tap.describe('Child Suite', () => {
|
||||
tap.beforeAll(async () => {
|
||||
executionOrder.push('child-beforeAll');
|
||||
console.log('🔰 Child beforeAll executed');
|
||||
});
|
||||
|
||||
tap.afterAll(async () => {
|
||||
executionOrder.push('child-afterAll');
|
||||
console.log('🏁 Child afterAll executed');
|
||||
});
|
||||
|
||||
tap.test('test in child', async () => {
|
||||
executionOrder.push('child-test');
|
||||
expect(executionOrder).toContain('parent-beforeAll');
|
||||
expect(executionOrder).toContain('child-beforeAll');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test parallel() fluent API
|
||||
tap.parallel().test('parallel test 1', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
executionOrder.push('parallel-1');
|
||||
console.log('⚡ Parallel test 1 executed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.parallel().test('parallel test 2', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
executionOrder.push('parallel-2');
|
||||
console.log('⚡ Parallel test 2 executed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Test parallel() with configuration
|
||||
tap
|
||||
.parallel()
|
||||
.tags('integration', 'parallel')
|
||||
.timeout(1000)
|
||||
.test('configured parallel test', async () => {
|
||||
executionOrder.push('parallel-configured');
|
||||
console.log('⚡ Configured parallel test executed');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
// Verify execution order
|
||||
tap.test('verify lifecycle execution order', async () => {
|
||||
// Give a moment for any async operations to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
console.log('📊 Execution order:', executionOrder);
|
||||
|
||||
// Verify preTask ran first
|
||||
expect(executionOrder[0]).toEqual('preTask');
|
||||
|
||||
// Verify suite beforeAll ran before tests
|
||||
const suiteBeforeAllIndex = executionOrder.indexOf('suite-beforeAll');
|
||||
const test1Index = executionOrder.indexOf('test-1');
|
||||
expect(suiteBeforeAllIndex).toBeLessThan(test1Index);
|
||||
|
||||
// Verify beforeEach ran before each test
|
||||
const beforeEachIndices = executionOrder
|
||||
.map((item, index) => item === 'suite-beforeEach' ? index : -1)
|
||||
.filter(index => index !== -1);
|
||||
expect(beforeEachIndices.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify afterEach ran after each test
|
||||
const afterEachIndices = executionOrder
|
||||
.map((item, index) => item === 'suite-afterEach' ? index : -1)
|
||||
.filter(index => index !== -1);
|
||||
expect(afterEachIndices.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify afterAll ran after all tests
|
||||
const suiteAfterAllIndex = executionOrder.indexOf('suite-afterAll');
|
||||
const test2Index = executionOrder.indexOf('test-2');
|
||||
expect(suiteAfterAllIndex).toBeGreaterThan(test2Index);
|
||||
|
||||
// Verify nested suite lifecycle
|
||||
expect(executionOrder).toContain('parent-beforeAll');
|
||||
expect(executionOrder).toContain('parent-test');
|
||||
expect(executionOrder).toContain('child-beforeAll');
|
||||
expect(executionOrder).toContain('child-test');
|
||||
expect(executionOrder).toContain('child-afterAll');
|
||||
expect(executionOrder).toContain('parent-afterAll');
|
||||
|
||||
// Verify parallel tests ran
|
||||
expect(executionOrder).toContain('parallel-1');
|
||||
expect(executionOrder).toContain('parallel-2');
|
||||
expect(executionOrder).toContain('parallel-configured');
|
||||
|
||||
console.log('✅ All lifecycle hooks executed in correct order');
|
||||
});
|
||||
|
||||
// This test will verify postTask ran (after tap.start() completes)
|
||||
tap.test('verify postTask execution', async () => {
|
||||
// PostTask hasn't run yet because tests are still running
|
||||
expect(postTaskRan).toBeFalse();
|
||||
console.log('✓ Verified postTask will run after all tests');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
import { tapNodeTools } from '../../ts_tapbundle_node/index.js';
|
||||
import { tapNodeTools } from '../../ts_tapbundle_serverside/index.js';
|
||||
|
||||
tap.test('should execure a command', async () => {
|
||||
const result = await tapNodeTools.runCommand('ls -la');
|
||||
|
||||
9
test/test.example.latest.docker.sh
Executable file
9
test/test.example.latest.docker.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Sample Docker test file
|
||||
# This file demonstrates the naming pattern: test.{baseName}.{variant}.docker.sh
|
||||
# The variant "latest" maps to the Dockerfile in the project root
|
||||
|
||||
echo "TAP version 13"
|
||||
echo "1..2"
|
||||
echo "ok 1 - Sample Docker test passes"
|
||||
echo "ok 2 - Docker environment is working"
|
||||
@@ -164,4 +164,40 @@ tap.test('parseTestFilename - handles full paths', async () => {
|
||||
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();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '2.4.3',
|
||||
version: '3.1.1',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
const result = await this.smartshellInstance.exec('bun --version', {
|
||||
const result = await this.smartshellInstance.execSilent('bun --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
@@ -47,11 +47,11 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
||||
}
|
||||
|
||||
// Bun version is just the version number
|
||||
const version = result.stdout.trim();
|
||||
const version = `v${result.stdout.trim()}`;
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: `Bun ${version}`,
|
||||
version: version,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -69,6 +69,9 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
||||
|
||||
const args: string[] = ['run'];
|
||||
|
||||
// Note: Bun automatically discovers bunfig.toml in the current directory
|
||||
// This ensures TypeScript decorator support is enabled if bunfig.toml is present
|
||||
|
||||
// Add extra args
|
||||
if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) {
|
||||
args.push(...mergedOptions.extraArgs);
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
||||
// The browser binary is usually handled by @push.rocks/smartbrowser
|
||||
return {
|
||||
available: true,
|
||||
version: 'Chromium (via smartbrowser)',
|
||||
version: 'via smartbrowser',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -31,8 +31,20 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
||||
* Get default Deno options
|
||||
*/
|
||||
protected getDefaultOptions(): DenoOptions {
|
||||
// Auto-detect deno.json or deno.jsonc config file for TypeScript decorator support
|
||||
let configPath: string | undefined;
|
||||
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
|
||||
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
|
||||
|
||||
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) {
|
||||
configPath = denoJsonPath;
|
||||
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) {
|
||||
configPath = denoJsoncPath;
|
||||
}
|
||||
|
||||
return {
|
||||
...super.getDefaultOptions(),
|
||||
configPath,
|
||||
permissions: [
|
||||
'--allow-read',
|
||||
'--allow-env',
|
||||
@@ -51,7 +63,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
const result = await this.smartshellInstance.exec('deno --version', {
|
||||
const result = await this.smartshellInstance.execSilent('deno --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
@@ -67,11 +79,11 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
||||
|
||||
// Parse Deno version from output (first line is "deno X.Y.Z")
|
||||
const versionMatch = result.stdout.match(/deno (\d+\.\d+\.\d+)/);
|
||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
||||
const version = versionMatch ? `v${versionMatch[1]}` : 'unknown';
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: `Deno ${version}`,
|
||||
version: version,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
251
ts/tstest.classes.runtime.docker.ts
Normal file
251
ts/tstest.classes.runtime.docker.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import {
|
||||
RuntimeAdapter,
|
||||
type RuntimeOptions,
|
||||
type RuntimeCommand,
|
||||
type RuntimeAvailability,
|
||||
} from './tstest.classes.runtime.adapter.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
import {
|
||||
parseDockerTestFilename,
|
||||
mapVariantToDockerfile,
|
||||
isDockerTestFile
|
||||
} from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Docker runtime adapter
|
||||
* Executes shell test files inside Docker containers
|
||||
* Pattern: test.{variant}.docker.sh
|
||||
* Variants map to Dockerfiles: latest -> Dockerfile, others -> Dockerfile_{variant}
|
||||
*/
|
||||
export class DockerRuntimeAdapter extends RuntimeAdapter {
|
||||
readonly id: Runtime = 'node'; // Using 'node' temporarily as Runtime type doesn't include 'docker'
|
||||
readonly displayName: string = 'Docker';
|
||||
|
||||
private builtImages: Set<string> = new Set(); // Track built images to avoid rebuilding
|
||||
|
||||
constructor(
|
||||
private logger: TsTestLogger,
|
||||
private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell
|
||||
private timeoutSeconds: number | null,
|
||||
private cwd: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker CLI is available
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
const result = await this.smartshellInstance.exec('docker --version');
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Docker command failed',
|
||||
};
|
||||
}
|
||||
|
||||
// Extract version from output like "Docker version 24.0.5, build ced0996"
|
||||
const versionMatch = result.stdout.match(/Docker version ([^,]+)/);
|
||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: `Docker not found: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command configuration for Docker test execution
|
||||
* This is used for informational purposes
|
||||
*/
|
||||
createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand {
|
||||
const parsed = parseDockerTestFilename(testFile);
|
||||
const dockerfilePath = mapVariantToDockerfile(parsed.variant, this.cwd);
|
||||
const imageName = `tstest-${parsed.variant}`;
|
||||
|
||||
return {
|
||||
command: 'docker',
|
||||
args: [
|
||||
'run',
|
||||
'--rm',
|
||||
'-v',
|
||||
`${this.cwd}/test:/test`,
|
||||
imageName,
|
||||
'taprun',
|
||||
`/test/${plugins.path.basename(testFile)}`
|
||||
],
|
||||
env: {},
|
||||
cwd: this.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Docker image from the specified Dockerfile
|
||||
*/
|
||||
private async buildDockerImage(dockerfilePath: string, imageName: string): Promise<void> {
|
||||
// Check if image is already built
|
||||
if (this.builtImages.has(imageName)) {
|
||||
this.logger.tapOutput(`Using cached Docker image: ${imageName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Dockerfile exists
|
||||
if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) {
|
||||
throw new Error(
|
||||
`Dockerfile not found: ${dockerfilePath}\n` +
|
||||
`Expected Dockerfile for Docker test variant.`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.tapOutput(`Building Docker image: ${imageName} from ${dockerfilePath}`);
|
||||
|
||||
try {
|
||||
const buildResult = await this.smartshellInstance.exec(
|
||||
`docker build -f ${dockerfilePath} -t ${imageName} ${this.cwd}`,
|
||||
{
|
||||
cwd: this.cwd,
|
||||
}
|
||||
);
|
||||
|
||||
if (buildResult.exitCode !== 0) {
|
||||
throw new Error(`Docker build failed:\n${buildResult.stderr}`);
|
||||
}
|
||||
|
||||
this.builtImages.add(imageName);
|
||||
this.logger.tapOutput(`✅ Docker image built successfully: ${imageName}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to build Docker image: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Docker test file
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: RuntimeOptions
|
||||
): Promise<TapParser> {
|
||||
this.logger.testFileStart(testFile, this.displayName, index, total);
|
||||
|
||||
// Parse the Docker test filename
|
||||
const parsed = parseDockerTestFilename(testFile);
|
||||
const dockerfilePath = mapVariantToDockerfile(parsed.variant, this.cwd);
|
||||
const imageName = `tstest-${parsed.variant}`;
|
||||
|
||||
// Build the Docker image
|
||||
await this.buildDockerImage(dockerfilePath, imageName);
|
||||
|
||||
// Prepare the test file path relative to the mounted directory
|
||||
// We need to get the path relative to cwd
|
||||
const absoluteTestPath = plugins.path.isAbsolute(testFile)
|
||||
? testFile
|
||||
: plugins.path.join(this.cwd, testFile);
|
||||
|
||||
const relativeTestPath = plugins.path.relative(this.cwd, absoluteTestPath);
|
||||
|
||||
// Create TAP parser
|
||||
const tapParser = new TapParser(testFile + ':docker', this.logger);
|
||||
|
||||
try {
|
||||
// Build docker run command
|
||||
const dockerArgs = [
|
||||
'run',
|
||||
'--rm',
|
||||
'-v',
|
||||
`${this.cwd}/test:/test`,
|
||||
imageName,
|
||||
'taprun',
|
||||
`/test/${plugins.path.basename(testFile)}`
|
||||
];
|
||||
|
||||
this.logger.tapOutput(`Executing: docker ${dockerArgs.join(' ')}`);
|
||||
|
||||
// Execute the Docker container
|
||||
const execPromise = this.smartshellInstance.execStreaming(
|
||||
`docker ${dockerArgs.join(' ')}`,
|
||||
{
|
||||
cwd: this.cwd,
|
||||
}
|
||||
);
|
||||
|
||||
// Set up timeout if configured
|
||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||
if (this.timeoutSeconds) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
this.logger.tapOutput(`⏱️ Test timeout (${this.timeoutSeconds}s) - killing container`);
|
||||
// Try to kill any running containers with this image
|
||||
this.smartshellInstance.exec(`docker ps -q --filter ancestor=${imageName} | xargs -r docker kill`);
|
||||
}, this.timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
// Stream output to TAP parser line by line
|
||||
execPromise.childProcess.stdout.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
tapParser.handleTapLog(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
execPromise.childProcess.stderr.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
this.logger.tapOutput(cs(`[stderr] ${output}`, 'orange'));
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
const result = await execPromise;
|
||||
|
||||
// Clear timeout
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
this.logger.tapOutput(cs(`❌ Docker test failed with exit code ${result.exitCode}`, 'red'));
|
||||
}
|
||||
|
||||
// Evaluate final result
|
||||
await tapParser.evaluateFinalResult();
|
||||
|
||||
} catch (error) {
|
||||
this.logger.tapOutput(cs(`❌ Error running Docker test: ${error.message}`, 'red'));
|
||||
// Add a failing test result to the parser
|
||||
tapParser.handleTapLog('not ok 1 - Docker test execution failed');
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
|
||||
return tapParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up built Docker images (optional, can be called at end of test suite)
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
for (const imageName of this.builtImages) {
|
||||
try {
|
||||
this.logger.tapOutput(`Removing Docker image: ${imageName}`);
|
||||
await this.smartshellInstance.exec(`docker rmi ${imageName}`);
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
this.logger.tapOutput(cs(`Warning: Failed to remove image ${imageName}: ${error.message}`, 'orange'));
|
||||
}
|
||||
}
|
||||
this.builtImages.clear();
|
||||
}
|
||||
}
|
||||
@@ -35,18 +35,11 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
||||
// Check Node.js version
|
||||
const nodeVersion = process.version;
|
||||
|
||||
// Check if tsrun is available
|
||||
const result = await this.smartshellInstance.exec('tsrun --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
}
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
// Check if tsrun module is available (imported as dependency)
|
||||
if (!plugins.tsrun || !plugins.tsrun.spawnPath) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'tsrun not found. Install with: pnpm install --save-dev @git.zone/tsrun',
|
||||
error: 'tsrun module not found or outdated (requires version 1.6.0+)',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,7 +89,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a test file in Node.js
|
||||
* Execute a test file in Node.js using tsrun's spawnPath API
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
@@ -109,28 +102,35 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
||||
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
// Build tsrun command
|
||||
let tsrunOptions = '';
|
||||
// Build spawn options
|
||||
const spawnOptions: any = {
|
||||
cwd: mergedOptions.cwd || process.cwd(),
|
||||
env: { ...mergedOptions.env },
|
||||
args: [] as string[],
|
||||
stdio: 'pipe' as const,
|
||||
};
|
||||
|
||||
// Add --web flag if needed
|
||||
if (process.argv.includes('--web')) {
|
||||
tsrunOptions += ' --web';
|
||||
spawnOptions.args.push('--web');
|
||||
}
|
||||
|
||||
// Set filter tags as environment variable
|
||||
if (this.filterTags.length > 0) {
|
||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
spawnOptions.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
// Check for 00init.ts file in test directory
|
||||
const testDir = plugins.path.dirname(testFile);
|
||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||
let runCommand = `tsrun ${testFile}${tsrunOptions}`;
|
||||
|
||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
||||
|
||||
// If 00init.ts exists, run it first
|
||||
// Determine which file to run
|
||||
let fileToRun = testFile;
|
||||
let loaderPath: string | null = null;
|
||||
|
||||
// If 00init.ts exists, create a loader file
|
||||
if (initFileExists) {
|
||||
// Create a temporary loader file that imports both 00init.ts and the test file
|
||||
const absoluteInitFile = plugins.path.resolve(initFile);
|
||||
const absoluteTestFile = plugins.path.resolve(testFile);
|
||||
const loaderContent = `
|
||||
@@ -139,10 +139,12 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
`;
|
||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
||||
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
||||
fileToRun = loaderPath;
|
||||
}
|
||||
|
||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
||||
// Spawn the test process using tsrun's spawnPath API
|
||||
// Pass undefined for fromFileUrl since fileToRun is already an absolute path
|
||||
const tsrunProcess = plugins.tsrun.spawnPath(fileToRun, undefined, spawnOptions);
|
||||
|
||||
// If we created a loader file, clean it up after test execution
|
||||
if (loaderPath) {
|
||||
@@ -156,8 +158,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
}
|
||||
};
|
||||
|
||||
execResultStreaming.childProcess.on('exit', cleanup);
|
||||
execResultStreaming.childProcess.on('error', cleanup);
|
||||
tsrunProcess.childProcess.on('exit', cleanup);
|
||||
tsrunProcess.childProcess.on('error', cleanup);
|
||||
}
|
||||
|
||||
// Start warning timer if no timeout was specified
|
||||
@@ -180,15 +182,15 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
// Use tsrun's terminate() to gracefully kill the process
|
||||
await tsrunProcess.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
tapParser.handleTapProcess(tsrunProcess.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
@@ -200,16 +202,16 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
}
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
// Ensure process is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
tsrunProcess.kill('SIGKILL');
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
// Process might already be dead
|
||||
}
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
await tapParser.handleTapProcess(tsrunProcess.childProcess);
|
||||
}
|
||||
|
||||
// Clear warning timer if it was set
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* - test.chromium.ts
|
||||
* - test.node+chromium.ts
|
||||
* - test.deno+bun.ts
|
||||
* - test.all.ts (runs on all runtimes)
|
||||
* - test.chromium.nonci.ts
|
||||
*/
|
||||
|
||||
@@ -28,7 +29,8 @@ export interface ParserConfig {
|
||||
|
||||
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
||||
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', 'sh']);
|
||||
const ALL_RUNTIMES: Runtime[] = ['node', 'chromium', 'deno', 'bun'];
|
||||
|
||||
// Legacy mappings for backwards compatibility
|
||||
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 validRuntimes: Runtime[] = [];
|
||||
const invalidRuntimes: string[] = [];
|
||||
let hasAllKeyword = false;
|
||||
|
||||
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
|
||||
if (!validRuntimes.includes(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 (strictUnknownRuntime) {
|
||||
throw new Error(
|
||||
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
||||
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}`
|
||||
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}, all`
|
||||
);
|
||||
} else {
|
||||
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
|
||||
if (KNOWN_RUNTIMES.has(token)) {
|
||||
runtimes = [token as Runtime];
|
||||
@@ -209,3 +228,81 @@ export function getLegacyMigrationTarget(fileName: string): string | null {
|
||||
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Docker test file information
|
||||
*/
|
||||
export interface DockerTestFileInfo {
|
||||
baseName: string;
|
||||
variant: string;
|
||||
isDockerTest: true;
|
||||
original: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a filename matches the Docker test pattern: *.{variant}.docker.sh
|
||||
* Examples: test.latest.docker.sh, test.integration.npmci.docker.sh
|
||||
*/
|
||||
export function isDockerTestFile(fileName: string): boolean {
|
||||
// Must end with .docker.sh
|
||||
if (!fileName.endsWith('.docker.sh')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract filename from path if needed
|
||||
const name = fileName.split('/').pop() || fileName;
|
||||
|
||||
// Must have at least 3 parts: [baseName, variant, docker, sh]
|
||||
const parts = name.split('.');
|
||||
return parts.length >= 4 && parts[parts.length - 2] === 'docker' && parts[parts.length - 1] === 'sh';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Docker test filename to extract variant and base name
|
||||
* Pattern: test.{baseName}.{variant}.docker.sh
|
||||
* Examples:
|
||||
* - test.latest.docker.sh -> { baseName: 'test', variant: 'latest' }
|
||||
* - test.integration.npmci.docker.sh -> { baseName: 'test.integration', variant: 'npmci' }
|
||||
*/
|
||||
export function parseDockerTestFilename(filePath: string): DockerTestFileInfo {
|
||||
// Extract just the filename from the path
|
||||
const fileName = filePath.split('/').pop() || filePath;
|
||||
const original = fileName;
|
||||
|
||||
if (!isDockerTestFile(fileName)) {
|
||||
throw new Error(`Not a valid Docker test file: "${fileName}". Expected pattern: *.{variant}.docker.sh`);
|
||||
}
|
||||
|
||||
// Remove .docker.sh suffix
|
||||
const withoutSuffix = fileName.slice(0, -10); // Remove '.docker.sh'
|
||||
const tokens = withoutSuffix.split('.');
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error(`Invalid Docker test file: empty basename in "${fileName}"`);
|
||||
}
|
||||
|
||||
// Last token before .docker.sh is the variant
|
||||
const variant = tokens[tokens.length - 1];
|
||||
|
||||
// Everything else is the base name
|
||||
const baseName = tokens.slice(0, -1).join('.');
|
||||
|
||||
return {
|
||||
baseName: baseName || 'test',
|
||||
variant,
|
||||
isDockerTest: true,
|
||||
original,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Docker variant to its corresponding Dockerfile path
|
||||
* "latest" -> "Dockerfile"
|
||||
* Other variants -> "Dockerfile_{variant}"
|
||||
*/
|
||||
export function mapVariantToDockerfile(variant: string, baseDir: string): string {
|
||||
if (variant === 'latest') {
|
||||
return `${baseDir}/Dockerfile`;
|
||||
}
|
||||
return `${baseDir}/Dockerfile_${variant}`;
|
||||
}
|
||||
|
||||
@@ -74,12 +74,20 @@ export class TestDirectory {
|
||||
case TestExecutionMode.DIRECTORY:
|
||||
// Directory mode - now recursive with ** pattern
|
||||
const dirPath = plugins.path.join(this.cwd, this.testPath);
|
||||
const testPattern = '**/test*.ts';
|
||||
|
||||
const testFiles = await plugins.smartfile.fs.listFileTree(dirPath, testPattern);
|
||||
// Search for both TypeScript test files and Docker shell test files
|
||||
const tsPattern = '**/test*.ts';
|
||||
const dockerPattern = '**/*.docker.sh';
|
||||
|
||||
const [tsFiles, dockerFiles] = await Promise.all([
|
||||
plugins.smartfile.fs.listFileTree(dirPath, tsPattern),
|
||||
plugins.smartfile.fs.listFileTree(dirPath, dockerPattern),
|
||||
]);
|
||||
|
||||
const allTestFiles = [...tsFiles, ...dockerFiles];
|
||||
|
||||
this.testfileArray = await Promise.all(
|
||||
testFiles.map(async (filePath) => {
|
||||
allTestFiles.map(async (filePath) => {
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: plugins.path.join(dirPath, filePath);
|
||||
|
||||
@@ -11,12 +11,13 @@ import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { LogOptions } from './tstest.logging.js';
|
||||
|
||||
// Runtime adapters
|
||||
import { parseTestFilename } from './tstest.classes.runtime.parser.js';
|
||||
import { parseTestFilename, isDockerTestFile, parseDockerTestFilename } from './tstest.classes.runtime.parser.js';
|
||||
import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js';
|
||||
import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js';
|
||||
import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js';
|
||||
import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
|
||||
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
|
||||
import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js';
|
||||
|
||||
export class TsTest {
|
||||
public testDir: TestDirectory;
|
||||
@@ -37,6 +38,7 @@ export class TsTest {
|
||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||
|
||||
public runtimeRegistry = new RuntimeAdapterRegistry();
|
||||
public dockerAdapter: DockerRuntimeAdapter | null = null;
|
||||
|
||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
|
||||
this.executionMode = executionModeArg;
|
||||
@@ -60,9 +62,29 @@ export class TsTest {
|
||||
this.runtimeRegistry.register(
|
||||
new BunRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
||||
);
|
||||
|
||||
// Initialize Docker adapter
|
||||
this.dockerAdapter = new DockerRuntimeAdapter(
|
||||
this.logger,
|
||||
this.smartshellInstance,
|
||||
this.timeoutSeconds,
|
||||
cwdArg
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display available runtimes
|
||||
*/
|
||||
private async checkEnvironment() {
|
||||
const availability = await this.runtimeRegistry.checkAvailability();
|
||||
this.logger.environmentCheck(availability);
|
||||
return availability;
|
||||
}
|
||||
|
||||
async run() {
|
||||
// Check and display environment
|
||||
await this.checkEnvironment();
|
||||
|
||||
// Move previous log files if --logfile option is used
|
||||
if (this.logger.options.logFile) {
|
||||
await this.movePreviousLogFiles();
|
||||
@@ -199,8 +221,14 @@ export class TsTest {
|
||||
}
|
||||
|
||||
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||
// Parse the filename to determine runtimes and modifiers
|
||||
const fileName = plugins.path.basename(fileNameArg);
|
||||
|
||||
// Check if this is a Docker test file
|
||||
if (isDockerTestFile(fileName)) {
|
||||
return await this.runDockerTest(fileNameArg, fileIndex, totalFiles, tapCombinator);
|
||||
}
|
||||
|
||||
// Parse the filename to determine runtimes and modifiers (for TypeScript tests)
|
||||
const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false });
|
||||
|
||||
// Check for nonci modifier in CI environment
|
||||
@@ -246,6 +274,28 @@ export class TsTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Docker test file
|
||||
*/
|
||||
private async runDockerTest(
|
||||
fileNameArg: string,
|
||||
fileIndex: number,
|
||||
totalFiles: number,
|
||||
tapCombinator: TapCombinator
|
||||
): Promise<void> {
|
||||
if (!this.dockerAdapter) {
|
||||
this.logger.tapOutput(cs('❌ Docker adapter not initialized', 'red'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParser);
|
||||
} catch (error) {
|
||||
this.logger.tapOutput(cs(`❌ Docker test failed: ${error.message}`, 'red'));
|
||||
}
|
||||
}
|
||||
|
||||
public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
||||
this.logger.testFileStart(fileNameArg, 'node.js', index, total);
|
||||
const tapParser = new TapParser(fileNameArg + ':node', this.logger);
|
||||
|
||||
@@ -138,6 +138,43 @@ export class TsTestLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Environment check - display available runtimes
|
||||
environmentCheck(availability: Map<string, { available: boolean; version?: string; error?: string }>) {
|
||||
if (this.options.json) {
|
||||
const runtimes: any = {};
|
||||
for (const [runtime, info] of availability) {
|
||||
runtimes[runtime] = info;
|
||||
}
|
||||
this.logJson({ event: 'environmentCheck', runtimes });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) return;
|
||||
|
||||
this.log(this.format('\n🌍 Test Environment', 'bold'));
|
||||
|
||||
// Define runtime display names
|
||||
const runtimeNames: Record<string, string> = {
|
||||
node: 'Node.js',
|
||||
deno: 'Deno',
|
||||
bun: 'Bun',
|
||||
chromium: 'Chrome/Chromium'
|
||||
};
|
||||
|
||||
// Display each runtime
|
||||
for (const [runtime, info] of availability) {
|
||||
const displayName = runtimeNames[runtime] || runtime;
|
||||
|
||||
if (info.available) {
|
||||
const versionStr = info.version ? ` ${info.version}` : '';
|
||||
this.log(this.format(` ✓ ${displayName}${versionStr}`, 'green'));
|
||||
} else {
|
||||
const errorStr = info.error ? ` (${info.error})` : '';
|
||||
this.log(this.format(` ✗ ${displayName}${errorStr}`, 'dim'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test execution
|
||||
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
||||
this.currentFileResult = {
|
||||
@@ -164,7 +201,10 @@ export class TsTestLogger {
|
||||
.replace(/\.ts$/, '') // Remove .ts extension
|
||||
.replace(/^\.\.__|^\.__|^__/, ''); // Clean up leading separators from relative paths
|
||||
|
||||
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}.log`);
|
||||
// Sanitize runtime name for use in filename (lowercase, no spaces/dots/special chars)
|
||||
const safeRuntime = runtime.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}__${safeRuntime}.log`);
|
||||
|
||||
// Ensure the directory exists
|
||||
const logDir = path.dirname(this.currentTestLogFile);
|
||||
|
||||
@@ -37,8 +37,9 @@ export {
|
||||
|
||||
// @git.zone scope
|
||||
import * as tsbundle from '@git.zone/tsbundle';
|
||||
import * as tsrun from '@git.zone/tsrun';
|
||||
|
||||
export { tsbundle };
|
||||
export { tsbundle, tsrun };
|
||||
|
||||
// sindresorhus
|
||||
import figures from 'figures';
|
||||
|
||||
@@ -91,6 +91,24 @@ tap.testParallel('should fetch user data', async () => {
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** The `tap.parallel().test()` fluent API is now the recommended way to define parallel tests (see Fluent API section below).
|
||||
|
||||
#### `tap.parallel()`
|
||||
|
||||
Returns a fluent test builder configured for parallel execution.
|
||||
|
||||
```typescript
|
||||
tap.parallel().test('should fetch data', async () => {
|
||||
// Parallel test
|
||||
});
|
||||
|
||||
// With full configuration
|
||||
tap.parallel()
|
||||
.tags('api')
|
||||
.retry(2)
|
||||
.test('configured parallel test', async () => {});
|
||||
```
|
||||
|
||||
#### `tap.describe(description, suiteFunction)`
|
||||
|
||||
Create a test suite to group related tests.
|
||||
@@ -141,22 +159,56 @@ tap
|
||||
});
|
||||
```
|
||||
|
||||
#### Parallel Tests with Fluent API
|
||||
|
||||
Use `tap.parallel()` to create parallel tests with fluent configuration:
|
||||
|
||||
```typescript
|
||||
// Simple parallel test
|
||||
tap.parallel().test('fetches user data', async () => {
|
||||
// Runs in parallel with other parallel tests
|
||||
});
|
||||
|
||||
// Parallel test with full configuration
|
||||
tap
|
||||
.parallel()
|
||||
.tags('api', 'integration')
|
||||
.retry(2)
|
||||
.timeout(5000)
|
||||
.test('should fetch data concurrently', async () => {
|
||||
// Configured parallel test
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** `tap.parallel().test()` is the recommended way to define parallel tests. The older `tap.testParallel()` method is still supported for backward compatibility.
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
#### Suite-Level Hooks
|
||||
|
||||
```typescript
|
||||
tap.describe('Database Tests', () => {
|
||||
tap.beforeAll(async (tapTools) => {
|
||||
// Runs once before all tests in this suite
|
||||
await initializeDatabaseConnection();
|
||||
});
|
||||
|
||||
tap.beforeEach(async (tapTools) => {
|
||||
// Runs before each test in this suite
|
||||
await clearTestData();
|
||||
});
|
||||
|
||||
tap.test('test 1', async () => { });
|
||||
tap.test('test 2', async () => { });
|
||||
|
||||
tap.afterEach(async (tapTools) => {
|
||||
// Runs after each test in this suite
|
||||
});
|
||||
|
||||
tap.test('test 1', async () => { });
|
||||
tap.test('test 2', async () => { });
|
||||
tap.afterAll(async (tapTools) => {
|
||||
// Runs once after all tests in this suite
|
||||
await closeDatabaseConnection();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -267,38 +319,169 @@ TSTEST_FILTER_TAGS=unit tstest test/mytest.node.ts
|
||||
|
||||
Each test receives a `tapTools` instance with utilities:
|
||||
|
||||
#### Test Control Methods
|
||||
|
||||
```typescript
|
||||
tap.test('should have utilities', async (tapTools) => {
|
||||
// Mark test as skipped
|
||||
tap.test('test control examples', async (tapTools) => {
|
||||
// Skip this test
|
||||
tapTools.skip('reason');
|
||||
|
||||
// Conditionally skip
|
||||
tapTools.skipIf(condition, 'reason');
|
||||
|
||||
// Mark test as skipped before execution
|
||||
tapTools.markAsSkipped('reason');
|
||||
|
||||
// Mark as todo
|
||||
tapTools.todo('not implemented');
|
||||
|
||||
// Allow test to fail without marking suite as failed
|
||||
tapTools.allowFailure();
|
||||
|
||||
// Configure retries
|
||||
tapTools.retry(3);
|
||||
|
||||
// Log test output
|
||||
tapTools.log('debug message');
|
||||
// Set timeout
|
||||
tapTools.timeout(5000);
|
||||
});
|
||||
```
|
||||
|
||||
#### Utility Methods
|
||||
|
||||
```typescript
|
||||
tap.test('utility examples', async (tapTools) => {
|
||||
// Delay execution
|
||||
await tapTools.delayFor(1000); // Wait 1 second
|
||||
await tapTools.delayForRandom(500, 1500); // Random delay
|
||||
|
||||
// Colored console output
|
||||
tapTools.coloredString('✓ Success', 'green');
|
||||
tapTools.coloredString('✗ Error', 'red');
|
||||
});
|
||||
```
|
||||
|
||||
#### Context and Data Sharing
|
||||
|
||||
```typescript
|
||||
tap.test('first test', async (tapTools) => {
|
||||
// Store data in context
|
||||
tapTools.context.set('userId', '12345');
|
||||
|
||||
// Store in testData property
|
||||
tapTools.testData = { username: 'alice' };
|
||||
});
|
||||
|
||||
tap.test('second test', async (tapTools) => {
|
||||
// Retrieve from context
|
||||
const userId = tapTools.context.get('userId');
|
||||
|
||||
// Check existence
|
||||
if (tapTools.context.has('userId')) {
|
||||
// Use data
|
||||
}
|
||||
|
||||
// Clear context
|
||||
tapTools.context.clear();
|
||||
});
|
||||
```
|
||||
|
||||
#### Fixtures
|
||||
|
||||
```typescript
|
||||
// Define a fixture globally (outside tests)
|
||||
import { TapTools } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
TapTools.defineFixture('database', async () => {
|
||||
const db = await createTestDatabase();
|
||||
return {
|
||||
value: db,
|
||||
cleanup: async () => await db.close()
|
||||
};
|
||||
});
|
||||
|
||||
// Use fixtures in tests
|
||||
tap.test('database test', async (tapTools) => {
|
||||
const db = await tapTools.fixture('database');
|
||||
// Use db...
|
||||
// Cleanup happens automatically
|
||||
});
|
||||
```
|
||||
|
||||
#### Factory Pattern
|
||||
|
||||
```typescript
|
||||
// Define a factory
|
||||
TapTools.defineFixture('user', async () => {
|
||||
return {
|
||||
value: null, // Not used for factories
|
||||
factory: async (data) => {
|
||||
return await createUser(data);
|
||||
},
|
||||
cleanup: async (user) => await user.delete()
|
||||
};
|
||||
});
|
||||
|
||||
// Use factory in tests
|
||||
tap.test('user test', async (tapTools) => {
|
||||
const user = await tapTools.factory('user').create({ name: 'Alice' });
|
||||
|
||||
// Create multiple
|
||||
const users = await tapTools.factory('user').createMany([
|
||||
{ name: 'Alice' },
|
||||
{ name: 'Bob' }
|
||||
]);
|
||||
|
||||
// Cleanup happens automatically
|
||||
});
|
||||
```
|
||||
|
||||
#### Snapshot Testing
|
||||
|
||||
```typescript
|
||||
tap.test('snapshot test', async (tapTools) => {
|
||||
const result = { name: 'Alice', age: 30 };
|
||||
|
||||
// Compare with stored snapshot
|
||||
await tapTools.matchSnapshot(result);
|
||||
|
||||
// Named snapshots
|
||||
await tapTools.matchSnapshot(result, 'user-data');
|
||||
});
|
||||
```
|
||||
|
||||
To update snapshots, run with:
|
||||
```bash
|
||||
UPDATE_SNAPSHOTS=true tstest test/mytest.ts
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Pre-Tasks
|
||||
### Pre-Tasks and Post-Tasks
|
||||
|
||||
Run setup tasks before any tests execute:
|
||||
Run setup and teardown tasks before/after all tests:
|
||||
|
||||
```typescript
|
||||
tap.preTask('setup database', async () => {
|
||||
// Runs before any tests
|
||||
await initializeDatabase();
|
||||
});
|
||||
|
||||
tap.test('first test', async () => {
|
||||
// Database is ready
|
||||
});
|
||||
|
||||
tap.test('second test', async () => {
|
||||
// Tests run...
|
||||
});
|
||||
|
||||
tap.postTask('cleanup database', async () => {
|
||||
// Runs after all tests complete
|
||||
await cleanupDatabase();
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Post tasks run after all tests but before the global `afterAll` hook.
|
||||
|
||||
### Test Priority
|
||||
|
||||
Organize tests by priority level:
|
||||
@@ -334,6 +517,50 @@ import { setProtocolEmitter } from '@git.zone/tstest/tapbundle';
|
||||
// Events: test:started, test:completed, assertion:failed, suite:started, suite:completed
|
||||
```
|
||||
|
||||
### Additional Tap Methods
|
||||
|
||||
#### Configuration and Inspection
|
||||
|
||||
```typescript
|
||||
// Get current test settings
|
||||
const settings = tap.getSettings();
|
||||
console.log(settings.timeout, settings.retries);
|
||||
|
||||
// Explicitly fail a test
|
||||
tap.test('validation test', async () => {
|
||||
if (invalidCondition) {
|
||||
tap.fail('Custom failure message');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Advanced Control
|
||||
|
||||
```typescript
|
||||
// Force stop test execution
|
||||
tap.stopForcefully(exitCode, immediate);
|
||||
|
||||
// Handle thrown errors (internal use)
|
||||
tap.threw(error);
|
||||
```
|
||||
|
||||
#### Parallel Test Variants
|
||||
|
||||
In addition to `tap.parallel().test()`, skip/only/todo modes also support parallel execution:
|
||||
|
||||
```typescript
|
||||
// Skip parallel test
|
||||
tap.skip.testParallel('not ready', async () => {});
|
||||
|
||||
// Only run this parallel test
|
||||
tap.only.testParallel('focus here', async () => {});
|
||||
|
||||
// Todo parallel test
|
||||
tap.todo.testParallel('implement later');
|
||||
```
|
||||
|
||||
**Note:** Using `tap.parallel()` fluent API is recommended over these direct methods.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always export `tap.start()`** at the end of test files:
|
||||
|
||||
21
ts_tapbundle/tapbundle.classes.posttask.ts
Normal file
21
ts_tapbundle/tapbundle.classes.posttask.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||
|
||||
export interface IPostTaskFunction {
|
||||
(tapTools?: TapTools): Promise<any>;
|
||||
}
|
||||
|
||||
export class PostTask {
|
||||
public description: string;
|
||||
public postTaskFunction: IPostTaskFunction;
|
||||
|
||||
constructor(descriptionArg: string, postTaskFunctionArg: IPostTaskFunction) {
|
||||
this.description = descriptionArg;
|
||||
this.postTaskFunction = postTaskFunctionArg;
|
||||
}
|
||||
|
||||
public async run() {
|
||||
console.log(`::__POSTTASK: ${this.description}`);
|
||||
await this.postTaskFunction(new TapTools(null));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
|
||||
import { type IPreTaskFunction, PreTask } from './tapbundle.classes.pretask.js';
|
||||
import { type IPostTaskFunction, PostTask } from './tapbundle.classes.posttask.js';
|
||||
import { TapTest, type ITestFunction } from './tapbundle.classes.taptest.js';
|
||||
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||
import { ProtocolEmitter, type ITestEvent } from '../dist_ts_tapbundle_protocol/index.js';
|
||||
import type { ITapSettings } from './tapbundle.interfaces.js';
|
||||
import { SettingsManager } from './tapbundle.classes.settingsmanager.js';
|
||||
@@ -9,6 +11,8 @@ import { SettingsManager } from './tapbundle.classes.settingsmanager.js';
|
||||
export interface ITestSuite {
|
||||
description: string;
|
||||
tests: TapTest<any>[];
|
||||
beforeAll?: ITestFunction<any>;
|
||||
afterAll?: ITestFunction<any>;
|
||||
beforeEach?: ITestFunction<any>;
|
||||
afterEach?: ITestFunction<any>;
|
||||
parent?: ITestSuite;
|
||||
@@ -21,9 +25,11 @@ class TestBuilder<T> {
|
||||
private _priority: 'high' | 'medium' | 'low' = 'medium';
|
||||
private _retryCount?: number;
|
||||
private _timeoutMs?: number;
|
||||
private _parallel: boolean = false;
|
||||
|
||||
constructor(tap: Tap<T>) {
|
||||
constructor(tap: Tap<T>, parallel: boolean = false) {
|
||||
this._tap = tap;
|
||||
this._parallel = parallel;
|
||||
}
|
||||
|
||||
tags(...tags: string[]) {
|
||||
@@ -47,7 +53,9 @@ class TestBuilder<T> {
|
||||
}
|
||||
|
||||
test(description: string, testFunction: ITestFunction<T>) {
|
||||
const test = this._tap.test(description, testFunction, 'normal');
|
||||
const test = this._parallel
|
||||
? this._tap.testParallel(description, testFunction)
|
||||
: this._tap.test(description, testFunction, 'normal');
|
||||
|
||||
// Apply settings to the test
|
||||
if (this._tags.length > 0) {
|
||||
@@ -138,6 +146,10 @@ export class Tap<T> {
|
||||
return builder.timeout(ms);
|
||||
}
|
||||
|
||||
public parallel() {
|
||||
return new TestBuilder<T>(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* skips a test
|
||||
* tests marked with tap.skip.test() are never executed
|
||||
@@ -236,6 +248,7 @@ export class Tap<T> {
|
||||
};
|
||||
|
||||
private _tapPreTasks: PreTask[] = [];
|
||||
private _tapPostTasks: PostTask[] = [];
|
||||
private _tapTests: TapTest<any>[] = [];
|
||||
private _tapTestsOnly: TapTest<any>[] = [];
|
||||
private _currentSuite: ITestSuite | null = null;
|
||||
@@ -304,12 +317,16 @@ export class Tap<T> {
|
||||
this._tapPreTasks.push(new PreTask(descriptionArg, functionArg));
|
||||
}
|
||||
|
||||
public postTask(descriptionArg: string, functionArg: IPostTaskFunction) {
|
||||
this._tapPostTasks.push(new PostTask(descriptionArg, functionArg));
|
||||
}
|
||||
|
||||
/**
|
||||
* A parallel test that will not be waited for before the next starts.
|
||||
* @param testDescription - A description of what the test does
|
||||
* @param testFunction - A Function that returns a Promise and resolves or rejects
|
||||
*/
|
||||
public testParallel(testDescription: string, testFunction: ITestFunction<T>) {
|
||||
public testParallel(testDescription: string, testFunction: ITestFunction<T>): TapTest<T> {
|
||||
const localTest = new TapTest({
|
||||
description: testDescription,
|
||||
testFunction,
|
||||
@@ -330,6 +347,8 @@ export class Tap<T> {
|
||||
} else {
|
||||
this._tapTests.push(localTest);
|
||||
}
|
||||
|
||||
return localTest;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,6 +379,28 @@ export class Tap<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a function to run once before all tests in the current suite
|
||||
*/
|
||||
public beforeAll(setupFunction: ITestFunction<any>) {
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.beforeAll = setupFunction;
|
||||
} else {
|
||||
throw new Error('beforeAll can only be used inside a describe block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a function to run once after all tests in the current suite
|
||||
*/
|
||||
public afterAll(teardownFunction: ITestFunction<any>) {
|
||||
if (this._currentSuite) {
|
||||
this._currentSuite.afterAll = teardownFunction;
|
||||
} else {
|
||||
throw new Error('afterAll can only be used inside a describe block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a function to run before each test in the current suite
|
||||
*/
|
||||
@@ -554,6 +595,11 @@ export class Tap<T> {
|
||||
console.log(failReason);
|
||||
}
|
||||
|
||||
// Run post tasks
|
||||
for (const postTask of this._tapPostTasks) {
|
||||
await postTask.run();
|
||||
}
|
||||
|
||||
// Run global afterAll hook if configured
|
||||
if (settings.afterAll) {
|
||||
try {
|
||||
@@ -597,6 +643,12 @@ export class Tap<T> {
|
||||
suiteName: suite.description
|
||||
}
|
||||
});
|
||||
|
||||
// Run beforeAll hook for this suite
|
||||
if (suite.beforeAll) {
|
||||
await suite.beforeAll(new TapTools(null as any));
|
||||
}
|
||||
|
||||
// Run beforeEach from parent suites
|
||||
const beforeEachFunctions: ITestFunction<any>[] = [];
|
||||
let currentSuite: ITestSuite | null = suite;
|
||||
@@ -667,6 +719,11 @@ export class Tap<T> {
|
||||
// Recursively run child suites
|
||||
await this._runSuite(suite, suite.children, promiseArray, context);
|
||||
|
||||
// Run afterAll hook for this suite
|
||||
if (suite.afterAll) {
|
||||
await suite.afterAll(new TapTools(null as any));
|
||||
}
|
||||
|
||||
// Emit suite:completed event
|
||||
this.emitEvent({
|
||||
eventType: 'suite:completed',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# @git.zone/tstest/tapbundle_node
|
||||
# @git.zone/tstest/tapbundle_serverside
|
||||
|
||||
> 🔧 Node.js-specific testing utilities for enhanced test capabilities
|
||||
> 🔧 Server-side testing utilities for Node.js runtime tests
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# tapbundle_node is included as part of @git.zone/tstest
|
||||
# tapbundle_serverside 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.
|
||||
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools enable shell command execution, environment variable management, HTTPS certificate generation, database testing, object storage testing, and test asset management - all functionality that only makes sense on the server-side.
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -25,11 +25,11 @@ pnpm install --save-dev @git.zone/tstest
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
import { tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
tap.test('should use node-specific tools', async () => {
|
||||
// Use Node.js-specific utilities
|
||||
tap.test('should use server-side tools', async () => {
|
||||
// Execute shell commands on the server-side
|
||||
const result = await tapNodeTools.runCommand('echo "hello"');
|
||||
console.log(result);
|
||||
});
|
||||
@@ -131,7 +131,7 @@ tap.test('should create HTTPS server', async () => {
|
||||
Create an ephemeral MongoDB instance for testing. Automatically started and ready to use.
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
|
||||
tap.test('should use MongoDB', async () => {
|
||||
const mongoInstance = await tapNodeTools.createSmartmongo();
|
||||
@@ -170,7 +170,7 @@ export default tap.start();
|
||||
Create a local S3-compatible storage instance for testing object storage operations.
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
|
||||
tap.test('should use S3 storage', async () => {
|
||||
const s3Instance = await tapNodeTools.createSmarts3();
|
||||
@@ -209,7 +209,7 @@ Utility for downloading and managing test assets.
|
||||
Download the Alpine Linux Docker image as a tarball for testing.
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
|
||||
tap.test('should provide docker image', async () => {
|
||||
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
|
||||
@@ -238,7 +238,7 @@ export default tap.start();
|
||||
The module exports useful path constants:
|
||||
|
||||
```typescript
|
||||
import * as paths from '@git.zone/tstest/tapbundle_node/paths';
|
||||
import * as paths from '@git.zone/tstest/tapbundle_serverside/paths';
|
||||
|
||||
console.log(paths.cwd); // Current working directory
|
||||
console.log(paths.testFilesDir); // ./.nogit/testfiles/
|
||||
@@ -249,7 +249,7 @@ console.log(paths.testFilesDir); // ./.nogit/testfiles/
|
||||
### Testing with External Services
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
tap.describe('User Service Integration', () => {
|
||||
@@ -280,7 +280,7 @@ export default tap.start();
|
||||
### Testing HTTPS Servers
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as https from 'https';
|
||||
|
||||
@@ -311,7 +311,7 @@ export default tap.start();
|
||||
### Environment-Dependent Tests
|
||||
|
||||
```typescript
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
tap.test('should authenticate with GitHub', async () => {
|
||||
@@ -332,12 +332,14 @@ export default tap.start();
|
||||
|
||||
## Runtime Requirements
|
||||
|
||||
⚠️ **Node.js Only**: All utilities in this module require Node.js runtime. They will not work in:
|
||||
⚠️ **Server-Side Only (Node.js)**: All utilities in this module are designed exclusively for server-side testing in Node.js runtime. They provide functionality like shell command execution, file system operations, and process management that only make sense on the server.
|
||||
|
||||
**NOT available in:**
|
||||
- Browser environments
|
||||
- Deno runtime
|
||||
- Bun runtime
|
||||
|
||||
For multi-runtime tests, use these utilities only in `.node.ts` test files.
|
||||
**Important:** Import tapbundle_serverside only in tests that run exclusively on the server-side (`.node.ts` test files). For cross-runtime tests, these utilities will fail in non-Node environments.
|
||||
|
||||
## File Naming
|
||||
|
||||
Reference in New Issue
Block a user