Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc388f1408 | |||
bac2f852c5 | |||
d9e0f1f758 | |||
42cd08eb1c | |||
553d5f0df7 | |||
6cc883dede | |||
fa9abbc4db | |||
56f0f0be16 | |||
dc0f859fad | |||
78ffad2f7d | |||
3fc4cee2b1 | |||
a57edeef64 | |||
1f73751a8c | |||
90741ed917 | |||
962fa2cd4d | |||
c085a20a4f | |||
1f355a10a1 | |||
a73ce99564 |
66
changelog.md
66
changelog.md
@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-15 - 1.5.0 - feat(cli)
|
||||
Improve test runner configuration: update test scripts, reorganize test directories, update dependencies and add local settings for command permissions.
|
||||
|
||||
- Updated package.json scripts to use pnpm and separate commands for tapbundle and tstest.
|
||||
- Reorganized tests into dedicated directories (test/tapbundle and test/tstest) and removed deprecated test files.
|
||||
- Refactored import paths and bumped dependency versions in tapbundle, tstest, and associated node utilities.
|
||||
- Added .claude/settings.local.json to configure local permissions for bash and web fetch commands.
|
||||
- Introduced ts/tspublish.json to define publish order.
|
||||
|
||||
## 2025-05-15 - 1.4.0 - feat(logging)
|
||||
Display failed test console logs in default mode
|
||||
|
||||
- Introduce log buffering in TsTestLogger to capture console output for failed tests
|
||||
- Enhance TapParser to collect and display error details when tests fail
|
||||
- Update README and project plan to document log improvements for debugging
|
||||
|
||||
## 2025-05-15 - 1.3.1 - fix(settings)
|
||||
Add local permissions configuration and remove obsolete test output log
|
||||
|
||||
- Added .claude/settings.local.json to configure allowed permissions for web fetch and bash commands
|
||||
- Removed test-output.log to eliminate accidental commit of test artifacts
|
||||
|
||||
## 2025-05-15 - 1.3.0 - feat(logger)
|
||||
Improve logging output and add --logfile support for persistent logs
|
||||
|
||||
- Add new .claude/settings.local.json with logging permissions configuration
|
||||
- Remove obsolete readme.plan.md
|
||||
- Introduce test/test.console.ts to capture and display console outputs during tests
|
||||
- Update CLI in ts/index.ts to replace '--log-file' with '--logfile' flag
|
||||
- Enhance TsTestLogger to support file logging, clean ANSI sequences, and improved JSON output
|
||||
- Forward TAP protocol logs to testConsoleOutput in TapParser for better console distinction
|
||||
|
||||
## 2025-05-15 - 1.2.0 - feat(logging)
|
||||
Improve logging output, CLI option parsing, and test report formatting.
|
||||
|
||||
- Added a centralized TsTestLogger with support for multiple verbosity levels, JSON output, and file logging (TODO).
|
||||
- Integrated new logger into CLI parsing, TapParser, TapCombinator, and TsTest classes to ensure consistent and structured output.
|
||||
- Introduced new CLI options (--quiet, --verbose, --no-color, --json, --log-file) for enhanced user control.
|
||||
- Enhanced visual design with progress indicators, detailed error aggregation, and performance summaries.
|
||||
- Updated documentation and logging code to align with improved CI/CD behavior, including skipping non-CI tests.
|
||||
|
||||
## 2025-05-15 - 1.1.0 - feat(cli)
|
||||
Enhance test discovery with support for single file and glob pattern execution using improved CLI argument detection
|
||||
|
||||
- Detect execution mode (file, glob, directory) based on CLI input in ts/index.ts
|
||||
- Refactor TestDirectory to load test files using SmartFile for single file and glob patterns
|
||||
- Update TsTest to pass execution mode and adjust test discovery accordingly
|
||||
- Bump dependency versions for typedserver, tsbundle, tapbundle, and others
|
||||
- Add .claude/settings.local.json for updated permissions configuration
|
||||
|
||||
## 2025-01-23 - 1.0.96 - fix(TsTest)
|
||||
Fixed improper type-check for promise-like testModule defaults
|
||||
|
||||
- Corrected the type-check for promise-like default exports in test modules
|
||||
- Removed unnecessary setTimeout used for async execution
|
||||
|
||||
## 2025-01-23 - 1.0.95 - fix(core)
|
||||
Fix delay handling in Chrome test execution
|
||||
|
||||
- Replaced smartdelay.delayFor with native Promise-based delay mechanism in runInChrome method.
|
||||
|
||||
## 2025-01-23 - 1.0.94 - fix(TsTest)
|
||||
Fix test module execution by ensuring promise resolution delay
|
||||
|
||||
- Added a delay to ensure promise resolution when dynamically importing test modules in the runInChrome method.
|
||||
|
||||
## 2025-01-23 - 1.0.93 - fix(tstest)
|
||||
Handle globalThis.tapPromise in browser runtime evaluation
|
||||
|
||||
|
47
package.json
47
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tstest",
|
||||
"version": "1.0.93",
|
||||
"version": "1.5.0",
|
||||
"private": false,
|
||||
"description": "a test utility to run tests that match test/**/*.ts",
|
||||
"main": "dist_ts/index.js",
|
||||
@ -12,32 +12,42 @@
|
||||
"tstest": "./cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "(npm run cleanUp && npm run prepareTest && npm run tstest)",
|
||||
"prepareTest": "git clone https://gitlab.com/sandboxzone/sandbox-npmts.git .nogit/sandbox-npmts && cd .nogit/sandbox-npmts && npm install",
|
||||
"tstest": "cd .nogit/sandbox-npmts && node ../../cli.ts.js test/ --web",
|
||||
"cleanUp": "rm -rf .nogit/sandbox-npmts",
|
||||
"build": "(tsbuild --web --allowimplicitany --skiplibcheck)",
|
||||
"test": "pnpm run build && pnpm run test:tapbundle && pnpm run test:tstest",
|
||||
"test:tapbundle": "tsx ./cli.child.ts test/tapbundle/**/*.ts",
|
||||
"test:tapbundle:verbose": "tsx ./cli.child.ts test/tapbundle/**/*.ts --verbose",
|
||||
"test:tstest": "tsx ./cli.child.ts test/tstest/**/*.ts",
|
||||
"test:tstest:verbose": "tsx ./cli.child.ts test/tstest/**/*.ts --verbose",
|
||||
"build": "(tsbuild tsfolders)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.0",
|
||||
"@types/node": "^22.10.9"
|
||||
"@git.zone/tsbuild": "^2.5.1",
|
||||
"@types/node": "^22.15.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedserver": "^3.0.53",
|
||||
"@git.zone/tsbundle": "^2.1.0",
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@push.rocks/consolecolor": "^2.0.2",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
"@push.rocks/smartbrowser": "^2.0.8",
|
||||
"@push.rocks/smartcrypto": "^2.0.4",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartfile": "^11.1.5",
|
||||
"@push.rocks/smartlog": "^3.0.7",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartshell": "^3.2.2",
|
||||
"@push.rocks/tapbundle": "^5.5.6",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@push.rocks/smartenv": "^5.0.12",
|
||||
"@push.rocks/smartexpect": "^2.4.2",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.1",
|
||||
"@push.rocks/smartmongo": "^2.0.12",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smarts3": "^2.2.5",
|
||||
"@push.rocks/smartshell": "^3.2.3",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"figures": "^6.1.0",
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -53,5 +63,6 @@
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
],
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
3596
pnpm-lock.yaml
generated
3596
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,62 @@
|
||||
# Architecture Overview
|
||||
|
||||
## Project Structure
|
||||
|
||||
This project integrates tstest with tapbundle through a modular architecture:
|
||||
|
||||
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
|
||||
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
|
||||
3. **tapbundle_node** (`/ts_tapbundle_node/`) - Node.js-specific testing utilities
|
||||
|
||||
## How Components Work Together
|
||||
|
||||
### Test Execution Flow
|
||||
|
||||
1. **CLI Entry Point** (`cli.js` <20> `cli.ts.js` <20> `cli.child.ts`)
|
||||
- The CLI uses tsx to run TypeScript files directly
|
||||
- Accepts glob patterns to find test files
|
||||
- Supports options like `--verbose`, `--quiet`, `--web`
|
||||
|
||||
2. **Test Discovery**
|
||||
- tstest scans for test files matching the provided pattern
|
||||
- Defaults to `test/**/*.ts` when no pattern is specified
|
||||
- Supports both file and directory modes
|
||||
|
||||
3. **Test Runner**
|
||||
- Each test file imports `tap` and `expect` from tapbundle
|
||||
- Tests are written using `tap.test()` with async functions
|
||||
- Browser tests are compiled with esbuild and run in Chromium via Puppeteer
|
||||
|
||||
### Key Integration Points
|
||||
|
||||
1. **Import Structure**
|
||||
- Test files import from local tapbundle: `import { tap, expect } from '../../ts_tapbundle/index.js'`
|
||||
- Node-specific tests also import from tapbundle_node: `import { tapNodeTools } from '../../ts_tapbundle_node/index.js'`
|
||||
|
||||
2. **WebHelpers**
|
||||
- Browser tests can use webhelpers for DOM manipulation
|
||||
- `webhelpers.html` - Template literal for creating HTML strings
|
||||
- `webhelpers.fixture` - Creates DOM elements from HTML strings
|
||||
- Automatically detects browser environment and only enables in browser context
|
||||
|
||||
3. **Build System**
|
||||
- Uses `tsbuild tsfolders` to compile TypeScript
|
||||
- Maintains separate output directories: `/dist_ts/`, `/dist_ts_tapbundle/`, `/dist_ts_tapbundle_node/`
|
||||
- Compilation order is resolved automatically based on dependencies
|
||||
|
||||
### Test Scripts
|
||||
|
||||
The package.json defines several test scripts:
|
||||
- `test` - Builds and runs all tests (tapbundle and tstest)
|
||||
- `test:tapbundle` - Runs tapbundle framework tests
|
||||
- `test:tstest` - Runs tstest's own tests
|
||||
- Both support `:verbose` variants for detailed output
|
||||
|
||||
### Environment Detection
|
||||
|
||||
The framework automatically detects the runtime environment:
|
||||
- Node.js tests run directly via tsx
|
||||
- Browser tests are compiled and served via a local server
|
||||
- WebHelpers are only enabled in browser environment
|
||||
|
||||
This architecture allows for seamless testing across both Node.js and browser environments while maintaining a clean separation of concerns.
|
209
readme.md
209
readme.md
@ -1,61 +1,204 @@
|
||||
# @gitzone/tstest
|
||||
a test utility to run tests that match test/**/*.ts
|
||||
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@gitzone/tstest)
|
||||
* [gitlab.com (source)](https://gitlab.com/gitzone/tstest)
|
||||
* [github.com (source mirror)](https://github.com/gitzone/tstest)
|
||||
* [docs (typedoc)](https://gitzone.gitlab.io/tstest/)
|
||||
* [code.foss.global (source)](https://code.foss.global/gitzone/tstest)
|
||||
|
||||
## Status for master
|
||||
## Why tstest?
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
||||
**tstest** is a TypeScript test runner that makes testing delightful. It's designed for modern development workflows with beautiful output, flexible test execution, and powerful features that make debugging a breeze.
|
||||
|
||||
### ✨ Key Features
|
||||
|
||||
- 🎯 **Smart Test Execution** - Run all tests, single files, or use glob patterns
|
||||
- 🎨 **Beautiful Output** - Color-coded results with emojis and clean formatting
|
||||
- 📊 **Multiple Output Modes** - Choose from normal, quiet, verbose, or JSON output
|
||||
- 🔍 **Automatic Discovery** - Finds all your test files automatically
|
||||
- 🌐 **Cross-Environment** - Supports Node.js and browser testing
|
||||
- 📝 **Detailed Logging** - Optional file logging for debugging
|
||||
- ⚡ **Performance Metrics** - See which tests are slow
|
||||
- 🤖 **CI/CD Ready** - JSON output mode for automation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install --save-dev @gitzone/tstest
|
||||
# or with pnpm
|
||||
pnpm add -D @gitzone/tstest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
## cli usage
|
||||
### Basic Test Execution
|
||||
|
||||
lets assume we have a directory called test/ where all our tests arae defined. Simply type
|
||||
|
||||
```
|
||||
```bash
|
||||
# Run all tests in a directory
|
||||
tstest test/
|
||||
|
||||
# Run a specific test file
|
||||
tstest test/test.mycomponent.ts
|
||||
|
||||
# Use glob patterns
|
||||
tstest "test/**/*.spec.ts"
|
||||
tstest "test/unit/*.ts"
|
||||
```
|
||||
|
||||
to run all tests.
|
||||
### Execution Modes
|
||||
|
||||
## Syntax
|
||||
**tstest** intelligently detects how you want to run your tests:
|
||||
|
||||
tstest supports tap syntax. In other words your testfiles are run in a subprocess, and the console output contains trigger messages for tstest to determine test status. Inside your testfile you should use `@pushrocks/tapbundle` for the best results.
|
||||
1. **Directory mode** - Recursively finds all test files
|
||||
2. **File mode** - Runs a single test file
|
||||
3. **Glob mode** - Uses pattern matching for flexible test selection
|
||||
|
||||
## Environments
|
||||
### Command Line Options
|
||||
|
||||
tstest supports different environments:
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--quiet`, `-q` | Minimal output - perfect for CI environments |
|
||||
| `--verbose`, `-v` | Show all console output from tests |
|
||||
| `--no-color` | Disable colored output |
|
||||
| `--json` | Output results as JSON |
|
||||
| `--logfile` | Save detailed logs to `.nogit/testlogs/[testname].log` |
|
||||
|
||||
- a testfile called `test-something.node.ts` will be run in node
|
||||
- a testfile called `test-something.chrome.ts` will be run in chrome environment (bundled through parcel and run through puppeteer)
|
||||
- a testfile called `test-something.both.ts` will be run in node an chrome, which is good for isomorphic packages.
|
||||
### Example Outputs
|
||||
|
||||
> note: there is alpha support for the deno environment by naming a file test-something.deno.ts
|
||||
#### Normal Output (Default)
|
||||
```
|
||||
🔍 Test Discovery
|
||||
Mode: directory
|
||||
Pattern: test
|
||||
Found: 4 test file(s)
|
||||
|
||||
▶️ test/test.ts (1/4)
|
||||
Runtime: node.js
|
||||
✅ prepare test (1ms)
|
||||
Summary: 1/1 PASSED
|
||||
|
||||
📊 Test Summary
|
||||
┌────────────────────────────────┐
|
||||
│ Total Files: 4 │
|
||||
│ Total Tests: 4 │
|
||||
│ Passed: 4 │
|
||||
│ Failed: 0 │
|
||||
│ Duration: 542ms │
|
||||
└────────────────────────────────┘
|
||||
|
||||
ALL TESTS PASSED! 🎉
|
||||
```
|
||||
|
||||
#### Quiet Mode
|
||||
```
|
||||
Found 4 tests
|
||||
✅ test functionality works
|
||||
✅ api calls return expected data
|
||||
✅ error handling works correctly
|
||||
✅ performance is within limits
|
||||
|
||||
Summary: 4/4 | 542ms | PASSED
|
||||
```
|
||||
|
||||
#### Verbose Mode
|
||||
Shows all console output from your tests, making debugging easier:
|
||||
```
|
||||
▶️ test/api.test.ts (1/1)
|
||||
Runtime: node.js
|
||||
Making API call to /users...
|
||||
Response received: 200 OK
|
||||
Processing user data...
|
||||
✅ api calls return expected data (145ms)
|
||||
Summary: 1/1 PASSED
|
||||
```
|
||||
|
||||
#### JSON Mode
|
||||
Perfect for CI/CD pipelines:
|
||||
```json
|
||||
{"event":"discovery","count":4,"pattern":"test","executionMode":"directory"}
|
||||
{"event":"fileStart","filename":"test/test.ts","runtime":"node.js","index":1,"total":4}
|
||||
{"event":"testResult","testName":"prepare test","passed":true,"duration":1}
|
||||
{"event":"summary","summary":{"totalFiles":4,"totalTests":4,"totalPassed":4,"totalFailed":0,"totalDuration":542}}
|
||||
```
|
||||
|
||||
## Test File Naming Conventions
|
||||
|
||||
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
|
||||
|
||||
tstest uses TAP (Test Anything Protocol) for test output. Use `@pushrocks/tapbundle` for the best experience:
|
||||
|
||||
```typescript
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
|
||||
tap.test('my awesome test', async () => {
|
||||
const result = await myFunction();
|
||||
expect(result).toEqual('expected value');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
```
|
||||
|
||||
## 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"
|
||||
```
|
||||
|
||||
### Automatic Logging
|
||||
|
||||
Use `--logfile` to automatically save test output:
|
||||
```bash
|
||||
tstest test/ --logfile
|
||||
```
|
||||
|
||||
This creates detailed logs in `.nogit/testlogs/[testname].log` for each test file.
|
||||
|
||||
### Performance Analysis
|
||||
|
||||
In verbose mode, see performance metrics:
|
||||
```
|
||||
⏱️ Performance Metrics:
|
||||
Average per test: 135ms
|
||||
Slowest test: api integration test (486ms)
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
For continuous integration, combine quiet and JSON modes:
|
||||
```bash
|
||||
# GitHub Actions example
|
||||
tstest test/ --json > test-results.json
|
||||
|
||||
# Or minimal output
|
||||
tstest test/ --quiet
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
## License
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
[](https://maintainedby.lossless.com)
|
41
readme.plan.md
Normal file
41
readme.plan.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Plan for showing logs for failed tests
|
||||
|
||||
!! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !!
|
||||
|
||||
## Goal
|
||||
When a test fails, we want to display all the console logs from that failed test in the terminal, even without the --verbose flag. This makes debugging failed tests much easier.
|
||||
|
||||
## Current Behavior
|
||||
- Default mode: Only shows test results, no console logs
|
||||
- Verbose mode: Shows all console logs from all tests
|
||||
- When a test fails: Only shows the error message
|
||||
|
||||
## Desired Behavior
|
||||
- Default mode: Shows test results, and IF a test fails, shows all console logs from that failed test
|
||||
- Verbose mode: Shows all console logs from all tests (unchanged)
|
||||
- When a test fails: Shows all console logs from that test plus the error
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Update TapParser
|
||||
- Store console logs for each test temporarily
|
||||
- When a test fails, mark that its logs should be shown
|
||||
|
||||
### 2. Update TsTestLogger
|
||||
- Add a new method to handle failed test logs
|
||||
- Modify testConsoleOutput to buffer logs when not in verbose mode
|
||||
- When a test fails, flush the buffered logs for that test
|
||||
|
||||
### 3. Update test result handling
|
||||
- When a test fails, trigger display of all buffered logs for that test
|
||||
- Clear logs after each test completes successfully
|
||||
|
||||
## Code Changes Needed
|
||||
1. Add log buffering to TapParser
|
||||
2. Update TsTestLogger to handle failed test logs
|
||||
3. Modify test result processing to show logs on failure
|
||||
|
||||
## Files to Modify
|
||||
- `ts/tstest.classes.tap.parser.ts` - Add log buffering
|
||||
- `ts/tstest.logging.ts` - Add failed test log handling
|
||||
- `ts/tstest.classes.tap.testresult.ts` - May need to store logs
|
48
test/tapbundle/test.browser.nonci.ts
Normal file
48
test/tapbundle/test.browser.nonci.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { tap, expect, webhelpers } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.preTask('custompretask', async () => {
|
||||
console.log('this is a pretask');
|
||||
});
|
||||
|
||||
tap.test('should have access to webhelpers', async () => {
|
||||
const myElement = await webhelpers.fixture(webhelpers.html`<div></div>`);
|
||||
expect(myElement).toBeInstanceOf(HTMLElement);
|
||||
console.log(myElement);
|
||||
});
|
||||
|
||||
const test1 = tap.test('my first test -> expect true to be true', async () => {
|
||||
return expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
const test2 = tap.test('my second test', async (tools) => {
|
||||
await tools.delayFor(50);
|
||||
});
|
||||
|
||||
const test3 = tap.test(
|
||||
'my third test -> test2 should take longer than test1 and endure at least 1000ms',
|
||||
async () => {
|
||||
expect(
|
||||
(await test1.testPromise).hrtMeasurement.milliSeconds <
|
||||
(await test2).hrtMeasurement.milliSeconds,
|
||||
).toBeTrue();
|
||||
expect((await test2.testPromise).hrtMeasurement.milliSeconds > 10).toBeTrue();
|
||||
},
|
||||
);
|
||||
|
||||
const test4 = tap.skip.test('my 4th test -> should fail', async (tools) => {
|
||||
tools.allowFailure();
|
||||
expect(false).toBeTrue();
|
||||
});
|
||||
|
||||
const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => {
|
||||
tools.timeout(1000);
|
||||
await tools.delayFor(500);
|
||||
});
|
||||
|
||||
const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => {
|
||||
tools.allowFailure();
|
||||
tools.timeout(1000);
|
||||
await tools.delayFor(100);
|
||||
});
|
||||
|
||||
await tap.start();
|
28
test/tapbundle/test.node.ts
Normal file
28
test/tapbundle/test.node.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
import { tapNodeTools } from '../../ts_tapbundle_node/index.js';
|
||||
|
||||
tap.test('should execure a command', async () => {
|
||||
const result = await tapNodeTools.runCommand('ls -la');
|
||||
expect(result.exitCode).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should create a https cert', async () => {
|
||||
const { key, cert } = await tapNodeTools.createHttpsCert('localhost');
|
||||
console.log(key);
|
||||
console.log(cert);
|
||||
expect(key).toInclude('-----BEGIN RSA PRIVATE KEY-----');
|
||||
expect(cert).toInclude('-----BEGIN CERTIFICATE-----');
|
||||
});
|
||||
|
||||
tap.test('should create a smartmongo instance', async () => {
|
||||
const smartmongo = await tapNodeTools.createSmartmongo();
|
||||
await smartmongo.stop();
|
||||
});
|
||||
|
||||
tap.test('should create a smarts3 instance', async () => {
|
||||
const smarts3 = await tapNodeTools.createSmarts3();
|
||||
await smarts3.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
5
test/tapbundle/test.tapwrap.ts
Normal file
5
test/tapbundle/test.tapwrap.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { tap, expect, TapWrap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('should run a test', async () => {});
|
||||
|
||||
tap.start();
|
49
test/tapbundle/test.ts
Normal file
49
test/tapbundle/test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.preTask('hi there', async () => {
|
||||
console.log('this is a pretask');
|
||||
});
|
||||
|
||||
const test1 = tap.test('my first test -> expect true to be true', async () => {
|
||||
return expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
const test2 = tap.test('my second test', async (tools) => {
|
||||
await tools.delayFor(1000);
|
||||
});
|
||||
|
||||
const test3 = tap.test(
|
||||
'my third test -> test2 should take longer than test1 and endure at least 1000ms',
|
||||
async () => {
|
||||
expect(
|
||||
(await test1.testPromise).hrtMeasurement.milliSeconds <
|
||||
(await test2).hrtMeasurement.milliSeconds,
|
||||
).toBeTrue();
|
||||
expect((await test2.testPromise).hrtMeasurement.milliSeconds > 1000).toBeTrue();
|
||||
},
|
||||
);
|
||||
|
||||
const test4 = tap.test('my 4th test -> should fail', async (tools) => {
|
||||
tools.allowFailure();
|
||||
expect(false).toBeFalse();
|
||||
return 'hello';
|
||||
});
|
||||
|
||||
const test5 = tap.test('my 5th test -> should pass in about 500ms', async (tools) => {
|
||||
const test4Result = await test4.testResultPromise;
|
||||
tools.timeout(1000);
|
||||
await tools.delayFor(500);
|
||||
});
|
||||
|
||||
const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (tools) => {
|
||||
tools.allowFailure();
|
||||
tools.timeout(1000);
|
||||
await tools.delayFor(2000);
|
||||
});
|
||||
|
||||
const test7 = tap.test('my 7th test -> should print a colored string', async (tools) => {
|
||||
const cs = await tools.coloredString('hello', 'red', 'cyan');
|
||||
console.log(cs);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -1,6 +0,0 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as tstest from '../ts/index.js';
|
||||
|
||||
tap.test('prepare test', async () => {});
|
||||
|
||||
tap.start();
|
8
test/tstest/subdir/test.sub.ts
Normal file
8
test/tstest/subdir/test.sub.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect, tap } from '../../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('subdirectory test execution', async () => {
|
||||
console.log('This test verifies subdirectory test discovery works');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
11
test/tstest/test.console.ts
Normal file
11
test/tstest/test.console.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('Test with console output', async () => {
|
||||
console.log('Log message 1 from test');
|
||||
console.log('Log message 2 from test');
|
||||
console.error('Error message from test');
|
||||
console.warn('Warning message from test');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
13
test/tstest/test.fail.ts
Normal file
13
test/tstest/test.fail.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('This test should fail', async () => {
|
||||
console.log('This test will fail on purpose');
|
||||
expect(true).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('This test should pass', async () => {
|
||||
console.log('This test will pass');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
23
test/tstest/test.failing-with-logs.ts
Normal file
23
test/tstest/test.failing-with-logs.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('Test that will fail with console logs', async () => {
|
||||
console.log('Starting the test...');
|
||||
console.log('Doing some setup work');
|
||||
console.log('About to check assertion');
|
||||
|
||||
const value = 42;
|
||||
console.log(`The value is: ${value}`);
|
||||
|
||||
// This will fail
|
||||
expect(value).toEqual(100);
|
||||
|
||||
console.log('This log will not be reached');
|
||||
});
|
||||
|
||||
tap.test('Test that passes', async () => {
|
||||
console.log('This test passes');
|
||||
console.log('So these logs should not show in default mode');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
8
test/tstest/test.glob.ts
Normal file
8
test/tstest/test.glob.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('glob pattern test execution', async () => {
|
||||
console.log('This test verifies glob pattern execution works');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
8
test/tstest/test.single.ts
Normal file
8
test/tstest/test.single.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
|
||||
tap.test('single file test execution', async () => {
|
||||
console.log('This test verifies single file execution works');
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
6
test/tstest/test.ts
Normal file
6
test/tstest/test.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||
import * as tstest from '../../ts/index.js';
|
||||
|
||||
tap.test('prepare test', async () => {});
|
||||
|
||||
tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '1.0.93',
|
||||
version: '1.5.0',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
67
ts/index.ts
67
ts/index.ts
@ -1,10 +1,71 @@
|
||||
import { TsTest } from './tstest.classes.tstest.js';
|
||||
import type { LogOptions } from './tstest.logging.js';
|
||||
|
||||
export enum TestExecutionMode {
|
||||
DIRECTORY = 'directory',
|
||||
FILE = 'file',
|
||||
GLOB = 'glob'
|
||||
}
|
||||
|
||||
export const runCli = async () => {
|
||||
if (!process.argv[2]) {
|
||||
console.error('You must specify a test directory as argument. Please try again.');
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const logOptions: LogOptions = {};
|
||||
let testPath: string | null = null;
|
||||
|
||||
// Parse options
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
switch (arg) {
|
||||
case '--quiet':
|
||||
case '-q':
|
||||
logOptions.quiet = true;
|
||||
break;
|
||||
case '--verbose':
|
||||
case '-v':
|
||||
logOptions.verbose = true;
|
||||
break;
|
||||
case '--no-color':
|
||||
logOptions.noColor = true;
|
||||
break;
|
||||
case '--json':
|
||||
logOptions.json = true;
|
||||
break;
|
||||
case '--log-file':
|
||||
case '--logfile':
|
||||
logOptions.logFile = true; // Set this as a flag, not a value
|
||||
break;
|
||||
default:
|
||||
if (!arg.startsWith('-')) {
|
||||
testPath = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!testPath) {
|
||||
console.error('You must specify a test directory/file/pattern as argument. Please try again.');
|
||||
console.error('\nUsage: tstest <path> [options]');
|
||||
console.error('\nOptions:');
|
||||
console.error(' --quiet, -q Minimal output');
|
||||
console.error(' --verbose, -v Verbose output');
|
||||
console.error(' --no-color Disable colored output');
|
||||
console.error(' --json Output results as JSON');
|
||||
console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log');
|
||||
process.exit(1);
|
||||
}
|
||||
const tsTestInstance = new TsTest(process.cwd(), process.argv[2]);
|
||||
|
||||
let executionMode: TestExecutionMode;
|
||||
|
||||
// Detect execution mode based on the argument
|
||||
if (testPath.includes('*') || testPath.includes('?') || testPath.includes('[') || testPath.includes('{')) {
|
||||
executionMode = TestExecutionMode.GLOB;
|
||||
} else if (testPath.endsWith('.ts')) {
|
||||
executionMode = TestExecutionMode.FILE;
|
||||
} else {
|
||||
executionMode = TestExecutionMode.DIRECTORY;
|
||||
}
|
||||
|
||||
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions);
|
||||
await tsTestInstance.run();
|
||||
};
|
||||
|
3
ts/tspublish.json
Normal file
3
ts/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 2
|
||||
}
|
@ -6,59 +6,37 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import * as logPrefixes from './tstest.logprefixes.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
|
||||
export class TapCombinator {
|
||||
tapParserStore: TapParser[] = [];
|
||||
private logger: TsTestLogger;
|
||||
|
||||
constructor(logger: TsTestLogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
addTapParser(tapParserArg: TapParser) {
|
||||
this.tapParserStore.push(tapParserArg);
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
console.log(
|
||||
`${logPrefixes.TsTestPrefix} RESULTS FOR ${this.tapParserStore.length} TESTFILE(S):`
|
||||
);
|
||||
|
||||
let failGlobal = false; // determine wether tstest should fail
|
||||
// Call the logger's summary method
|
||||
this.logger.summary();
|
||||
|
||||
// Check for failures
|
||||
let failGlobal = false;
|
||||
for (const tapParser of this.tapParserStore) {
|
||||
if (!tapParser.expectedTests) {
|
||||
if (!tapParser.expectedTests ||
|
||||
tapParser.expectedTests !== tapParser.receivedTests ||
|
||||
tapParser.getErrorTests().length > 0) {
|
||||
failGlobal = true;
|
||||
let overviewString =
|
||||
logPrefixes.TsTestPrefix +
|
||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
||||
` ${plugins.figures.pointer} ` +
|
||||
`does not specify tests!`;
|
||||
console.log(overviewString);
|
||||
} else if (tapParser.expectedTests !== tapParser.receivedTests) {
|
||||
failGlobal = true;
|
||||
let overviewString =
|
||||
logPrefixes.TsTestPrefix +
|
||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
||||
` ${plugins.figures.pointer} ` +
|
||||
tapParser.getTestOverviewAsString() +
|
||||
`did not execute all specified tests!`;
|
||||
console.log(overviewString);
|
||||
} else if (tapParser.getErrorTests().length === 0) {
|
||||
let overviewString =
|
||||
logPrefixes.TsTestPrefix +
|
||||
cs(` ${tapParser.fileName} ${plugins.figures.tick}`, 'green') +
|
||||
` ${plugins.figures.pointer} ` +
|
||||
tapParser.getTestOverviewAsString();
|
||||
console.log(overviewString);
|
||||
} else {
|
||||
failGlobal = true;
|
||||
let overviewString =
|
||||
logPrefixes.TsTestPrefix +
|
||||
cs(` ${tapParser.fileName} ${plugins.figures.cross}`, 'red') +
|
||||
` ${plugins.figures.pointer} ` +
|
||||
tapParser.getTestOverviewAsString();
|
||||
console.log(overviewString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log(cs(plugins.figures.hamburger.repeat(48), 'cyan'));
|
||||
if (!failGlobal) {
|
||||
console.log(cs('FINAL RESULT: SUCCESS!', 'green'));
|
||||
} else {
|
||||
console.log(cs('FINAL RESULT: FAIL!', 'red'));
|
||||
|
||||
// Exit with error code if tests failed
|
||||
if (failGlobal) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { TapTestResult } from './tstest.classes.tap.testresult.js';
|
||||
import * as logPrefixes from './tstest.logprefixes.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
|
||||
export class TapParser {
|
||||
testStore: TapTestResult[] = [];
|
||||
@ -17,13 +18,19 @@ export class TapParser {
|
||||
|
||||
testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*)\s#\stime=(.*)ms$/;
|
||||
activeTapTestResult: TapTestResult;
|
||||
collectingErrorDetails: boolean = false;
|
||||
currentTestError: string[] = [];
|
||||
|
||||
pretaskRegex = /^::__PRETASK:(.*)$/;
|
||||
|
||||
private logger: TsTestLogger;
|
||||
|
||||
/**
|
||||
* the constructor for TapParser
|
||||
*/
|
||||
constructor(public fileName: string) {}
|
||||
constructor(public fileName: string, logger?: TsTestLogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private _getNewTapTestResult() {
|
||||
this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
|
||||
@ -45,9 +52,9 @@ export class TapParser {
|
||||
logLineIsTapProtocol = true;
|
||||
const regexResult = this.expectedTestsRegex.exec(logLine);
|
||||
this.expectedTests = parseInt(regexResult[2]);
|
||||
console.log(
|
||||
`${logPrefixes.TapPrefix} ${cs(`Expecting ${this.expectedTests} tests!`, 'blue')}`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
|
||||
}
|
||||
|
||||
// initiating first TapResult
|
||||
this._getNewTapTestResult();
|
||||
@ -55,7 +62,9 @@ export class TapParser {
|
||||
logLineIsTapProtocol = true;
|
||||
const pretaskContentMatch = this.pretaskRegex.exec(logLine);
|
||||
if (pretaskContentMatch && pretaskContentMatch[1]) {
|
||||
console.log(`${logPrefixes.TapPretaskPrefix} Pretask ->${pretaskContentMatch[1]}: Success.`);
|
||||
if (this.logger) {
|
||||
this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
|
||||
}
|
||||
}
|
||||
} else if (this.testStatusRegex.test(logLine)) {
|
||||
logLineIsTapProtocol = true;
|
||||
@ -73,26 +82,23 @@ export class TapParser {
|
||||
|
||||
// test for protocol error
|
||||
if (testId !== this.activeTapTestResult.id) {
|
||||
console.log(
|
||||
`${logPrefixes.TapErrorPrefix} Something is strange! Test Ids are not equal!`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.error('Something is strange! Test Ids are not equal!');
|
||||
}
|
||||
}
|
||||
this.activeTapTestResult.setTestResult(testOk);
|
||||
|
||||
if (testOk) {
|
||||
console.log(
|
||||
logPrefixes.TapPrefix,
|
||||
`${cs(`T${testId} ${plugins.figures.tick}`, 'green')} ${plugins.figures.arrowRight} ` +
|
||||
cs(testSubject, 'blue') +
|
||||
` | ${cs(`${testDuration} ms`, 'orange')}`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.testResult(testSubject, true, testDuration);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
logPrefixes.TapPrefix,
|
||||
`${cs(`T${testId} ${plugins.figures.cross}`, 'red')} ${plugins.figures.arrowRight} ` +
|
||||
cs(testSubject, 'blue') +
|
||||
` | ${cs(`${testDuration} ms`, 'orange')}`
|
||||
);
|
||||
// Start collecting error details for failed test
|
||||
this.collectingErrorDetails = true;
|
||||
this.currentTestError = [];
|
||||
if (this.logger) {
|
||||
this.logger.testResult(testSubject, false, testDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,10 +106,43 @@ export class TapParser {
|
||||
if (this.activeTapTestResult) {
|
||||
this.activeTapTestResult.addLogLine(logLine);
|
||||
}
|
||||
console.log(logLine);
|
||||
|
||||
// Check if we're collecting error details
|
||||
if (this.collectingErrorDetails) {
|
||||
// Check if this line is an error detail (starts with Error: or has stack trace characteristics)
|
||||
if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
|
||||
this.currentTestError.push(logLine);
|
||||
} else if (this.currentTestError.length > 0) {
|
||||
// End of error details, show the error
|
||||
const errorMessage = this.currentTestError.join('\n');
|
||||
if (this.logger) {
|
||||
this.logger.testErrorDetails(errorMessage);
|
||||
}
|
||||
this.collectingErrorDetails = false;
|
||||
this.currentTestError = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Don't output TAP error details as console output when we're collecting them
|
||||
if (!this.collectingErrorDetails || (!logLine.trim().startsWith('Error:') && !logLine.trim().match(/^\s*at\s/))) {
|
||||
if (this.logger) {
|
||||
// This is console output from the test file, not TAP protocol
|
||||
this.logger.testConsoleOutput(logLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
|
||||
// Ensure any pending error is shown before settling the test
|
||||
if (this.collectingErrorDetails && this.currentTestError.length > 0) {
|
||||
const errorMessage = this.currentTestError.join('\n');
|
||||
if (this.logger) {
|
||||
this.logger.testErrorDetails(errorMessage);
|
||||
}
|
||||
this.collectingErrorDetails = false;
|
||||
this.currentTestError = [];
|
||||
}
|
||||
|
||||
this.testStore.push(this.activeTapTestResult);
|
||||
this._getNewTapTestResult();
|
||||
}
|
||||
@ -172,38 +211,32 @@ export class TapParser {
|
||||
|
||||
// check wether all tests ran
|
||||
if (this.expectedTests === this.receivedTests) {
|
||||
console.log(
|
||||
`${logPrefixes.TapPrefix} ${cs(
|
||||
`${this.receivedTests} out of ${this.expectedTests} Tests completed!`,
|
||||
'green'
|
||||
)}`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`${logPrefixes.TapErrorPrefix} ${cs(
|
||||
`Only ${this.receivedTests} out of ${this.expectedTests} completed!`,
|
||||
'red'
|
||||
)}`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
|
||||
}
|
||||
}
|
||||
if (!this.expectedTests) {
|
||||
console.log(cs('Error: No tests were defined. Therefore the testfile failed!', 'red'));
|
||||
if (this.logger) {
|
||||
this.logger.error('No tests were defined. Therefore the testfile failed!');
|
||||
}
|
||||
} else if (this.expectedTests !== this.receivedTests) {
|
||||
console.log(
|
||||
cs(
|
||||
'Error: The amount of received tests and expectedTests is unequal! Therefore the testfile failed',
|
||||
'red'
|
||||
)
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
|
||||
}
|
||||
} else if (this.getErrorTests().length === 0) {
|
||||
console.log(`${logPrefixes.TapPrefix} ${cs(`All tests are successfull!!!`, 'green')}`);
|
||||
if (this.logger) {
|
||||
this.logger.tapOutput('All tests are successfull!!!');
|
||||
this.logger.testFileEnd(this.receivedTests, 0, 0);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`${logPrefixes.TapPrefix} ${cs(
|
||||
`${this.getErrorTests().length} tests threw an error!!!`,
|
||||
'red'
|
||||
)}`
|
||||
);
|
||||
if (this.logger) {
|
||||
this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
|
||||
this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import * as paths from './tstest.paths.js';
|
||||
import { SmartFile } from '@push.rocks/smartfile';
|
||||
import { TestExecutionMode } from './index.js';
|
||||
|
||||
// tap related stuff
|
||||
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
||||
@ -14,14 +15,14 @@ export class TestDirectory {
|
||||
cwd: string;
|
||||
|
||||
/**
|
||||
* the relative location of the test dir
|
||||
* the test path or pattern
|
||||
*/
|
||||
relativePath: string;
|
||||
testPath: string;
|
||||
|
||||
/**
|
||||
* the absolute path of the test dir
|
||||
* the execution mode
|
||||
*/
|
||||
absolutePath: string;
|
||||
executionMode: TestExecutionMode;
|
||||
|
||||
/**
|
||||
* an array of Smartfiles
|
||||
@ -30,27 +31,71 @@ export class TestDirectory {
|
||||
|
||||
/**
|
||||
* the constructor for TestDirectory
|
||||
* tell it the path
|
||||
* @param pathToTestDirectory
|
||||
* @param cwdArg - the current working directory
|
||||
* @param testPathArg - the test path/pattern
|
||||
* @param executionModeArg - the execution mode
|
||||
*/
|
||||
constructor(cwdArg: string, relativePathToTestDirectory: string) {
|
||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode) {
|
||||
this.cwd = cwdArg;
|
||||
this.relativePath = relativePathToTestDirectory;
|
||||
this.testPath = testPathArg;
|
||||
this.executionMode = executionModeArg;
|
||||
}
|
||||
|
||||
private async _init() {
|
||||
this.testfileArray = await plugins.smartfile.fs.fileTreeToObject(
|
||||
plugins.path.join(this.cwd, this.relativePath),
|
||||
'test*.ts'
|
||||
);
|
||||
switch (this.executionMode) {
|
||||
case TestExecutionMode.FILE:
|
||||
// Single file mode
|
||||
const filePath = plugins.path.isAbsolute(this.testPath)
|
||||
? this.testPath
|
||||
: plugins.path.join(this.cwd, this.testPath);
|
||||
|
||||
if (await plugins.smartfile.fs.fileExists(filePath)) {
|
||||
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)];
|
||||
} else {
|
||||
throw new Error(`Test file not found: ${filePath}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case TestExecutionMode.GLOB:
|
||||
// Glob pattern mode - use listFileTree which supports glob patterns
|
||||
const globPattern = this.testPath;
|
||||
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern);
|
||||
|
||||
this.testfileArray = await Promise.all(
|
||||
matchedFiles.map(async (filePath) => {
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: plugins.path.join(this.cwd, filePath);
|
||||
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
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);
|
||||
|
||||
this.testfileArray = await Promise.all(
|
||||
testFiles.map(async (filePath) => {
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: plugins.path.join(dirPath, filePath);
|
||||
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async getTestFilePathArray() {
|
||||
await this._init();
|
||||
const testFilePaths: string[] = [];
|
||||
for (const testFile of this.testfileArray) {
|
||||
const filePath = plugins.path.join(this.relativePath, testFile.path);
|
||||
testFilePaths.push(filePath);
|
||||
// Use the path directly from the SmartFile
|
||||
testFilePaths.push(testFile.path);
|
||||
}
|
||||
return testFilePaths;
|
||||
}
|
||||
|
@ -7,9 +7,14 @@ import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import { TestDirectory } from './tstest.classes.testdirectory.js';
|
||||
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TestExecutionMode } from './index.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { LogOptions } from './tstest.logging.js';
|
||||
|
||||
export class TsTest {
|
||||
public testDir: TestDirectory;
|
||||
public executionMode: TestExecutionMode;
|
||||
public logger: TsTestLogger;
|
||||
|
||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
@ -20,61 +25,57 @@ export class TsTest {
|
||||
|
||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||
|
||||
constructor(cwdArg: string, relativePathToTestDirectory: string) {
|
||||
this.testDir = new TestDirectory(cwdArg, relativePathToTestDirectory);
|
||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}) {
|
||||
this.executionMode = executionModeArg;
|
||||
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||
this.logger = new TsTestLogger(logOptions);
|
||||
}
|
||||
|
||||
async run() {
|
||||
const fileNamesToRun: string[] = await this.testDir.getTestFilePathArray();
|
||||
console.log(cs(plugins.figures.hamburger.repeat(80), 'cyan'));
|
||||
console.log('');
|
||||
console.log(`${logPrefixes.TsTestPrefix} FOUND ${fileNamesToRun.length} TESTFILE(S):`);
|
||||
for (const fileName of fileNamesToRun) {
|
||||
console.log(`${logPrefixes.TsTestPrefix} ${cs(fileName, 'orange')}`);
|
||||
}
|
||||
console.log('-'.repeat(48));
|
||||
console.log(''); // force new line
|
||||
|
||||
// Log test discovery
|
||||
this.logger.testDiscovery(
|
||||
fileNamesToRun.length,
|
||||
this.testDir.testPath,
|
||||
this.executionMode
|
||||
);
|
||||
|
||||
const tapCombinator = new TapCombinator(); // lets create the TapCombinator
|
||||
const tapCombinator = new TapCombinator(this.logger); // lets create the TapCombinator
|
||||
let fileIndex = 0;
|
||||
for (const fileNameArg of fileNamesToRun) {
|
||||
fileIndex++;
|
||||
switch (true) {
|
||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||
console.log('!!!!!!!!!!!');
|
||||
console.log(
|
||||
`not running testfile ${fileNameArg}, since we are CI and file name includes '.nonci.' tag`
|
||||
);
|
||||
console.log('!!!!!!!!!!!');
|
||||
this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
|
||||
break;
|
||||
case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
|
||||
const tapParserBrowser = await this.runInChrome(fileNameArg);
|
||||
const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserBrowser);
|
||||
break;
|
||||
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
||||
console.log('>>>>>>> TEST PART 1: chrome');
|
||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg);
|
||||
this.logger.sectionStart('Part 1: Chrome');
|
||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserBothBrowser);
|
||||
console.log(cs(`|`.repeat(16), 'cyan'));
|
||||
console.log(''); // force new line
|
||||
console.log('>>>>>>> TEST PART 2: node');
|
||||
const tapParserBothNode = await this.runInNode(fileNameArg);
|
||||
this.logger.sectionEnd();
|
||||
|
||||
this.logger.sectionStart('Part 2: Node');
|
||||
const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserBothNode);
|
||||
this.logger.sectionEnd();
|
||||
break;
|
||||
default:
|
||||
const tapParserNode = await this.runInNode(fileNameArg);
|
||||
const tapParserNode = await this.runInNode(fileNameArg, fileIndex, fileNamesToRun.length);
|
||||
tapCombinator.addTapParser(tapParserNode);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(cs(`^`.repeat(16), 'cyan'));
|
||||
console.log(''); // force new line
|
||||
}
|
||||
tapCombinator.evaluate();
|
||||
}
|
||||
|
||||
public async runInNode(fileNameArg: string): Promise<TapParser> {
|
||||
console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in node.js runtime.`);
|
||||
console.log(`${cs(`= `.repeat(32), 'cyan')}`);
|
||||
const tapParser = new TapParser(fileNameArg + ':node');
|
||||
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);
|
||||
|
||||
// tsrun options
|
||||
let tsrunOptions = '';
|
||||
@ -89,9 +90,8 @@ export class TsTest {
|
||||
return tapParser;
|
||||
}
|
||||
|
||||
public async runInChrome(fileNameArg: string): Promise<TapParser> {
|
||||
console.log(`${cs('=> ', 'blue')} Running ${cs(fileNameArg, 'orange')} in chromium runtime.`);
|
||||
console.log(`${cs(`= `.repeat(32), 'cyan')}`);
|
||||
public async runInChrome(fileNameArg: string, index: number, total: number): Promise<TapParser> {
|
||||
this.logger.testFileStart(fileNameArg, 'chromium', index, total);
|
||||
|
||||
// lets get all our paths sorted
|
||||
const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
|
||||
@ -130,11 +130,17 @@ export class TsTest {
|
||||
await server.start();
|
||||
|
||||
// lets handle realtime comms
|
||||
const tapParser = new TapParser(fileNameArg + ':chrome');
|
||||
const tapParser = new TapParser(fileNameArg + ':chrome', this.logger);
|
||||
const wss = new plugins.ws.WebSocketServer({ port: 8080 });
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
tapParser.handleTapLog(message.toString());
|
||||
const messageStr = message.toString();
|
||||
if (messageStr.startsWith('console:')) {
|
||||
const [, level, ...messageParts] = messageStr.split(':');
|
||||
this.logger.browserConsole(messageParts.join(':'), level);
|
||||
} else {
|
||||
tapParser.handleTapLog(messageStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -173,12 +179,12 @@ export class TsTest {
|
||||
if (testModule && testModule.default && testModule.default instanceof Promise) {
|
||||
// Execute the exported test function
|
||||
await testModule.default;
|
||||
} else if (testModule && testModule.default && testModule.default.then === 'function') {
|
||||
} else if (testModule && testModule.default && typeof testModule.default.then === 'function') {
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.log('Test module default export is just promiselike: Something might be messing with your Promise implementation.');
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
await testModule.default;
|
||||
} else if (globalThis.tapPromise && globalThis.tapPromise.then === 'function') {
|
||||
} else if (globalThis.tapPromise && typeof globalThis.tapPromise.then === 'function') {
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.log('Using globalThis.tapPromise');
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
|
399
ts/tstest.logging.ts
Normal file
399
ts/tstest.logging.ts
Normal file
@ -0,0 +1,399 @@
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface LogOptions {
|
||||
quiet?: boolean;
|
||||
verbose?: boolean;
|
||||
noColor?: boolean;
|
||||
json?: boolean;
|
||||
logFile?: boolean;
|
||||
}
|
||||
|
||||
export interface TestFileResult {
|
||||
file: string;
|
||||
passed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
duration: number;
|
||||
tests: Array<{
|
||||
name: string;
|
||||
passed: boolean;
|
||||
duration: number;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface TestSummary {
|
||||
totalFiles: number;
|
||||
totalTests: number;
|
||||
totalPassed: number;
|
||||
totalFailed: number;
|
||||
totalDuration: number;
|
||||
fileResults: TestFileResult[];
|
||||
}
|
||||
|
||||
export class TsTestLogger {
|
||||
private options: LogOptions;
|
||||
private startTime: number;
|
||||
private fileResults: TestFileResult[] = [];
|
||||
private currentFileResult: TestFileResult | null = null;
|
||||
private currentTestLogFile: string | null = null;
|
||||
private currentTestLogs: string[] = []; // Buffer for current test logs
|
||||
private currentTestFailed: boolean = false;
|
||||
|
||||
constructor(options: LogOptions = {}) {
|
||||
this.options = options;
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
private format(text: string, color?: string): string {
|
||||
if (this.options.noColor || !color) {
|
||||
return text;
|
||||
}
|
||||
return cs(text, color as any);
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
if (this.options.json) {
|
||||
// For JSON mode, skip console output
|
||||
// JSON output is handled by logJson method
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(message);
|
||||
|
||||
// Log to the current test file log if we're in a test and --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(message);
|
||||
}
|
||||
}
|
||||
|
||||
private logToFile(message: string) {
|
||||
// This method is no longer used since we use logToTestFile for individual test logs
|
||||
// Keeping it for potential future use with a global log file
|
||||
}
|
||||
|
||||
private logToTestFile(message: string) {
|
||||
try {
|
||||
// Remove ANSI color codes for file logging
|
||||
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
|
||||
|
||||
// Append to test log file
|
||||
fs.appendFileSync(this.currentTestLogFile, cleanMessage + '\n');
|
||||
} catch (error) {
|
||||
// Silently fail to avoid disrupting the test run
|
||||
}
|
||||
}
|
||||
|
||||
private logJson(data: any) {
|
||||
const jsonString = JSON.stringify(data);
|
||||
console.log(jsonString);
|
||||
|
||||
// Also log to test file if --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(jsonString);
|
||||
}
|
||||
}
|
||||
|
||||
// Section separators
|
||||
sectionStart(title: string) {
|
||||
if (this.options.quiet || this.options.json) return;
|
||||
this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
|
||||
}
|
||||
|
||||
sectionEnd() {
|
||||
if (this.options.quiet || this.options.json) return;
|
||||
this.log(this.format('─'.repeat(50), 'dim'));
|
||||
}
|
||||
|
||||
// Progress indication
|
||||
progress(current: number, total: number, message: string) {
|
||||
if (this.options.quiet || this.options.json) return;
|
||||
const percentage = Math.round((current / total) * 100);
|
||||
const filled = Math.round((current / total) * 20);
|
||||
const empty = 20 - filled;
|
||||
|
||||
this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
|
||||
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
||||
}
|
||||
|
||||
// Test discovery
|
||||
testDiscovery(count: number, pattern: string, executionMode: string) {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'discovery', count, pattern, executionMode });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) {
|
||||
this.log(`Found ${count} tests`);
|
||||
} else {
|
||||
this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
|
||||
this.log(this.format(` Mode: ${executionMode}`, 'dim'));
|
||||
this.log(this.format(` Pattern: ${pattern}`, 'dim'));
|
||||
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
||||
}
|
||||
}
|
||||
|
||||
// Test execution
|
||||
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
||||
this.currentFileResult = {
|
||||
file: filename,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
duration: 0,
|
||||
tests: []
|
||||
};
|
||||
|
||||
// Reset test-specific state
|
||||
this.currentTestLogs = [];
|
||||
this.currentTestFailed = false;
|
||||
|
||||
// Only set up test log file if --logfile option is specified
|
||||
if (this.options.logFile) {
|
||||
const baseFilename = path.basename(filename, '.ts');
|
||||
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${baseFilename}.log`);
|
||||
|
||||
// Ensure the directory exists
|
||||
const logDir = path.dirname(this.currentTestLogFile);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Clear the log file for this test
|
||||
fs.writeFileSync(this.currentTestLogFile, '');
|
||||
}
|
||||
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'fileStart', filename, runtime, index, total });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) return;
|
||||
|
||||
this.log(this.format(`\n▶️ ${filename} (${index}/${total})`, 'blue'));
|
||||
this.log(this.format(` Runtime: ${runtime}`, 'dim'));
|
||||
}
|
||||
|
||||
testResult(testName: string, passed: boolean, duration: number, error?: string) {
|
||||
if (this.currentFileResult) {
|
||||
this.currentFileResult.tests.push({ name: testName, passed, duration, error });
|
||||
this.currentFileResult.total++;
|
||||
if (passed) {
|
||||
this.currentFileResult.passed++;
|
||||
} else {
|
||||
this.currentFileResult.failed++;
|
||||
this.currentTestFailed = true;
|
||||
}
|
||||
this.currentFileResult.duration += duration;
|
||||
}
|
||||
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'testResult', testName, passed, duration, error });
|
||||
return;
|
||||
}
|
||||
|
||||
// If test failed and we have buffered logs, show them now
|
||||
if (!passed && this.currentTestLogs.length > 0 && !this.options.verbose) {
|
||||
this.log(this.format(' 📋 Console output from failed test:', 'yellow'));
|
||||
this.currentTestLogs.forEach(logMessage => {
|
||||
this.log(this.format(` ${logMessage}`, 'dim'));
|
||||
});
|
||||
}
|
||||
|
||||
const icon = passed ? '✅' : '❌';
|
||||
const color = passed ? 'green' : 'red';
|
||||
|
||||
if (this.options.quiet) {
|
||||
this.log(`${icon} ${testName}`);
|
||||
} else {
|
||||
this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
|
||||
if (error && !passed) {
|
||||
this.log(this.format(` ${error}`, 'red'));
|
||||
}
|
||||
}
|
||||
|
||||
// Clear logs after each test
|
||||
this.currentTestLogs = [];
|
||||
}
|
||||
|
||||
testFileEnd(passed: number, failed: number, duration: number) {
|
||||
if (this.currentFileResult) {
|
||||
this.fileResults.push(this.currentFileResult);
|
||||
this.currentFileResult = null;
|
||||
}
|
||||
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'fileEnd', passed, failed, duration });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.quiet) {
|
||||
const total = passed + failed;
|
||||
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
||||
const color = failed === 0 ? 'green' : 'red';
|
||||
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
||||
}
|
||||
|
||||
// Clear the current test log file reference only if using --logfile
|
||||
if (this.options.logFile) {
|
||||
this.currentTestLogFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
// TAP output forwarding (for TAP protocol messages)
|
||||
tapOutput(message: string, isError: boolean = false) {
|
||||
if (this.options.json) return;
|
||||
|
||||
// Never show raw TAP protocol messages in console
|
||||
// They are already processed by TapParser and shown in our format
|
||||
|
||||
// Always log to test file if --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(` ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Console output from test files (non-TAP output)
|
||||
testConsoleOutput(message: string) {
|
||||
if (this.options.json) return;
|
||||
|
||||
// In verbose mode, show console output immediately
|
||||
if (this.options.verbose) {
|
||||
this.log(this.format(` ${message}`, 'dim'));
|
||||
} else {
|
||||
// In non-verbose mode, buffer the logs
|
||||
this.currentTestLogs.push(message);
|
||||
}
|
||||
|
||||
// Always log to test file if --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(` ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Browser console
|
||||
browserConsole(message: string, level: string = 'log') {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'browserConsole', message, level });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.quiet) {
|
||||
const prefix = level === 'error' ? '🌐❌' : '🌐';
|
||||
const color = level === 'error' ? 'red' : 'magenta';
|
||||
this.log(this.format(` ${prefix} ${message}`, color));
|
||||
}
|
||||
}
|
||||
|
||||
// Test error details display
|
||||
testErrorDetails(errorMessage: string) {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'testError', error: errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.quiet) {
|
||||
this.log(this.format(' Error details:', 'red'));
|
||||
errorMessage.split('\n').forEach(line => {
|
||||
this.log(this.format(` ${line}`, 'red'));
|
||||
});
|
||||
}
|
||||
|
||||
// Always log to test file if --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(` Error: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Final summary
|
||||
summary() {
|
||||
const totalDuration = Date.now() - this.startTime;
|
||||
const summary: TestSummary = {
|
||||
totalFiles: this.fileResults.length,
|
||||
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
||||
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
||||
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
||||
totalDuration,
|
||||
fileResults: this.fileResults
|
||||
};
|
||||
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'summary', summary });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) {
|
||||
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
||||
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Detailed summary
|
||||
this.log(this.format('\n📊 Test Summary', 'bold'));
|
||||
this.log(this.format('┌────────────────────────────────┐', 'dim'));
|
||||
this.log(this.format(`│ Total Files: ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
|
||||
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
||||
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
||||
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
||||
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
||||
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
||||
|
||||
// File results
|
||||
if (summary.totalFailed > 0) {
|
||||
this.log(this.format('\n❌ Failed Tests:', 'red'));
|
||||
this.fileResults.forEach(fileResult => {
|
||||
if (fileResult.failed > 0) {
|
||||
this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
|
||||
fileResult.tests.filter(t => !t.passed).forEach(test => {
|
||||
this.log(this.format(` ❌ ${test.name}`, 'red'));
|
||||
if (test.error) {
|
||||
this.log(this.format(` ${test.error}`, 'dim'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Performance metrics
|
||||
if (this.options.verbose) {
|
||||
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
||||
const slowestTest = this.fileResults
|
||||
.flatMap(r => r.tests)
|
||||
.sort((a, b) => b.duration - a.duration)[0];
|
||||
|
||||
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
||||
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
||||
if (slowestTest) {
|
||||
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
||||
}
|
||||
}
|
||||
|
||||
// Final status
|
||||
const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
|
||||
const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
|
||||
this.log(this.format(`\n${status}`, statusColor));
|
||||
}
|
||||
|
||||
// Error display
|
||||
error(message: string, file?: string, stack?: string) {
|
||||
if (this.options.json) {
|
||||
this.logJson({ event: 'error', message, file, stack });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.quiet) {
|
||||
console.error(`ERROR: ${message}`);
|
||||
} else {
|
||||
this.log(this.format('\n⚠️ Error', 'red'));
|
||||
if (file) this.log(this.format(` File: ${file}`, 'yellow'));
|
||||
this.log(this.format(` ${message}`, 'red'));
|
||||
if (stack && this.options.verbose) {
|
||||
this.log(this.format(` Stack:`, 'dim'));
|
||||
this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
import * as tapbundle from '@push.rocks/tapbundle';
|
||||
import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
||||
|
||||
export {
|
||||
consolecolor,
|
||||
|
8
ts_tapbundle/00_commitinfo_data.ts
Normal file
8
ts_tapbundle/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/tapbundle',
|
||||
version: '6.0.3',
|
||||
description: 'A comprehensive testing automation library that provides a wide range of utilities and tools for TAP (Test Anything Protocol) based testing, especially suitable for projects using tapbuffer.'
|
||||
}
|
7
ts_tapbundle/index.ts
Normal file
7
ts_tapbundle/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { tap } from './tapbundle.classes.tap.js';
|
||||
export { TapWrap } from './tapbundle.classes.tapwrap.js';
|
||||
export { webhelpers } from './webhelpers.js';
|
||||
|
||||
import { expect } from '@push.rocks/smartexpect';
|
||||
|
||||
export { expect };
|
21
ts_tapbundle/tapbundle.classes.pretask.ts
Normal file
21
ts_tapbundle/tapbundle.classes.pretask.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||
|
||||
export interface IPreTaskFunction {
|
||||
(tapTools?: TapTools): Promise<any>;
|
||||
}
|
||||
|
||||
export class PreTask {
|
||||
public description: string;
|
||||
public preTaskFunction: IPreTaskFunction;
|
||||
|
||||
constructor(descriptionArg: string, preTaskFunctionArg: IPreTaskFunction) {
|
||||
this.description = descriptionArg;
|
||||
this.preTaskFunction = preTaskFunctionArg;
|
||||
}
|
||||
|
||||
public async run() {
|
||||
console.log(`::__PRETASK: ${this.description}`);
|
||||
await this.preTaskFunction(new TapTools(null));
|
||||
}
|
||||
}
|
173
ts_tapbundle/tapbundle.classes.tap.ts
Normal file
173
ts_tapbundle/tapbundle.classes.tap.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
|
||||
import { type IPreTaskFunction, PreTask } from './tapbundle.classes.pretask.js';
|
||||
import { TapTest, type ITestFunction } from './tapbundle.classes.taptest.js';
|
||||
export class Tap<T> {
|
||||
/**
|
||||
* skips a test
|
||||
* tests marked with tap.skip.test() are never executed
|
||||
*/
|
||||
public skip = {
|
||||
test: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
},
|
||||
testParallel: (descriptionArg: string, functionArg: ITestFunction<T>) => {
|
||||
console.log(`skipped test: ${descriptionArg}`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* only executes tests marked as ONLY
|
||||
*/
|
||||
public only = {
|
||||
test: (descriptionArg: string, testFunctionArg: ITestFunction<T>) => {
|
||||
this.test(descriptionArg, testFunctionArg, 'only');
|
||||
},
|
||||
};
|
||||
|
||||
private _tapPreTasks: PreTask[] = [];
|
||||
private _tapTests: TapTest<any>[] = [];
|
||||
private _tapTestsOnly: TapTest<any>[] = [];
|
||||
|
||||
/**
|
||||
* Normal test function, will run one by one
|
||||
* @param testDescription - A description of what the test does
|
||||
* @param testFunction - A Function that returns a Promise and resolves or rejects
|
||||
*/
|
||||
public test(
|
||||
testDescription: string,
|
||||
testFunction: ITestFunction<T>,
|
||||
modeArg: 'normal' | 'only' | 'skip' = 'normal',
|
||||
): TapTest<T> {
|
||||
const localTest = new TapTest<T>({
|
||||
description: testDescription,
|
||||
testFunction,
|
||||
parallel: false,
|
||||
});
|
||||
if (modeArg === 'normal') {
|
||||
this._tapTests.push(localTest);
|
||||
} else if (modeArg === 'only') {
|
||||
this._tapTestsOnly.push(localTest);
|
||||
}
|
||||
return localTest;
|
||||
}
|
||||
|
||||
public preTask(descriptionArg: string, functionArg: IPreTaskFunction) {
|
||||
this._tapPreTasks.push(new PreTask(descriptionArg, functionArg));
|
||||
}
|
||||
|
||||
/**
|
||||
* A parallel test that will not be waited for before the next starts.
|
||||
* @param testDescription - A description of what the test does
|
||||
* @param testFunction - A Function that returns a Promise and resolves or rejects
|
||||
*/
|
||||
public testParallel(testDescription: string, testFunction: ITestFunction<T>) {
|
||||
this._tapTests.push(
|
||||
new TapTest({
|
||||
description: testDescription,
|
||||
testFunction,
|
||||
parallel: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the test evaluation
|
||||
*/
|
||||
public async start(optionsArg?: { throwOnError: boolean }) {
|
||||
// lets set the tapbundle promise
|
||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||
smartenvInstance.isBrowser
|
||||
? ((globalThis as any).tapbundleDeferred = plugins.smartpromise.defer())
|
||||
: null;
|
||||
|
||||
// lets continue with running the tests
|
||||
const promiseArray: Array<Promise<any>> = [];
|
||||
|
||||
// safeguard against empty test array
|
||||
if (this._tapTests.length === 0) {
|
||||
console.log('no tests specified. Ending here!');
|
||||
// TODO: throw proper error
|
||||
return;
|
||||
}
|
||||
|
||||
// determine which tests to run
|
||||
let concerningTests: TapTest[];
|
||||
if (this._tapTestsOnly.length > 0) {
|
||||
concerningTests = this._tapTestsOnly;
|
||||
} else {
|
||||
concerningTests = this._tapTests;
|
||||
}
|
||||
|
||||
// lets run the pretasks
|
||||
for (const preTask of this._tapPreTasks) {
|
||||
await preTask.run();
|
||||
}
|
||||
|
||||
console.log(`1..${concerningTests.length}`);
|
||||
for (let testKey = 0; testKey < concerningTests.length; testKey++) {
|
||||
const currentTest = concerningTests[testKey];
|
||||
const testPromise = currentTest.run(testKey);
|
||||
if (currentTest.parallel) {
|
||||
promiseArray.push(testPromise);
|
||||
} else {
|
||||
await testPromise;
|
||||
}
|
||||
}
|
||||
await Promise.all(promiseArray);
|
||||
|
||||
// when tests have been run and all promises are fullfilled
|
||||
const failReasons: string[] = [];
|
||||
const executionNotes: string[] = [];
|
||||
// collect failed tests
|
||||
for (const tapTest of concerningTests) {
|
||||
if (tapTest.status !== 'success') {
|
||||
failReasons.push(
|
||||
`Test ${tapTest.testKey + 1} failed with status ${tapTest.status}:\n` +
|
||||
`|| ${tapTest.description}\n` +
|
||||
`|| for more information please take a look the logs above`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// render fail Reasons
|
||||
for (const failReason of failReasons) {
|
||||
console.log(failReason);
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.throwOnError && failReasons.length > 0) {
|
||||
if (!smartenvInstance.isBrowser) process.exit(1);
|
||||
}
|
||||
if (smartenvInstance.isBrowser) {
|
||||
(globalThis as any).tapbundleDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public async stopForcefully(codeArg = 0, directArg = false) {
|
||||
console.log(`tap stopping forcefully! Code: ${codeArg} / Direct: ${directArg}`);
|
||||
if (directArg) {
|
||||
process.exit(codeArg);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
process.exit(codeArg);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle errors
|
||||
*/
|
||||
public threw(err: Error) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly fail the current test with a custom message
|
||||
* @param message - The failure message to display
|
||||
*/
|
||||
public fail(message: string = 'Test failed'): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export let tap = new Tap();
|
87
ts_tapbundle/tapbundle.classes.taptest.ts
Normal file
87
ts_tapbundle/tapbundle.classes.taptest.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { tapCreator } from './tapbundle.tapcreator.js';
|
||||
import { TapTools } from './tapbundle.classes.taptools.js';
|
||||
|
||||
// imported interfaces
|
||||
import { Deferred } from '@push.rocks/smartpromise';
|
||||
import { HrtMeasurement } from '@push.rocks/smarttime';
|
||||
|
||||
// interfaces
|
||||
export type TTestStatus = 'success' | 'error' | 'pending' | 'errorAfterSuccess' | 'timeout';
|
||||
|
||||
export interface ITestFunction<T> {
|
||||
(tapTools?: TapTools): Promise<T>;
|
||||
}
|
||||
|
||||
export class TapTest<T = unknown> {
|
||||
public description: string;
|
||||
public failureAllowed: boolean;
|
||||
public hrtMeasurement: HrtMeasurement;
|
||||
public parallel: boolean;
|
||||
public status: TTestStatus;
|
||||
public tapTools: TapTools;
|
||||
public testFunction: ITestFunction<T>;
|
||||
public testKey: number; // the testKey the position in the test qeue. Set upon calling .run()
|
||||
private testDeferred: Deferred<TapTest<T>> = plugins.smartpromise.defer();
|
||||
public testPromise: Promise<TapTest<T>> = this.testDeferred.promise;
|
||||
private testResultDeferred: Deferred<T> = plugins.smartpromise.defer();
|
||||
public testResultPromise: Promise<T> = this.testResultDeferred.promise;
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
constructor(optionsArg: {
|
||||
description: string;
|
||||
testFunction: ITestFunction<T>;
|
||||
parallel: boolean;
|
||||
}) {
|
||||
this.description = optionsArg.description;
|
||||
this.hrtMeasurement = new HrtMeasurement();
|
||||
this.parallel = optionsArg.parallel;
|
||||
this.status = 'pending';
|
||||
this.tapTools = new TapTools(this);
|
||||
this.testFunction = optionsArg.testFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* run the test
|
||||
*/
|
||||
public async run(testKeyArg: number) {
|
||||
this.hrtMeasurement.start();
|
||||
this.testKey = testKeyArg;
|
||||
const testNumber = testKeyArg + 1;
|
||||
try {
|
||||
const testReturnValue = await this.testFunction(this.tapTools);
|
||||
if (this.status === 'timeout') {
|
||||
throw new Error('Test succeeded, but timed out...');
|
||||
}
|
||||
this.hrtMeasurement.stop();
|
||||
console.log(
|
||||
`ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`,
|
||||
);
|
||||
this.status = 'success';
|
||||
this.testDeferred.resolve(this);
|
||||
this.testResultDeferred.resolve(testReturnValue);
|
||||
} catch (err: any) {
|
||||
this.hrtMeasurement.stop();
|
||||
console.log(
|
||||
`not ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`,
|
||||
);
|
||||
this.testDeferred.resolve(this);
|
||||
this.testResultDeferred.resolve(err);
|
||||
|
||||
// if the test has already succeeded before
|
||||
if (this.status === 'success') {
|
||||
this.status = 'errorAfterSuccess';
|
||||
console.log('!!! ALERT !!!: weird behaviour, since test has been already successfull');
|
||||
} else {
|
||||
this.status = 'error';
|
||||
}
|
||||
|
||||
// if the test is allowed to fail
|
||||
if (this.failureAllowed) {
|
||||
console.log(`please note: failure allowed!`);
|
||||
}
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
68
ts_tapbundle/tapbundle.classes.taptools.ts
Normal file
68
ts_tapbundle/tapbundle.classes.taptools.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { TapTest } from './tapbundle.classes.taptest.js';
|
||||
|
||||
export interface IPromiseFunc {
|
||||
(): Promise<any>;
|
||||
}
|
||||
|
||||
export class TapTools {
|
||||
/**
|
||||
* the referenced TapTest
|
||||
*/
|
||||
private _tapTest: TapTest;
|
||||
|
||||
constructor(TapTestArg: TapTest<any>) {
|
||||
this._tapTest = TapTestArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* allow failure
|
||||
*/
|
||||
public allowFailure() {
|
||||
this._tapTest.failureAllowed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* async/await delay method
|
||||
*/
|
||||
public async delayFor(timeMilliArg: number) {
|
||||
await plugins.smartdelay.delayFor(timeMilliArg);
|
||||
}
|
||||
|
||||
public async delayForRandom(timeMilliMinArg: number, timeMilliMaxArg: number) {
|
||||
await plugins.smartdelay.delayForRandom(timeMilliMinArg, timeMilliMaxArg);
|
||||
}
|
||||
|
||||
public async coloredString(...args: Parameters<typeof plugins.consolecolor.coloredString>) {
|
||||
return plugins.consolecolor.coloredString(...args);
|
||||
}
|
||||
|
||||
public async timeout(timeMilliArg: number) {
|
||||
const timeout = new plugins.smartdelay.Timeout(timeMilliArg);
|
||||
timeout.makeUnrefed();
|
||||
await timeout.promise;
|
||||
if (this._tapTest.status === 'pending') {
|
||||
this._tapTest.status = 'timeout';
|
||||
}
|
||||
}
|
||||
|
||||
public async returnError(throwingFuncArg: IPromiseFunc) {
|
||||
let funcErr: Error;
|
||||
try {
|
||||
await throwingFuncArg();
|
||||
} catch (err: any) {
|
||||
funcErr = err;
|
||||
}
|
||||
return funcErr;
|
||||
}
|
||||
|
||||
public defer() {
|
||||
return plugins.smartpromise.defer();
|
||||
}
|
||||
|
||||
public cumulativeDefer() {
|
||||
return plugins.smartpromise.cumulativeDefer();
|
||||
}
|
||||
|
||||
public smartjson = plugins.smartjson;
|
||||
}
|
13
ts_tapbundle/tapbundle.classes.tapwrap.ts
Normal file
13
ts_tapbundle/tapbundle.classes.tapwrap.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
|
||||
export interface ITapWrapOptions {
|
||||
before: () => Promise<any>;
|
||||
after: () => {};
|
||||
}
|
||||
|
||||
export class TapWrap {
|
||||
public options: ITapWrapOptions;
|
||||
constructor(optionsArg: ITapWrapOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
}
|
9
ts_tapbundle/tapbundle.plugins.ts
Normal file
9
ts_tapbundle/tapbundle.plugins.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// pushrocks
|
||||
import * as consolecolor from '@push.rocks/consolecolor';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartenv from '@push.rocks/smartenv';
|
||||
import * as smartexpect from '@push.rocks/smartexpect';
|
||||
import * as smartjson from '@push.rocks/smartjson';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export { consolecolor, smartdelay, smartenv, smartexpect, smartjson, smartpromise };
|
7
ts_tapbundle/tapbundle.tapcreator.ts
Normal file
7
ts_tapbundle/tapbundle.tapcreator.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
|
||||
export class TapCreator {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
export let tapCreator = new TapCreator();
|
3
ts_tapbundle/tspublish.json
Normal file
3
ts_tapbundle/tspublish.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 1
|
||||
}
|
40
ts_tapbundle/webhelpers.ts
Normal file
40
ts_tapbundle/webhelpers.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as plugins from './tapbundle.plugins.js';
|
||||
import { tap } from './tapbundle.classes.tap.js';
|
||||
|
||||
class WebHelpers {
|
||||
html: any;
|
||||
fixture: any;
|
||||
|
||||
constructor() {
|
||||
const smartenv = new plugins.smartenv.Smartenv();
|
||||
|
||||
// Initialize HTML template tag function
|
||||
this.html = (strings: TemplateStringsArray, ...values: any[]) => {
|
||||
let result = '';
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
result += strings[i];
|
||||
if (i < values.length) {
|
||||
result += values[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Initialize fixture function based on environment
|
||||
if (smartenv.isBrowser) {
|
||||
this.fixture = async (htmlString: string): Promise<HTMLElement> => {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = htmlString.trim();
|
||||
const element = container.firstChild as HTMLElement;
|
||||
return element;
|
||||
};
|
||||
} else {
|
||||
// Node.js environment - provide a stub or alternative implementation
|
||||
this.fixture = async (htmlString: string): Promise<any> => {
|
||||
throw new Error('WebHelpers.fixture is only available in browser environment');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const webhelpers = new WebHelpers();
|
98
ts_tapbundle_node/classes.tapnodetools.ts
Normal file
98
ts_tapbundle_node/classes.tapnodetools.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { TestFileProvider } from './classes.testfileprovider.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
class TapNodeTools {
|
||||
private smartshellInstance: plugins.smartshell.Smartshell;
|
||||
public testFileProvider = new TestFileProvider();
|
||||
|
||||
constructor() {}
|
||||
|
||||
private qenv: plugins.qenv.Qenv;
|
||||
public async getQenv(): Promise<plugins.qenv.Qenv> {
|
||||
this.qenv = this.qenv || new plugins.qenv.Qenv('./', '.nogit/');
|
||||
return this.qenv;
|
||||
}
|
||||
public async getEnvVarOnDemand(envVarNameArg: string): Promise<string> {
|
||||
const qenv = await this.getQenv();
|
||||
return qenv.getEnvVarOnDemand(envVarNameArg);
|
||||
}
|
||||
|
||||
public async runCommand(commandArg: string): Promise<any> {
|
||||
if (!this.smartshellInstance) {
|
||||
this.smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
}
|
||||
const result = await this.smartshellInstance.exec(commandArg);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async createHttpsCert(
|
||||
commonName: string = 'localhost',
|
||||
allowSelfSigned: boolean = true
|
||||
): Promise<{ key: string; cert: string }> {
|
||||
if (allowSelfSigned) {
|
||||
// set node to allow self-signed certificates
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
}
|
||||
|
||||
// Generate a key pair
|
||||
const keys = plugins.smartcrypto.nodeForge.pki.rsa.generateKeyPair(2048);
|
||||
|
||||
// Create a self-signed certificate
|
||||
const cert = plugins.smartcrypto.nodeForge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
const attrs = [
|
||||
{ name: 'commonName', value: commonName },
|
||||
{ name: 'countryName', value: 'US' },
|
||||
{ shortName: 'ST', value: 'California' },
|
||||
{ name: 'localityName', value: 'San Francisco' },
|
||||
{ name: 'organizationName', value: 'My Company' },
|
||||
{ shortName: 'OU', value: 'Dev' },
|
||||
];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
|
||||
// Sign the certificate with its own private key (self-signed)
|
||||
cert.sign(keys.privateKey, plugins.smartcrypto.nodeForge.md.sha256.create());
|
||||
|
||||
// PEM encode the private key and certificate
|
||||
const pemKey = plugins.smartcrypto.nodeForge.pki.privateKeyToPem(keys.privateKey);
|
||||
const pemCert = plugins.smartcrypto.nodeForge.pki.certificateToPem(cert);
|
||||
|
||||
return {
|
||||
key: pemKey,
|
||||
cert: pemCert,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* create and return a smartmongo instance
|
||||
*/
|
||||
public async createSmartmongo() {
|
||||
const smartmongoMod = await import('@push.rocks/smartmongo');
|
||||
const smartmongoInstance = new smartmongoMod.SmartMongo();
|
||||
await smartmongoInstance.start();
|
||||
return smartmongoInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* create and return a smarts3 instance
|
||||
*/
|
||||
public async createSmarts3() {
|
||||
const smarts3Mod = await import('@push.rocks/smarts3');
|
||||
const smarts3Instance = new smarts3Mod.Smarts3({
|
||||
port: 3003,
|
||||
cleanSlate: true,
|
||||
});
|
||||
await smarts3Instance.start();
|
||||
return smarts3Instance;
|
||||
}
|
||||
}
|
||||
|
||||
export const tapNodeTools = new TapNodeTools();
|
17
ts_tapbundle_node/classes.testfileprovider.ts
Normal file
17
ts_tapbundle_node/classes.testfileprovider.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
|
||||
export const fileUrls = {
|
||||
dockerAlpineImage: 'https://code.foss.global/testassets/docker/raw/branch/main/alpine.tar',
|
||||
}
|
||||
|
||||
export class TestFileProvider {
|
||||
public async getDockerAlpineImageAsLocalTarball(): Promise<string> {
|
||||
const filePath = plugins.path.join(paths.testFilesDir, 'alpine.tar')
|
||||
// fetch the docker alpine image
|
||||
const response = await plugins.smartrequest.getBinary(fileUrls.dockerAlpineImage);
|
||||
await plugins.smartfile.fs.ensureDir(paths.testFilesDir);
|
||||
await plugins.smartfile.memory.toFs(response.body, filePath);
|
||||
return filePath;
|
||||
}
|
||||
}
|
2
ts_tapbundle_node/index.ts
Normal file
2
ts_tapbundle_node/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './classes.tapnodetools.js';
|
||||
|
4
ts_tapbundle_node/paths.ts
Normal file
4
ts_tapbundle_node/paths.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const cwd = process.cwd();
|
||||
export const testFilesDir = plugins.path.join(cwd, './.nogit/testfiles/');
|
16
ts_tapbundle_node/plugins.ts
Normal file
16
ts_tapbundle_node/plugins.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// node native
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export { crypto,fs, path, };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
|
||||
export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, };
|
Reference in New Issue
Block a user